mirror of
https://github.com/Alfresco/alfresco-ng2-components.git
synced 2025-07-24 17:32:15 +00:00
ACA-3426 - Search Headers for Document List (#5800)
* [ACA-3426] Move filter-menu inside search and renamed as search-header * [ACA-3426] adf-search-header removed from document-list and implemented in app-files * [ACA-3426] Allow custom header filters inside document-list * [ACA-3426] Decouple search from the document-list * [ACA-3409] NodePaging ouputed to the DL * [ACA-3426] - fixed injection for service * Dev baptiste aca 3430 (#5773) * [ACA-3430] Add style to filter and hide action buttons from facet widgets * [ACA-3430] Update eventEmitter created in the DL and create unit tests for the search-header Co-authored-by: BaptisteMahe <mahe.baptiste.19@gmail.com> * [ACA-3426] - added parent for service * [ACA-3426] - added parent for service - fixed method * [ACA-3426] Revert update EventEmitter inside DL * [ACA-3436] Use of the node input instead of nodeUpdate mehtod * [ACA-3426] Add clear behaviour to search-header * [ACA-3426] Remove useless update exposition * [ACA-3426] Update filter button styles and padding inside the filter menu * [ACA-3443] Propagate filters states through DL and datatable to avoid hiding the header * [ACA-3426] Refactor showHeader logic and use it for the filters * [ACA-3426] - fixed pagination for filter result * [ACA-3426] - fixed messed files after rebase * [ACA-3426] - added simplified config version * [ACA-3426] - enabling created by filter * [ACA-3426] Fix search-date-range apply method * [ACA-3426] Fix loading style and default showHeaderMode * [ACA-3426] Changed showHedaer default to always * [ACA-3426] - stabilised the feature and added injection token * [ACA-3426] Add unit test for showHeader new behaviour * [ACA-3426] Add documentation to search-header * [ACA-3426] - added parent filtering for special folders * [ACA-3426] - added unit test for search header * [ACA-3426] - fixed search fitler behavour * [ACA-3426] - fixed search result inject service * [ACA-3426] - fixed search result inject service for search sorting * [ACA-3426] - fixed title for matching selector * [ACA-3426] - fixed app config with missing search widget * Update search-header.component.md Co-authored-by: BaptisteMahe <mahe.baptiste.19@gmail.com> Co-authored-by: Eugenio Romano <eromano@users.noreply.github.com>
This commit is contained in:
@@ -11,7 +11,7 @@
|
||||
[loading]="loading"
|
||||
[display]="display"
|
||||
[noPermission]="noPermission"
|
||||
[showHeader]="!isEmpty() && showHeader"
|
||||
[showHeader]="showHeader"
|
||||
[rowMenuCacheEnabled]="false"
|
||||
[stickyHeader]="stickyHeader"
|
||||
[allowFiltering]="allowFiltering"
|
||||
@@ -24,9 +24,13 @@
|
||||
(row-unselect)="onNodeUnselect($event.detail)"
|
||||
[class.adf-datatable-gallery-thumbnails]="data.thumbnails">
|
||||
|
||||
<ng-template let-col>
|
||||
<adf-filter-menu class="adf-filter-menu" [col]="col"></adf-filter-menu>
|
||||
</ng-template>
|
||||
<adf-header-filter-template>
|
||||
<ng-template let-col>
|
||||
<ng-template [ngTemplateOutlet]="customHeaderFilterTemplate?.template"
|
||||
[ngTemplateOutletContext]="{$implicit: col}">
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
</adf-header-filter-template>
|
||||
|
||||
<adf-no-content-template>
|
||||
<ng-template>
|
||||
|
@@ -207,11 +207,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.adf-filter-button {
|
||||
mat-icon {
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -25,7 +25,8 @@ import {
|
||||
DataColumn,
|
||||
DataTableComponent,
|
||||
DataTableModule,
|
||||
ObjectDataTableAdapter
|
||||
ObjectDataTableAdapter,
|
||||
ShowHeaderMode
|
||||
} from '@alfresco/adf-core';
|
||||
import { Subject, of, throwError } from 'rxjs';
|
||||
import {
|
||||
@@ -330,7 +331,7 @@ describe('DocumentList', () => {
|
||||
});
|
||||
|
||||
it('should hide the header if showHeader is false', () => {
|
||||
documentList.showHeader = false;
|
||||
documentList.showHeader = ShowHeaderMode.Data;
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
@@ -338,7 +339,7 @@ describe('DocumentList', () => {
|
||||
});
|
||||
|
||||
it('should show the header if showHeader is true', () => {
|
||||
documentList.showHeader = true;
|
||||
documentList.showHeader = ShowHeaderMode.Data;
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
|
@@ -30,6 +30,7 @@ import {
|
||||
DataSorting,
|
||||
DataTableComponent,
|
||||
DisplayMode,
|
||||
ShowHeaderMode,
|
||||
ObjectDataColumn,
|
||||
PaginatedComponent,
|
||||
AppConfigService,
|
||||
@@ -40,6 +41,7 @@ import {
|
||||
CustomLoadingContentTemplateDirective,
|
||||
CustomNoPermissionTemplateDirective,
|
||||
CustomEmptyContentTemplateDirective,
|
||||
CustomHeaderFilterTemplateDirective,
|
||||
RequestPaginationModel,
|
||||
AlfrescoApiService,
|
||||
UserPreferenceValues,
|
||||
@@ -91,6 +93,9 @@ export class DocumentListComponent implements OnInit, OnChanges, OnDestroy, Afte
|
||||
@ContentChild(CustomEmptyContentTemplateDirective)
|
||||
customNoContentTemplate: CustomEmptyContentTemplateDirective;
|
||||
|
||||
@ContentChild(CustomHeaderFilterTemplateDirective)
|
||||
customHeaderFilterTemplate: CustomHeaderFilterTemplateDirective;
|
||||
|
||||
/** Include additional information about the node in the server request. For example: association, isLink, isLocked and others. */
|
||||
@Input()
|
||||
includeFields: string[];
|
||||
@@ -123,7 +128,7 @@ export class DocumentListComponent implements OnInit, OnChanges, OnDestroy, Afte
|
||||
|
||||
/** Toggles the header */
|
||||
@Input()
|
||||
showHeader: boolean = true;
|
||||
showHeader: string = ShowHeaderMode.Data;
|
||||
|
||||
/** User interaction for folder navigation or file preview.
|
||||
* Valid values are "click" and "dblclick". Default value: "dblclick"
|
||||
@@ -308,7 +313,7 @@ export class DocumentListComponent implements OnInit, OnChanges, OnDestroy, Afte
|
||||
noPermission: boolean = false;
|
||||
selection = new Array<NodeEntry>();
|
||||
$folderNode: Subject<Node> = new Subject<Node>();
|
||||
allowFiltering: boolean = false;
|
||||
allowFiltering: boolean = true;
|
||||
|
||||
// @deprecated 3.0.0
|
||||
folderNode: Node;
|
||||
@@ -854,5 +859,4 @@ export class DocumentListComponent implements OnInit, OnChanges, OnDestroy, Afte
|
||||
this.setLoadingState(false);
|
||||
this.error.emit(err);
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -1,16 +0,0 @@
|
||||
<button mat-icon-button [matMenuTriggerFor]="filter"
|
||||
(click)="onMenuButtonClick($event)"
|
||||
class="adf-filter-button"
|
||||
matTooltip="{{ 'ADF-DOCUMENT-LIST.FILTER_MENU.FILTER_BY' | translate:{category: col.title} }}">
|
||||
<adf-icon value="adf:filter" color="primary"></adf-icon>
|
||||
</button>
|
||||
|
||||
<mat-menu #filter="matMenu">
|
||||
<div>{{col.title}}</div>
|
||||
<mat-dialog-actions>
|
||||
<button mat-button>{{ 'ADF-DOCUMENT-LIST.FILTER_MENU.CLEAR' | translate | uppercase }}
|
||||
</button>
|
||||
<button mat-button>{{ 'ADF-DOCUMENT-LIST.FILTER_MENU.APPLY' | translate | uppercase }}
|
||||
</button>
|
||||
</mat-dialog-actions>
|
||||
</mat-menu>
|
@@ -32,7 +32,6 @@ import { LibraryStatusColumnComponent } from './components/library-status-column
|
||||
import { LibraryRoleColumnComponent } from './components/library-role-column/library-role-column.component';
|
||||
import { LibraryNameColumnComponent } from './components/library-name-column/library-name-column.component';
|
||||
import { NameColumnComponent } from './components/name-column/name-column.component';
|
||||
import { FilterMenuComponent } from './components/filter-menu/filter-menu.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@@ -51,8 +50,7 @@ import { FilterMenuComponent } from './components/filter-menu/filter-menu.compon
|
||||
LibraryNameColumnComponent,
|
||||
NameColumnComponent,
|
||||
ContentActionComponent,
|
||||
ContentActionListComponent,
|
||||
FilterMenuComponent
|
||||
ContentActionListComponent
|
||||
],
|
||||
exports: [
|
||||
DocumentListComponent,
|
||||
|
@@ -62,11 +62,6 @@
|
||||
"VIEW": "View",
|
||||
"REMOVE": "Remove",
|
||||
"DOWNLOAD": "Download"
|
||||
},
|
||||
"FILTER_MENU" : {
|
||||
"FILTER_BY": "Filter by {{ category }}",
|
||||
"CLEAR": "Clear",
|
||||
"APPLY": "Apply"
|
||||
}
|
||||
},
|
||||
"ALFRESCO_DOCUMENT_LIST": {
|
||||
@@ -289,6 +284,12 @@
|
||||
"PROCESS_ACTION": "Start Process"
|
||||
}
|
||||
}
|
||||
},
|
||||
"SEARCH_HEADER" : {
|
||||
"TITLE":"Filter",
|
||||
"FILTER_BY": "Filter by {{ category }}",
|
||||
"CLEAR": "Clear",
|
||||
"APPLY": "Apply"
|
||||
}
|
||||
},
|
||||
"PERMISSION": {
|
||||
|
@@ -36,7 +36,8 @@ import {
|
||||
MatSlideToggleModule,
|
||||
MatRadioModule,
|
||||
MatSliderModule,
|
||||
MatTreeModule
|
||||
MatTreeModule,
|
||||
MatBadgeModule
|
||||
} from '@angular/material';
|
||||
|
||||
export function modules() {
|
||||
@@ -60,7 +61,8 @@ export function modules() {
|
||||
MatSlideToggleModule,
|
||||
MatRadioModule,
|
||||
MatSliderModule,
|
||||
MatTreeModule
|
||||
MatTreeModule,
|
||||
MatBadgeModule
|
||||
];
|
||||
}
|
||||
|
||||
|
@@ -15,9 +15,9 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Node } from '@alfresco/js-api';
|
||||
import { Node, NodePaging } from '@alfresco/js-api';
|
||||
|
||||
export const fakeNodeWithCreatePermission = new Node({
|
||||
export const fakeNodeWithCreatePermission = new Node({
|
||||
isFile: false,
|
||||
createdByUser: { id: 'admin', displayName: 'Administrator' },
|
||||
modifiedAt: '2017-06-08T13:53:46.495Z',
|
||||
@@ -193,3 +193,25 @@ export const fakeGetSiteMembership = {
|
||||
}]
|
||||
}
|
||||
};
|
||||
|
||||
export const fakeNodePaging: NodePaging = {
|
||||
list: {
|
||||
pagination: {
|
||||
count: 5,
|
||||
hasMoreItems: false,
|
||||
totalItems: 5,
|
||||
skipCount: 0,
|
||||
maxItems: 100
|
||||
}, entries: [{
|
||||
entry: fakeNodeWithNoPermission
|
||||
}, {
|
||||
entry: fakeNodeWithNoPermission
|
||||
}, {
|
||||
entry: fakeNodeWithNoPermission
|
||||
}, {
|
||||
entry: fakeNodeWithNoPermission
|
||||
}, {
|
||||
entry: fakeNodeWithNoPermission
|
||||
}]
|
||||
}
|
||||
};
|
||||
|
@@ -0,0 +1,439 @@
|
||||
/*!
|
||||
* @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 { Injectable } from '@angular/core';
|
||||
import { Subject } from 'rxjs';
|
||||
import { AlfrescoApiService, AppConfigService } from '@alfresco/adf-core';
|
||||
import {
|
||||
QueryBody,
|
||||
RequestFacetFields,
|
||||
RequestFacetField,
|
||||
RequestSortDefinitionInner,
|
||||
ResultSetPaging,
|
||||
RequestHighlight
|
||||
} from '@alfresco/js-api';
|
||||
import { SearchCategory } from './search-category.interface';
|
||||
import { FilterQuery } from './filter-query.interface';
|
||||
import { SearchRange } from './search-range.interface';
|
||||
import { SearchConfiguration } from './search-configuration.interface';
|
||||
import { FacetQuery } from './facet-query.interface';
|
||||
import { SearchSortingDefinition } from './search-sorting-definition.interface';
|
||||
import { FacetField } from './facet-field.interface';
|
||||
import { FacetFieldBucket } from './facet-field-bucket.interface';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export abstract class BaseQueryBuilderService {
|
||||
|
||||
private _userQuery = '';
|
||||
|
||||
updated = new Subject<QueryBody>();
|
||||
executed = new Subject<ResultSetPaging>();
|
||||
error = new Subject();
|
||||
|
||||
categories: Array<SearchCategory> = [];
|
||||
queryFragments: { [id: string]: string } = {};
|
||||
filterQueries: FilterQuery[] = [];
|
||||
paging: { maxItems?: number; skipCount?: number } = null;
|
||||
sorting: Array<SearchSortingDefinition> = [];
|
||||
|
||||
protected userFacetBuckets: { [key: string]: Array<FacetFieldBucket> } = {};
|
||||
|
||||
get userQuery(): string {
|
||||
return this._userQuery;
|
||||
}
|
||||
|
||||
set userQuery(value: string) {
|
||||
value = (value || '').trim();
|
||||
this._userQuery = value ? `(${value})` : '';
|
||||
}
|
||||
|
||||
config: SearchConfiguration = {
|
||||
categories: []
|
||||
};
|
||||
|
||||
// TODO: to be supported in future iterations
|
||||
ranges: { [id: string]: SearchRange } = {};
|
||||
|
||||
constructor(protected appConfig: AppConfigService, protected alfrescoApiService: AlfrescoApiService) {
|
||||
this.setUpConfiguration();
|
||||
}
|
||||
|
||||
public abstract loadConfiguration(): SearchConfiguration;
|
||||
|
||||
public abstract isFilterServiceActive(): boolean;
|
||||
|
||||
public setUpConfiguration() {
|
||||
const currentConfig = this.loadConfiguration();
|
||||
this.setUpSearchConfiguration(currentConfig);
|
||||
}
|
||||
|
||||
private setUpSearchConfiguration(currentConfiguration: SearchConfiguration) {
|
||||
if (currentConfiguration) {
|
||||
this.config = JSON.parse(JSON.stringify(currentConfiguration));
|
||||
this.categories = (this.config.categories || []).filter(
|
||||
category => category.enabled
|
||||
);
|
||||
this.filterQueries = this.config.filterQueries || [];
|
||||
this.userFacetBuckets = {};
|
||||
if (this.config.sorting) {
|
||||
this.sorting = this.config.sorting.defaults || [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a facet bucket to a field.
|
||||
* @param field The target field
|
||||
* @param bucket Bucket to add
|
||||
*/
|
||||
addUserFacetBucket(field: FacetField, bucket: FacetFieldBucket) {
|
||||
if (field && field.field && bucket) {
|
||||
const buckets = this.userFacetBuckets[field.field] || [];
|
||||
const existing = buckets.find((facetBucket) => facetBucket.label === bucket.label);
|
||||
if (!existing) {
|
||||
buckets.push(bucket);
|
||||
}
|
||||
this.userFacetBuckets[field.field] = buckets;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the buckets currently added to a field
|
||||
* @param field The target fields
|
||||
* @returns Bucket array
|
||||
*/
|
||||
getUserFacetBuckets(field: string) {
|
||||
return this.userFacetBuckets[field] || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes an existing bucket from a field.
|
||||
* @param field The target field
|
||||
* @param bucket Bucket to remove
|
||||
*/
|
||||
removeUserFacetBucket(field: FacetField, bucket: FacetFieldBucket) {
|
||||
if (field && field.field && bucket) {
|
||||
const buckets = this.userFacetBuckets[field.field] || [];
|
||||
this.userFacetBuckets[field.field] = buckets
|
||||
.filter((facetBucket) => facetBucket.label !== bucket.label);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a filter query to the current query.
|
||||
* @param query Query string to add
|
||||
*/
|
||||
addFilterQuery(query: string): void {
|
||||
if (query) {
|
||||
const existing = this.filterQueries.find((filterQuery) => filterQuery.query === query);
|
||||
if (!existing) {
|
||||
this.filterQueries.push({ query: query });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes an existing filter query.
|
||||
* @param query The query to remove
|
||||
*/
|
||||
removeFilterQuery(query: string): void {
|
||||
if (query) {
|
||||
this.filterQueries = this.filterQueries
|
||||
.filter((filterQuery) => filterQuery.query !== query);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a facet query by label.
|
||||
* @param label Label of the query
|
||||
* @returns Facet query data
|
||||
*/
|
||||
getFacetQuery(label: string): FacetQuery {
|
||||
if (label && this.hasFacetQueries) {
|
||||
const result = this.config.facetQueries.queries.find((query) => query.label === label);
|
||||
if (result) {
|
||||
return { ...result };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a facet field by label.
|
||||
* @param label Label of the facet field
|
||||
* @returns Facet field data
|
||||
*/
|
||||
getFacetField(label: string): FacetField {
|
||||
if (label) {
|
||||
const fields = this.config.facetFields.fields || [];
|
||||
const result = fields.find((field) => field.label === label);
|
||||
if (result) {
|
||||
result.label = this.getSupportedLabel(result.label);
|
||||
return { ...result };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the current query and triggers the `updated` event.
|
||||
*/
|
||||
update(): void {
|
||||
const query = this.buildQuery();
|
||||
this.updated.next(query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds and executes the current query.
|
||||
* @returns Nothing
|
||||
*/
|
||||
async execute() {
|
||||
try {
|
||||
const query = this.buildQuery();
|
||||
if (query) {
|
||||
const resultSetPaging: ResultSetPaging = await this.alfrescoApiService.searchApi.search(query);
|
||||
this.executed.next(resultSetPaging);
|
||||
}
|
||||
} catch (error) {
|
||||
this.error.next(error);
|
||||
|
||||
this.executed.next({
|
||||
list: {
|
||||
pagination: {
|
||||
totalItems: 0
|
||||
},
|
||||
entries: []
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the current query.
|
||||
* @returns The finished query
|
||||
*/
|
||||
buildQuery(): QueryBody {
|
||||
const query = this.getFinalQuery();
|
||||
|
||||
const include = this.config.include || [];
|
||||
if (include.length === 0) {
|
||||
include.push('path', 'allowableOperations');
|
||||
}
|
||||
|
||||
if (query) {
|
||||
const result: QueryBody = <QueryBody> {
|
||||
query: {
|
||||
query: query,
|
||||
language: 'afts'
|
||||
},
|
||||
include: include,
|
||||
paging: this.paging,
|
||||
fields: this.config.fields,
|
||||
filterQueries: this.filterQueries,
|
||||
facetQueries: this.facetQueries,
|
||||
facetIntervals: this.facetIntervals,
|
||||
facetFields: this.facetFields,
|
||||
sort: this.sort,
|
||||
highlight: this.highlight
|
||||
};
|
||||
|
||||
result['facetFormat'] = 'V2';
|
||||
return result;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the primary sorting definition.
|
||||
* @returns The primary sorting definition
|
||||
*/
|
||||
getPrimarySorting(): SearchSortingDefinition {
|
||||
if (this.sorting && this.sorting.length > 0) {
|
||||
return this.sorting[0];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all pre-configured sorting options that users can choose from.
|
||||
* @returns Pre-configured sorting options
|
||||
*/
|
||||
getSortingOptions(): SearchSortingDefinition[] {
|
||||
if (this.config && this.config.sorting) {
|
||||
return this.config.sorting.options || [];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the query group.
|
||||
* @param query Target query
|
||||
* @returns Query group
|
||||
*/
|
||||
getQueryGroup(query) {
|
||||
return query.group || this.config.facetQueries.label || 'Facet Queries';
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if FacetQueries has been defined
|
||||
* @returns True if defined, false otherwise
|
||||
*/
|
||||
get hasFacetQueries(): boolean {
|
||||
if (this.config
|
||||
&& this.config.facetQueries
|
||||
&& this.config.facetQueries.queries
|
||||
&& this.config.facetQueries.queries.length > 0) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if FacetIntervals has been defined
|
||||
* @returns True if defined, false otherwise
|
||||
*/
|
||||
get hasFacetIntervals(): boolean {
|
||||
if (this.config
|
||||
&& this.config.facetIntervals
|
||||
&& this.config.facetIntervals.intervals
|
||||
&& this.config.facetIntervals.intervals.length > 0) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
get hasFacetHighlight(): boolean {
|
||||
return this.config && this.config.highlight ? true : false;
|
||||
}
|
||||
|
||||
protected get sort(): RequestSortDefinitionInner[] {
|
||||
return this.sorting.map((def) => {
|
||||
return new RequestSortDefinitionInner({
|
||||
type: def.type,
|
||||
field: def.field,
|
||||
ascending: def.ascending
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
protected get facetQueries(): FacetQuery[] {
|
||||
if (this.hasFacetQueries) {
|
||||
return this.config.facetQueries.queries.map((query) => {
|
||||
query.group = this.getQueryGroup(query);
|
||||
return <FacetQuery> { ...query };
|
||||
});
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected get facetIntervals(): any {
|
||||
if (this.hasFacetIntervals) {
|
||||
const configIntervals = this.config.facetIntervals;
|
||||
|
||||
return {
|
||||
intervals: configIntervals.intervals.map((interval) => <any> {
|
||||
label: this.getSupportedLabel(interval.label),
|
||||
field: interval.field,
|
||||
sets: interval.sets.map((set) => <any> {
|
||||
label: this.getSupportedLabel(set.label),
|
||||
start: set.start,
|
||||
end: set.end,
|
||||
startInclusive: set.startInclusive,
|
||||
endInclusive: set.endInclusive
|
||||
})
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected get highlight(): RequestHighlight {
|
||||
return this.hasFacetHighlight ? this.config.highlight : null;
|
||||
}
|
||||
|
||||
protected getFinalQuery(): string {
|
||||
let query = '';
|
||||
|
||||
this.categories.forEach((facet) => {
|
||||
const customQuery = this.queryFragments[facet.id];
|
||||
if (customQuery) {
|
||||
if (query.length > 0) {
|
||||
query += ' AND ';
|
||||
}
|
||||
query += `(${customQuery})`;
|
||||
}
|
||||
});
|
||||
|
||||
let result = [this.userQuery, query]
|
||||
.filter((entry) => entry)
|
||||
.join(' AND ');
|
||||
|
||||
if (this.userFacetBuckets) {
|
||||
Object.keys(this.userFacetBuckets).forEach((key) => {
|
||||
const subQuery = (this.userFacetBuckets[key] || [])
|
||||
.filter((bucket) => bucket.filterQuery)
|
||||
.map((bucket) => bucket.filterQuery)
|
||||
.join(' OR ');
|
||||
if (subQuery) {
|
||||
if (result.length > 0) {
|
||||
result += ' AND ';
|
||||
}
|
||||
result += `(${subQuery})`;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
protected get facetFields(): RequestFacetFields {
|
||||
const facetFields = this.config.facetFields && this.config.facetFields.fields;
|
||||
|
||||
if (facetFields && facetFields.length > 0) {
|
||||
return {
|
||||
facets: facetFields.map((facet) => <RequestFacetField> {
|
||||
field: facet.field,
|
||||
mincount: facet.mincount,
|
||||
label: this.getSupportedLabel(facet.label),
|
||||
limit: facet.limit,
|
||||
offset: facet.offset,
|
||||
prefix: facet.prefix
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encloses a label name with double quotes if it contains whitespace characters.
|
||||
* @param configLabel Original label text
|
||||
* @returns Label, possibly with quotes if it contains spaces
|
||||
*/
|
||||
getSupportedLabel(configLabel: string): string {
|
||||
const spaceInsideLabelIndex = configLabel.search(/\s/g);
|
||||
if (spaceInsideLabelIndex > -1) {
|
||||
return `"${configLabel}"`;
|
||||
}
|
||||
return configLabel;
|
||||
}
|
||||
}
|
@@ -125,6 +125,10 @@ export class SearchDateRangeComponent implements SearchWidget, OnInit, OnDestroy
|
||||
}
|
||||
}
|
||||
|
||||
applyCurrentForm() {
|
||||
this.apply(this.form.value, this.form.valid);
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.form.reset({
|
||||
from: '',
|
||||
|
@@ -59,8 +59,8 @@ describe('SearchFilterComponent', () => {
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
queryBuilder = TestBed.get(SearchQueryBuilderService);
|
||||
fixture = TestBed.createComponent(SearchFilterComponent);
|
||||
queryBuilder = fixture.componentInstance.queryBuilder;
|
||||
appConfigService = TestBed.get(AppConfigService);
|
||||
const translationService = fixture.debugElement.injector.get(TranslationService);
|
||||
spyOn(translationService, 'instant').and.callFake((key) => key ? `${key}_translated` : null);
|
||||
@@ -737,7 +737,7 @@ describe('SearchFilterComponent', () => {
|
||||
|
||||
it('should not show the disabled widget', async(() => {
|
||||
appConfigService.config.search = { categories: disabledCategories };
|
||||
queryBuilder.resetToDefaults();
|
||||
queryBuilder.setUpConfiguration();
|
||||
|
||||
fixture.detectChanges();
|
||||
fixture.whenStable().then(() => {
|
||||
@@ -748,7 +748,7 @@ describe('SearchFilterComponent', () => {
|
||||
|
||||
it('should show the widget in expanded mode', async(() => {
|
||||
appConfigService.config.search = { categories: expandedCategories };
|
||||
queryBuilder.resetToDefaults();
|
||||
queryBuilder.setUpConfiguration();
|
||||
|
||||
fixture.detectChanges();
|
||||
fixture.whenStable().then(() => {
|
||||
@@ -769,7 +769,7 @@ describe('SearchFilterComponent', () => {
|
||||
|
||||
it('should show the widgets only if configured', async(() => {
|
||||
appConfigService.config.search = { categories: simpleCategories };
|
||||
queryBuilder.resetToDefaults();
|
||||
queryBuilder.setUpConfiguration();
|
||||
|
||||
fixture.detectChanges();
|
||||
fixture.whenStable().then(() => {
|
||||
@@ -784,7 +784,7 @@ describe('SearchFilterComponent', () => {
|
||||
it('should be update the search query when name changed', async( async () => {
|
||||
spyOn(queryBuilder, 'update').and.stub();
|
||||
appConfigService.config.search = searchFilter;
|
||||
queryBuilder.resetToDefaults();
|
||||
queryBuilder.setUpConfiguration();
|
||||
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
@@ -805,7 +805,7 @@ describe('SearchFilterComponent', () => {
|
||||
it('should show the long facet options list with pagination', () => {
|
||||
const panel = '[data-automation-id="expansion-panel-Size facet queries"]';
|
||||
appConfigService.config.search = searchFilter;
|
||||
queryBuilder.resetToDefaults();
|
||||
queryBuilder.setUpConfiguration();
|
||||
|
||||
fixture.detectChanges();
|
||||
queryBuilder.executed.next(<any> mockSearchResult);
|
||||
@@ -871,7 +871,7 @@ describe('SearchFilterComponent', () => {
|
||||
delete filter.facetQueries;
|
||||
|
||||
appConfigService.config.search = filter;
|
||||
queryBuilder.resetToDefaults();
|
||||
queryBuilder.setUpConfiguration();
|
||||
|
||||
fixture.detectChanges();
|
||||
queryBuilder.executed.next(<any> mockSearchResult);
|
||||
@@ -884,7 +884,7 @@ describe('SearchFilterComponent', () => {
|
||||
it('should search the facets options and select it', () => {
|
||||
const panel = '[data-automation-id="expansion-panel-Size facet queries"]';
|
||||
appConfigService.config.search = searchFilter;
|
||||
queryBuilder.resetToDefaults();
|
||||
queryBuilder.setUpConfiguration();
|
||||
fixture.detectChanges();
|
||||
queryBuilder.executed.next(<any> mockSearchResult);
|
||||
fixture.detectChanges();
|
||||
@@ -930,7 +930,7 @@ describe('SearchFilterComponent', () => {
|
||||
const panel1 = '[data-automation-id="expansion-panel-Size facet queries"]';
|
||||
const panel2 = '[data-automation-id="expansion-panel-Type facet queries"]';
|
||||
appConfigService.config.search = searchFilter;
|
||||
queryBuilder.resetToDefaults();
|
||||
queryBuilder.setUpConfiguration();
|
||||
fixture.detectChanges();
|
||||
queryBuilder.executed.next(<any> mockSearchResult);
|
||||
fixture.detectChanges();
|
||||
|
@@ -15,7 +15,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Component, ViewEncapsulation, OnInit, OnDestroy } from '@angular/core';
|
||||
import { Component, ViewEncapsulation, OnInit, OnDestroy, Inject } from '@angular/core';
|
||||
import { MatCheckboxChange } from '@angular/material';
|
||||
import { SearchService, TranslationService } from '@alfresco/adf-core';
|
||||
import { SearchQueryBuilderService } from '../../search-query-builder.service';
|
||||
@@ -25,6 +25,7 @@ import { SearchFilterList } from './models/search-filter-list.model';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
import { GenericBucket, GenericFacetResponse, ResultSetContext, ResultSetPaging } from '@alfresco/js-api';
|
||||
import { Subject } from 'rxjs';
|
||||
import { SEARCH_QUERY_SERVICE_TOKEN } from '../../search-query-service.token';
|
||||
|
||||
export interface SelectedBucket {
|
||||
field: FacetField;
|
||||
@@ -58,7 +59,7 @@ export class SearchFilterComponent implements OnInit, OnDestroy {
|
||||
|
||||
private onDestroy$ = new Subject<boolean>();
|
||||
|
||||
constructor(public queryBuilder: SearchQueryBuilderService,
|
||||
constructor(@Inject(SEARCH_QUERY_SERVICE_TOKEN) public queryBuilder: SearchQueryBuilderService,
|
||||
private searchService: SearchService,
|
||||
private translationService: TranslationService) {
|
||||
if (queryBuilder.config && queryBuilder.config.facetQueries) {
|
||||
|
@@ -0,0 +1,37 @@
|
||||
<div *ngIf="isFilterServiceActive">
|
||||
<div *ngIf="!!category" class="adf-filter">
|
||||
<button mat-icon-button [matMenuTriggerFor]="filter"
|
||||
id="filter-menu-button"
|
||||
#menuTrigger="matMenuTrigger"
|
||||
(click)="onMenuButtonClick($event)"
|
||||
class="adf-filter-button"
|
||||
matTooltip="{{ 'SEARCH.SEARCH_HEADER.FILTER_BY' | translate: { category: col.title || 'Type' } }}">
|
||||
<adf-icon value="adf:filter"
|
||||
[ngClass]="{ 'adf-icon-active': isActive || menuTrigger.menuOpen }"
|
||||
matBadge="⁠"
|
||||
matBadgeColor="warn"
|
||||
[matBadgeHidden]="!isActive"></adf-icon>
|
||||
</button>
|
||||
|
||||
<mat-menu #filter="matMenu" class="adf-filter-menu">
|
||||
<div (click)="onMenuClick($event)" class="adf-filter-container">
|
||||
<div class="adf-filter-title">{{ 'SEARCH.SEARCH_HEADER.TITLE' | translate }}</div>
|
||||
<adf-search-widget-container
|
||||
[id]="category?.id"
|
||||
[selector]="category?.component?.selector"
|
||||
[settings]="category?.component?.settings">
|
||||
</adf-search-widget-container>
|
||||
</div>
|
||||
<mat-dialog-actions class="adf-filter-actions">
|
||||
<button mat-button id="clear-filter-button"
|
||||
(click)="onClearButtonClick($event)">{{ 'SEARCH.SEARCH_HEADER.CLEAR' | translate | uppercase }}
|
||||
</button>
|
||||
<button mat-button color="primary"
|
||||
id="apply-filter-button"
|
||||
class="adf-filter-apply-button" (click)="onApplyButtonClick()">
|
||||
{{ 'SEARCH.SEARCH_HEADER.APPLY' | translate | uppercase }}
|
||||
</button>
|
||||
</mat-dialog-actions>
|
||||
</mat-menu>
|
||||
</div>
|
||||
</div>
|
@@ -0,0 +1,72 @@
|
||||
@mixin adf-filter-menu-theme($theme) {
|
||||
$primary: map-get($theme, primary);
|
||||
$foreground: map-get($theme, foreground);
|
||||
$background: map-get($theme, background);
|
||||
|
||||
.adf-filter {
|
||||
|
||||
&-button {
|
||||
margin-left: -7px !important;
|
||||
|
||||
.adf-icon {
|
||||
opacity: 1;
|
||||
color: mat-color($foreground, icon);
|
||||
|
||||
&-active {
|
||||
color: mat-color($primary);
|
||||
}
|
||||
}
|
||||
|
||||
.mat-icon {
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
}
|
||||
|
||||
.mat-badge-content {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
top: -3px !important;
|
||||
right: -6px !important;
|
||||
}
|
||||
}
|
||||
|
||||
&-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 15px 15px 10px;
|
||||
|
||||
.adf-facet-buttons {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.adf-search-check-list {
|
||||
padding: 5px 0;
|
||||
}
|
||||
}
|
||||
|
||||
&-title {
|
||||
font-size: 1.1em;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
&-actions {
|
||||
background-color: mat-color($background, background);
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: 15px;
|
||||
|
||||
> button {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
}
|
||||
|
||||
&-apply-button {
|
||||
background-color: mat-color($background, hover);
|
||||
}
|
||||
}
|
||||
|
||||
.mat-menu-panel.adf-filter-menu .mat-menu-content {
|
||||
min-width: 260px;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
@@ -0,0 +1,147 @@
|
||||
/*!
|
||||
* @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';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { SearchService, setupTestBed, AlfrescoApiService } from '@alfresco/adf-core';
|
||||
import { SearchHeaderComponent } from './search-header.component';
|
||||
import { SearchHeaderQueryBuilderService } from '../../search-header-query-builder.service';
|
||||
import { ContentTestingModule } from '../../../testing/content.testing.module';
|
||||
import { fakeNodePaging } from '../../../mock';
|
||||
import { SEARCH_QUERY_SERVICE_TOKEN } from '../../search-query-service.token';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { SimpleChange } from '@angular/core';
|
||||
|
||||
const mockCategory: any = {
|
||||
'id': 'queryName',
|
||||
'name': 'Name',
|
||||
'columnKey': 'name',
|
||||
'enabled': true,
|
||||
'expanded': true,
|
||||
'component': {
|
||||
'selector': 'text',
|
||||
'settings': {
|
||||
'pattern': "cm:name:'(.*?)'",
|
||||
'field': 'cm:name',
|
||||
'placeholder': 'Enter the name'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
describe('SearchHeaderComponent', () => {
|
||||
let fixture: ComponentFixture<SearchHeaderComponent>;
|
||||
let component: SearchHeaderComponent;
|
||||
let queryBuilder: SearchHeaderQueryBuilderService;
|
||||
let alfrescoApiService: AlfrescoApiService;
|
||||
|
||||
const searchMock: any = {
|
||||
dataLoaded: new Subject()
|
||||
};
|
||||
|
||||
setupTestBed({
|
||||
imports: [
|
||||
TranslateModule.forRoot(),
|
||||
ContentTestingModule
|
||||
],
|
||||
providers: [
|
||||
{ provide: SearchService, useValue: searchMock },
|
||||
{ provide: SEARCH_QUERY_SERVICE_TOKEN, useClass: SearchHeaderQueryBuilderService }
|
||||
]
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(SearchHeaderComponent);
|
||||
component = fixture.componentInstance;
|
||||
queryBuilder = fixture.componentInstance['searchHeaderQueryBuilder'];
|
||||
alfrescoApiService = TestBed.get(AlfrescoApiService);
|
||||
component.col = {key: '123', type: 'text'};
|
||||
spyOn(queryBuilder, 'getCategoryForColumn').and.returnValue(mockCategory);
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fixture.destroy();
|
||||
});
|
||||
|
||||
it('should show the filter when a category is found', async () => {
|
||||
expect(queryBuilder.isFilterServiceActive()).toBe(true);
|
||||
const element = fixture.nativeElement.querySelector('.adf-filter');
|
||||
expect(element).not.toBeNull();
|
||||
expect(element).not.toBeUndefined();
|
||||
});
|
||||
|
||||
it('should emit the node paging received from the queryBuilder after the filter gets applied', async (done) => {
|
||||
spyOn(alfrescoApiService.searchApi, 'search').and.returnValue(Promise.resolve(fakeNodePaging));
|
||||
spyOn(queryBuilder, 'buildQuery').and.returnValue({});
|
||||
component.update.subscribe((newNodePaging) => {
|
||||
expect(newNodePaging).toBe(fakeNodePaging);
|
||||
done();
|
||||
});
|
||||
const menuButton: HTMLButtonElement = fixture.nativeElement.querySelector('#filter-menu-button');
|
||||
menuButton.click();
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
const applyButton = fixture.debugElement.query(By.css('#apply-filter-button'));
|
||||
applyButton.triggerEventHandler('click', {});
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
});
|
||||
|
||||
it('should execute a new query when the page size is changed', async (done) => {
|
||||
spyOn(alfrescoApiService.searchApi, 'search').and.returnValue(Promise.resolve(fakeNodePaging));
|
||||
spyOn(queryBuilder, 'buildQuery').and.returnValue({});
|
||||
component.update.subscribe((newNodePaging) => {
|
||||
expect(newNodePaging).toBe(fakeNodePaging);
|
||||
done();
|
||||
});
|
||||
const maxItem = new SimpleChange(10, 20, false);
|
||||
component.ngOnChanges({ 'maxItems': maxItem });
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
});
|
||||
|
||||
it('should execute a new query when a new page is requested', async (done) => {
|
||||
spyOn(alfrescoApiService.searchApi, 'search').and.returnValue(Promise.resolve(fakeNodePaging));
|
||||
spyOn(queryBuilder, 'buildQuery').and.returnValue({});
|
||||
component.update.subscribe((newNodePaging) => {
|
||||
expect(newNodePaging).toBe(fakeNodePaging);
|
||||
done();
|
||||
});
|
||||
const skipCount = new SimpleChange(0, 10, false);
|
||||
component.ngOnChanges({ 'skipCount': skipCount });
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
});
|
||||
|
||||
it('should emit the clear event when no filter has been selected', async (done) => {
|
||||
spyOn(queryBuilder, 'isNoFilterActive').and.returnValue(true);
|
||||
spyOn(alfrescoApiService.searchApi, 'search').and.returnValue(Promise.resolve(fakeNodePaging));
|
||||
spyOn(queryBuilder, 'buildQuery').and.returnValue({});
|
||||
spyOn(component.widgetContainer, 'resetInnerWidget').and.stub();
|
||||
const fakeEvent = jasmine.createSpyObj('event', ['stopPropagation']);
|
||||
component.clear.subscribe(() => {
|
||||
done();
|
||||
});
|
||||
const menuButton: HTMLButtonElement = fixture.nativeElement.querySelector('#filter-menu-button');
|
||||
menuButton.click();
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
const clearButton = fixture.debugElement.query(By.css('#clear-filter-button'));
|
||||
clearButton.triggerEventHandler('click', fakeEvent);
|
||||
fixture.detectChanges();
|
||||
});
|
||||
});
|
@@ -0,0 +1,137 @@
|
||||
/*!
|
||||
* @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, Input, Output, OnInit, OnChanges, EventEmitter, SimpleChanges, ViewEncapsulation, ViewChild, Inject, OnDestroy } from '@angular/core';
|
||||
import { DataColumn } from '@alfresco/adf-core';
|
||||
import { SearchWidgetContainerComponent } from '../search-widget-container/search-widget-container.component';
|
||||
import { SearchHeaderQueryBuilderService } from '../../search-header-query-builder.service';
|
||||
import { NodePaging } from '@alfresco/js-api';
|
||||
import { SearchCategory } from '../../search-category.interface';
|
||||
import { SEARCH_QUERY_SERVICE_TOKEN } from '../../search-query-service.token';
|
||||
import { Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
|
||||
@Component({
|
||||
selector: 'adf-search-header',
|
||||
templateUrl: './search-header.component.html',
|
||||
styleUrls: ['./search-header.component.scss'],
|
||||
encapsulation: ViewEncapsulation.None
|
||||
})
|
||||
export class SearchHeaderComponent implements OnInit, OnChanges, OnDestroy {
|
||||
@Input()
|
||||
col: DataColumn;
|
||||
|
||||
@Input()
|
||||
currentFolderNodeId: string;
|
||||
|
||||
@Input()
|
||||
maxItems: number;
|
||||
|
||||
@Input()
|
||||
skipCount: number;
|
||||
|
||||
@Output()
|
||||
update: EventEmitter<NodePaging> = new EventEmitter();
|
||||
|
||||
@Output()
|
||||
clear: EventEmitter<any> = new EventEmitter();
|
||||
|
||||
@ViewChild(SearchWidgetContainerComponent)
|
||||
widgetContainer: SearchWidgetContainerComponent;
|
||||
|
||||
public isActive: boolean;
|
||||
|
||||
category: SearchCategory;
|
||||
isFilterServiceActive: boolean;
|
||||
|
||||
private onDestroy$ = new Subject<boolean>();
|
||||
|
||||
constructor(@Inject(SEARCH_QUERY_SERVICE_TOKEN) private searchHeaderQueryBuilder: SearchHeaderQueryBuilderService) {
|
||||
this.isFilterServiceActive = this.searchHeaderQueryBuilder.isFilterServiceActive();
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.category = this.searchHeaderQueryBuilder.getCategoryForColumn(
|
||||
this.col.key
|
||||
);
|
||||
|
||||
this.searchHeaderQueryBuilder.executed
|
||||
.pipe(takeUntil(this.onDestroy$))
|
||||
.subscribe((newNodePaging: NodePaging) => {
|
||||
this.update.emit(newNodePaging);
|
||||
});
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges) {
|
||||
if (changes['currentFolderNodeId'] && changes['currentFolderNodeId'].currentValue) {
|
||||
const currentIdValue = changes['currentFolderNodeId'].currentValue;
|
||||
const previousIdValue = changes['currentFolderNodeId'].previousValue;
|
||||
this.searchHeaderQueryBuilder.setCurrentRootFolderId(
|
||||
currentIdValue,
|
||||
previousIdValue
|
||||
);
|
||||
|
||||
this.isActive = false;
|
||||
}
|
||||
|
||||
if (changes['maxItems'] || changes['skipCount']) {
|
||||
let actualMaxItems = this.maxItems;
|
||||
let actualSkipCount = this.skipCount;
|
||||
|
||||
if (changes['maxItems'] && changes['maxItems'].currentValue) {
|
||||
actualMaxItems = changes['maxItems'].currentValue;
|
||||
}
|
||||
if (changes['skipCount'] && changes['skipCount'].currentValue) {
|
||||
actualSkipCount = changes['skipCount'].currentValue;
|
||||
}
|
||||
|
||||
this.searchHeaderQueryBuilder.setupCurrentPagination(actualMaxItems, actualSkipCount);
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.onDestroy$.next(true);
|
||||
this.onDestroy$.complete();
|
||||
}
|
||||
|
||||
onMenuButtonClick(event: Event) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
onMenuClick(event: Event) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
onApplyButtonClick() {
|
||||
this.isActive = true;
|
||||
this.widgetContainer.applyInnerWidget();
|
||||
this.searchHeaderQueryBuilder.setActiveFilter(this.category.columnKey);
|
||||
this.searchHeaderQueryBuilder.execute();
|
||||
}
|
||||
|
||||
onClearButtonClick(event: Event) {
|
||||
event.stopPropagation();
|
||||
this.widgetContainer.resetInnerWidget();
|
||||
this.isActive = false;
|
||||
this.searchHeaderQueryBuilder.removeActiveFilter(this.category.columnKey);
|
||||
if (this.searchHeaderQueryBuilder.isNoFilterActive()) {
|
||||
this.clear.emit();
|
||||
} else {
|
||||
this.searchHeaderQueryBuilder.execute();
|
||||
}
|
||||
}
|
||||
}
|
@@ -89,4 +89,11 @@ export class SearchRadioComponent implements SearchWidget, OnInit {
|
||||
changeHandler(event: MatRadioChange) {
|
||||
this.setValue(event.value);
|
||||
}
|
||||
|
||||
reset() {
|
||||
const initialValue = this.getSelectedValue();
|
||||
if (initialValue !== null) {
|
||||
this.setValue(initialValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -8,7 +8,7 @@
|
||||
data-automation-id="slider-range">
|
||||
</mat-slider>
|
||||
|
||||
<div class="facet-buttons">
|
||||
<div class="adf-facet-buttons">
|
||||
<button mat-button color="primary" (click)="reset()" data-automation-id="slider-btn-clear">
|
||||
{{ 'SEARCH.FILTER.ACTIONS.CLEAR' | translate }}
|
||||
</button>
|
||||
|
@@ -15,9 +15,10 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Component, OnInit, ViewEncapsulation } from '@angular/core';
|
||||
import { Component, OnInit, ViewEncapsulation, Inject } from '@angular/core';
|
||||
import { SearchQueryBuilderService } from '../../search-query-builder.service';
|
||||
import { SearchSortingDefinition } from '../../search-sorting-definition.interface';
|
||||
import { SEARCH_QUERY_SERVICE_TOKEN } from '../../search-query-service.token';
|
||||
|
||||
@Component({
|
||||
selector: 'adf-search-sorting-picker',
|
||||
@@ -32,7 +33,7 @@ export class SearchSortingPickerComponent implements OnInit {
|
||||
value: string;
|
||||
ascending: boolean;
|
||||
|
||||
constructor(private queryBuilder: SearchQueryBuilderService) {}
|
||||
constructor(@Inject(SEARCH_QUERY_SERVICE_TOKEN) private queryBuilder: SearchQueryBuilderService) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.options = this.queryBuilder.getSortingOptions();
|
||||
|
@@ -15,9 +15,10 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Component, Input, ViewChild, ViewContainerRef, OnInit, OnDestroy, ComponentRef, ComponentFactoryResolver } from '@angular/core';
|
||||
import { SearchQueryBuilderService } from '../../search-query-builder.service';
|
||||
import { Component, Input, ViewChild, ViewContainerRef, OnInit, OnDestroy, ComponentRef, ComponentFactoryResolver, Inject } from '@angular/core';
|
||||
import { SearchFilterService } from '../search-filter/search-filter.service';
|
||||
import { BaseQueryBuilderService } from '../../base-query-builder.service';
|
||||
import { SEARCH_QUERY_SERVICE_TOKEN } from '../../search-query-service.token';
|
||||
|
||||
@Component({
|
||||
selector: 'adf-search-widget-container',
|
||||
@@ -44,7 +45,7 @@ export class SearchWidgetContainerComponent implements OnInit, OnDestroy {
|
||||
|
||||
constructor(
|
||||
private searchFilterService: SearchFilterService,
|
||||
private queryBuilder: SearchQueryBuilderService,
|
||||
@Inject(SEARCH_QUERY_SERVICE_TOKEN) private queryBuilder: BaseQueryBuilderService,
|
||||
private componentFactoryResolver: ComponentFactoryResolver) {
|
||||
}
|
||||
|
||||
@@ -75,4 +76,15 @@ export class SearchWidgetContainerComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
applyInnerWidget() {
|
||||
if (this.selector === 'date-range' && this.componentRef && this.componentRef.instance) {
|
||||
this.componentRef.instance.applyCurrentForm();
|
||||
}
|
||||
}
|
||||
|
||||
resetInnerWidget() {
|
||||
if (this.componentRef && this.componentRef.instance) {
|
||||
this.componentRef.instance.reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -25,6 +25,8 @@ export * from './search-widget.interface';
|
||||
export * from './search-configuration.interface';
|
||||
export * from './search-query-builder.service';
|
||||
export * from './search-range.interface';
|
||||
export * from './search-query-service.token';
|
||||
export * from './search-header-query-builder.service';
|
||||
|
||||
export * from './components/search.component';
|
||||
export * from './components/search-control.component';
|
||||
|
@@ -20,6 +20,7 @@ import { SearchWidgetSettings } from './search-widget-settings.interface';
|
||||
export interface SearchCategory {
|
||||
id: string;
|
||||
name: string;
|
||||
columnKey?: string;
|
||||
enabled: boolean;
|
||||
expanded: boolean;
|
||||
component: {
|
||||
|
@@ -0,0 +1,178 @@
|
||||
/*!
|
||||
* @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 { SearchConfiguration } from './search-configuration.interface';
|
||||
import { AppConfigService } from '@alfresco/adf-core';
|
||||
import { SearchHeaderQueryBuilderService } from './search-header-query-builder.service';
|
||||
|
||||
describe('SearchHeaderQueryBuilder', () => {
|
||||
|
||||
const buildConfig = (searchSettings): AppConfigService => {
|
||||
const config = new AppConfigService(null);
|
||||
config.config['search-headers'] = searchSettings;
|
||||
return config;
|
||||
};
|
||||
|
||||
it('should load the configuration from app config', () => {
|
||||
const config: SearchConfiguration = {
|
||||
categories: [
|
||||
<any> { id: 'cat1', enabled: true },
|
||||
<any> { id: 'cat2', enabled: true }
|
||||
],
|
||||
filterQueries: [{ query: 'query1' }, { query: 'query2' }]
|
||||
};
|
||||
|
||||
const builder = new SearchHeaderQueryBuilderService(
|
||||
buildConfig(config),
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
builder.categories = [];
|
||||
builder.filterQueries = [];
|
||||
|
||||
expect(builder.categories.length).toBe(0);
|
||||
expect(builder.filterQueries.length).toBe(0);
|
||||
|
||||
builder.setUpConfiguration();
|
||||
|
||||
expect(builder.categories.length).toBe(2);
|
||||
expect(builder.filterQueries.length).toBe(2);
|
||||
});
|
||||
|
||||
it('should return the category assigned to a column key', () => {
|
||||
const config: SearchConfiguration = {
|
||||
categories: [
|
||||
<any> { id: 'cat1', columnKey: 'fake-key-1', enabled: true },
|
||||
<any> { id: 'cat2', columnKey: 'fake-key-2', enabled: true }
|
||||
],
|
||||
filterQueries: [{ query: 'query1' }, { query: 'query2' }]
|
||||
};
|
||||
|
||||
const service = new SearchHeaderQueryBuilderService(
|
||||
buildConfig(config),
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
const category = service.getCategoryForColumn('fake-key-1');
|
||||
expect(category).not.toBeNull();
|
||||
expect(category).not.toBeUndefined();
|
||||
expect(category.columnKey).toBe('fake-key-1');
|
||||
});
|
||||
|
||||
it('should have empty user query by default', () => {
|
||||
const builder = new SearchHeaderQueryBuilderService(
|
||||
buildConfig({}),
|
||||
null,
|
||||
null
|
||||
);
|
||||
expect(builder.userQuery).toBe('');
|
||||
});
|
||||
|
||||
it('should add the extra filter for the parent node', () => {
|
||||
const config: SearchConfiguration = {
|
||||
categories: [
|
||||
<any> { id: 'cat1', enabled: true },
|
||||
<any> { id: 'cat2', enabled: true }
|
||||
],
|
||||
filterQueries: [{ query: 'query1' }, { query: 'query2' }]
|
||||
};
|
||||
|
||||
const expectedResult = [
|
||||
{ query: 'query1' },
|
||||
{ query: 'query2' },
|
||||
{ query: 'PARENT:"workspace://SpacesStore/fake-node-id"' }
|
||||
];
|
||||
|
||||
const searchHeaderService = new SearchHeaderQueryBuilderService(
|
||||
buildConfig(config),
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
searchHeaderService.setCurrentRootFolderId('fake-node-id', undefined);
|
||||
|
||||
expect(searchHeaderService.filterQueries).toEqual(expectedResult, 'Filters are not as expected');
|
||||
});
|
||||
|
||||
it('should not add again the parent filter if that node is already added', () => {
|
||||
|
||||
const expectedResult = [
|
||||
{ query: 'query1' },
|
||||
{ query: 'query2' },
|
||||
{ query: 'PARENT:"workspace://SpacesStore/fake-node-id' }
|
||||
];
|
||||
|
||||
const config: SearchConfiguration = {
|
||||
categories: [
|
||||
<any> { id: 'cat1', enabled: true },
|
||||
<any> { id: 'cat2', enabled: true }
|
||||
],
|
||||
filterQueries: expectedResult
|
||||
};
|
||||
|
||||
const searchHeaderService = new SearchHeaderQueryBuilderService(
|
||||
buildConfig(config),
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
searchHeaderService.setCurrentRootFolderId('fake-node-id', undefined);
|
||||
|
||||
expect(searchHeaderService.filterQueries).toEqual(
|
||||
expectedResult,
|
||||
'Filters are not as expected'
|
||||
);
|
||||
});
|
||||
|
||||
it('should replace the new query filter for the old parent node with the new one', () => {
|
||||
const expectedResult = [
|
||||
{ query: 'query1' },
|
||||
{ query: 'query2' },
|
||||
{ query: 'PARENT:"workspace://SpacesStore/fake-next-node-id"' }
|
||||
];
|
||||
|
||||
const config: SearchConfiguration = {
|
||||
categories: [
|
||||
<any> { id: 'cat1', enabled: true },
|
||||
<any> { id: 'cat2', enabled: true }
|
||||
],
|
||||
filterQueries: [
|
||||
{ query: 'query1' },
|
||||
{ query: 'query2' },
|
||||
{ query: 'PARENT:"workspace://SpacesStore/fake-node-id' }
|
||||
]
|
||||
};
|
||||
|
||||
const searchHeaderService = new SearchHeaderQueryBuilderService(
|
||||
buildConfig(config),
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
searchHeaderService.setCurrentRootFolderId(
|
||||
'fake-next-node-id',
|
||||
'fake-node-id'
|
||||
);
|
||||
|
||||
expect(searchHeaderService.filterQueries).toEqual(
|
||||
expectedResult,
|
||||
'Filters are not as expected'
|
||||
);
|
||||
});
|
||||
});
|
@@ -0,0 +1,116 @@
|
||||
/*!
|
||||
* @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 { Injectable } from '@angular/core';
|
||||
import { AlfrescoApiService, AppConfigService, NodesApiService } from '@alfresco/adf-core';
|
||||
import { SearchConfiguration } from './search-configuration.interface';
|
||||
import { BaseQueryBuilderService } from './base-query-builder.service';
|
||||
import { SearchCategory } from './search-category.interface';
|
||||
import { MinimalNode } from '@alfresco/js-api';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class SearchHeaderQueryBuilderService extends BaseQueryBuilderService {
|
||||
|
||||
private customSources = ['-trashcan-', '-sharedlinks-', '-sites-', '-mysites-', '-favorites-', '-recent-', '-my-'];
|
||||
|
||||
activeFilters: string[] = [];
|
||||
currentParentFolderID: string;
|
||||
|
||||
constructor(appConfig: AppConfigService, alfrescoApiService: AlfrescoApiService, private nodeApiService: NodesApiService) {
|
||||
super(appConfig, alfrescoApiService);
|
||||
}
|
||||
|
||||
public isFilterServiceActive(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
loadConfiguration(): SearchConfiguration {
|
||||
return this.appConfig.get<SearchConfiguration>('search-headers');
|
||||
}
|
||||
|
||||
setupCurrentPagination(maxItems: number, skipCount: number) {
|
||||
if (!this.paging ||
|
||||
(this.paging &&
|
||||
this.paging.maxItems !== maxItems || this.paging.skipCount !== skipCount)) {
|
||||
this.paging = { maxItems, skipCount };
|
||||
this.execute();
|
||||
}
|
||||
}
|
||||
|
||||
setActiveFilter(columnActivated: string) {
|
||||
this.activeFilters.push(columnActivated);
|
||||
}
|
||||
|
||||
isNoFilterActive(): boolean {
|
||||
return this.activeFilters.length === 0;
|
||||
}
|
||||
|
||||
removeActiveFilter(columnRemoved: string) {
|
||||
const removeIndex = this.activeFilters.findIndex((column) => column === columnRemoved);
|
||||
this.activeFilters.splice(removeIndex, 1);
|
||||
}
|
||||
|
||||
getCategoryForColumn(columnKey: string): SearchCategory {
|
||||
let foundCategory = null;
|
||||
if (this.categories !== null) {
|
||||
foundCategory = this.categories.find(
|
||||
category => category.columnKey === columnKey
|
||||
);
|
||||
}
|
||||
return foundCategory;
|
||||
}
|
||||
|
||||
setCurrentRootFolderId(currentFolderId: string, previousFolderId: string) {
|
||||
if (this.customSources.includes(currentFolderId)) {
|
||||
if (currentFolderId !== this.currentParentFolderID) {
|
||||
this.nodeApiService.getNode(currentFolderId).subscribe((nodeEntity: MinimalNode) => {
|
||||
this.updateCurrentParentFilter(nodeEntity.id, previousFolderId);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
this.updateCurrentParentFilter(currentFolderId, previousFolderId);
|
||||
}
|
||||
}
|
||||
|
||||
private updateCurrentParentFilter(currentFolderId: string, previousFolderId: string) {
|
||||
const alreadyAddedFilter = this.filterQueries.find(filterQueries =>
|
||||
filterQueries.query.includes(currentFolderId)
|
||||
);
|
||||
|
||||
if (!alreadyAddedFilter) {
|
||||
this.removeOldFolderFiltering(previousFolderId, this.currentParentFolderID);
|
||||
this.currentParentFolderID = currentFolderId;
|
||||
this.filterQueries.push({
|
||||
query: `PARENT:"workspace://SpacesStore/${currentFolderId}"`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private removeOldFolderFiltering(previousFolderId: string, actualSetFolderId: string) {
|
||||
if (previousFolderId || actualSetFolderId) {
|
||||
const folderIdToRetrieve = previousFolderId ? previousFolderId : actualSetFolderId;
|
||||
const oldFilterIndex = this.filterQueries.findIndex(filterQueries =>
|
||||
filterQueries.query.includes(folderIdToRetrieve)
|
||||
);
|
||||
if (oldFilterIndex) {
|
||||
this.filterQueries.splice(oldFilterIndex, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -47,7 +47,7 @@ describe('SearchQueryBuilder', () => {
|
||||
expect(builder.categories.length).toBe(0);
|
||||
expect(builder.filterQueries.length).toBe(0);
|
||||
|
||||
builder.resetToDefaults();
|
||||
builder.setUpConfiguration();
|
||||
|
||||
expect(builder.categories.length).toBe(2);
|
||||
expect(builder.filterQueries.length).toBe(2);
|
||||
|
@@ -16,417 +16,24 @@
|
||||
*/
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Subject } from 'rxjs';
|
||||
import { AlfrescoApiService, AppConfigService } from '@alfresco/adf-core';
|
||||
import {
|
||||
QueryBody,
|
||||
RequestFacetFields,
|
||||
RequestFacetField,
|
||||
RequestSortDefinitionInner,
|
||||
ResultSetPaging,
|
||||
RequestHighlight
|
||||
} from '@alfresco/js-api';
|
||||
import { SearchCategory } from './search-category.interface';
|
||||
import { FilterQuery } from './filter-query.interface';
|
||||
import { SearchRange } from './search-range.interface';
|
||||
import { SearchConfiguration } from './search-configuration.interface';
|
||||
import { FacetQuery } from './facet-query.interface';
|
||||
import { SearchSortingDefinition } from './search-sorting-definition.interface';
|
||||
import { FacetField } from './facet-field.interface';
|
||||
import { FacetFieldBucket } from './facet-field-bucket.interface';
|
||||
import { BaseQueryBuilderService } from './base-query-builder.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class SearchQueryBuilderService {
|
||||
export class SearchQueryBuilderService extends BaseQueryBuilderService {
|
||||
|
||||
private _userQuery = '';
|
||||
|
||||
updated = new Subject<QueryBody>();
|
||||
executed = new Subject<ResultSetPaging>();
|
||||
error = new Subject();
|
||||
|
||||
categories: Array<SearchCategory> = [];
|
||||
queryFragments: { [id: string]: string } = {};
|
||||
filterQueries: FilterQuery[] = [];
|
||||
paging: { maxItems?: number; skipCount?: number } = null;
|
||||
sorting: Array<SearchSortingDefinition> = [];
|
||||
|
||||
protected userFacetBuckets: { [key: string]: Array<FacetFieldBucket> } = {};
|
||||
|
||||
get userQuery(): string {
|
||||
return this._userQuery;
|
||||
}
|
||||
|
||||
set userQuery(value: string) {
|
||||
value = (value || '').trim();
|
||||
this._userQuery = value ? `(${value})` : '';
|
||||
}
|
||||
|
||||
config: SearchConfiguration = {
|
||||
categories: []
|
||||
};
|
||||
|
||||
// TODO: to be supported in future iterations
|
||||
ranges: { [id: string]: SearchRange } = {};
|
||||
|
||||
constructor(private appConfig: AppConfigService, private alfrescoApiService: AlfrescoApiService) {
|
||||
this.resetToDefaults();
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the query to the defaults specified in the app config.
|
||||
*/
|
||||
resetToDefaults() {
|
||||
const template = this.appConfig.get<SearchConfiguration>('search');
|
||||
if (template) {
|
||||
this.config = JSON.parse(JSON.stringify(template));
|
||||
this.categories = (this.config.categories || []).filter((category) => category.enabled);
|
||||
this.filterQueries = this.config.filterQueries || [];
|
||||
this.userFacetBuckets = {};
|
||||
if (this.config.sorting) {
|
||||
this.sorting = this.config.sorting.defaults || [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a facet bucket to a field.
|
||||
* @param field The target field
|
||||
* @param bucket Bucket to add
|
||||
*/
|
||||
addUserFacetBucket(field: FacetField, bucket: FacetFieldBucket) {
|
||||
if (field && field.field && bucket) {
|
||||
const buckets = this.userFacetBuckets[field.field] || [];
|
||||
const existing = buckets.find((facetBucket) => facetBucket.label === bucket.label);
|
||||
if (!existing) {
|
||||
buckets.push(bucket);
|
||||
}
|
||||
this.userFacetBuckets[field.field] = buckets;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the buckets currently added to a field
|
||||
* @param field The target fields
|
||||
* @returns Bucket array
|
||||
*/
|
||||
getUserFacetBuckets(field: string) {
|
||||
return this.userFacetBuckets[field] || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes an existing bucket from a field.
|
||||
* @param field The target field
|
||||
* @param bucket Bucket to remove
|
||||
*/
|
||||
removeUserFacetBucket(field: FacetField, bucket: FacetFieldBucket) {
|
||||
if (field && field.field && bucket) {
|
||||
const buckets = this.userFacetBuckets[field.field] || [];
|
||||
this.userFacetBuckets[field.field] = buckets
|
||||
.filter((facetBucket) => facetBucket.label !== bucket.label);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a filter query to the current query.
|
||||
* @param query Query string to add
|
||||
*/
|
||||
addFilterQuery(query: string): void {
|
||||
if (query) {
|
||||
const existing = this.filterQueries.find((filterQuery) => filterQuery.query === query);
|
||||
if (!existing) {
|
||||
this.filterQueries.push({ query: query });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes an existing filter query.
|
||||
* @param query The query to remove
|
||||
*/
|
||||
removeFilterQuery(query: string): void {
|
||||
if (query) {
|
||||
this.filterQueries = this.filterQueries
|
||||
.filter((filterQuery) => filterQuery.query !== query);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a facet query by label.
|
||||
* @param label Label of the query
|
||||
* @returns Facet query data
|
||||
*/
|
||||
getFacetQuery(label: string): FacetQuery {
|
||||
if (label && this.hasFacetQueries) {
|
||||
const result = this.config.facetQueries.queries.find((query) => query.label === label);
|
||||
if (result) {
|
||||
return { ...result };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a facet field by label.
|
||||
* @param label Label of the facet field
|
||||
* @returns Facet field data
|
||||
*/
|
||||
getFacetField(label: string): FacetField {
|
||||
if (label) {
|
||||
const fields = this.config.facetFields.fields || [];
|
||||
const result = fields.find((field) => field.label === label);
|
||||
if (result) {
|
||||
result.label = this.getSupportedLabel(result.label);
|
||||
return { ...result };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the current query and triggers the `updated` event.
|
||||
*/
|
||||
update(): void {
|
||||
const query = this.buildQuery();
|
||||
this.updated.next(query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds and executes the current query.
|
||||
* @returns Nothing
|
||||
*/
|
||||
async execute() {
|
||||
try {
|
||||
const query = this.buildQuery();
|
||||
if (query) {
|
||||
const resultSetPaging: ResultSetPaging = await this.alfrescoApiService.searchApi.search(query);
|
||||
this.executed.next(resultSetPaging);
|
||||
}
|
||||
} catch (error) {
|
||||
this.error.next(error);
|
||||
|
||||
this.executed.next({
|
||||
list: {
|
||||
pagination: {
|
||||
totalItems: 0
|
||||
},
|
||||
entries: []
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the current query.
|
||||
* @returns The finished query
|
||||
*/
|
||||
buildQuery(): QueryBody {
|
||||
const query = this.getFinalQuery();
|
||||
|
||||
const include = this.config.include || [];
|
||||
if (include.length === 0) {
|
||||
include.push('path', 'allowableOperations');
|
||||
}
|
||||
|
||||
if (query) {
|
||||
const result: QueryBody = <QueryBody> {
|
||||
query: {
|
||||
query: query,
|
||||
language: 'afts'
|
||||
},
|
||||
include: include,
|
||||
paging: this.paging,
|
||||
fields: this.config.fields,
|
||||
filterQueries: this.filterQueries,
|
||||
facetQueries: this.facetQueries,
|
||||
facetIntervals: this.facetIntervals,
|
||||
facetFields: this.facetFields,
|
||||
sort: this.sort,
|
||||
highlight: this.highlight
|
||||
};
|
||||
|
||||
result['facetFormat'] = 'V2';
|
||||
return result;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the primary sorting definition.
|
||||
* @returns The primary sorting definition
|
||||
*/
|
||||
getPrimarySorting(): SearchSortingDefinition {
|
||||
if (this.sorting && this.sorting.length > 0) {
|
||||
return this.sorting[0];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all pre-configured sorting options that users can choose from.
|
||||
* @returns Pre-configured sorting options
|
||||
*/
|
||||
getSortingOptions(): SearchSortingDefinition[] {
|
||||
if (this.config && this.config.sorting) {
|
||||
return this.config.sorting.options || [];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the query group.
|
||||
* @param query Target query
|
||||
* @returns Query group
|
||||
*/
|
||||
getQueryGroup(query) {
|
||||
return query.group || this.config.facetQueries.label || 'Facet Queries';
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if FacetQueries has been defined
|
||||
* @returns True if defined, false otherwise
|
||||
*/
|
||||
get hasFacetQueries(): boolean {
|
||||
if (this.config
|
||||
&& this.config.facetQueries
|
||||
&& this.config.facetQueries.queries
|
||||
&& this.config.facetQueries.queries.length > 0) {
|
||||
return true;
|
||||
}
|
||||
public isFilterServiceActive(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if FacetIntervals has been defined
|
||||
* @returns True if defined, false otherwise
|
||||
*/
|
||||
get hasFacetIntervals(): boolean {
|
||||
if (this.config
|
||||
&& this.config.facetIntervals
|
||||
&& this.config.facetIntervals.intervals
|
||||
&& this.config.facetIntervals.intervals.length > 0) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
constructor(appConfig: AppConfigService, alfrescoApiService: AlfrescoApiService) {
|
||||
super(appConfig, alfrescoApiService);
|
||||
}
|
||||
|
||||
get hasFacetHighlight(): boolean {
|
||||
return this.config && this.config.highlight ? true : false;
|
||||
}
|
||||
|
||||
protected get sort(): RequestSortDefinitionInner[] {
|
||||
return this.sorting.map((def) => {
|
||||
return new RequestSortDefinitionInner({
|
||||
type: def.type,
|
||||
field: def.field,
|
||||
ascending: def.ascending
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
protected get facetQueries(): FacetQuery[] {
|
||||
if (this.hasFacetQueries) {
|
||||
return this.config.facetQueries.queries.map((query) => {
|
||||
query.group = this.getQueryGroup(query);
|
||||
return <FacetQuery> { ...query };
|
||||
});
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected get facetIntervals(): any {
|
||||
if (this.hasFacetIntervals) {
|
||||
const configIntervals = this.config.facetIntervals;
|
||||
|
||||
return {
|
||||
intervals: configIntervals.intervals.map((interval) => <any> {
|
||||
label: this.getSupportedLabel(interval.label),
|
||||
field: interval.field,
|
||||
sets: interval.sets.map((set) => <any> {
|
||||
label: this.getSupportedLabel(set.label),
|
||||
start: set.start,
|
||||
end: set.end,
|
||||
startInclusive: set.startInclusive,
|
||||
endInclusive: set.endInclusive
|
||||
})
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected get highlight(): RequestHighlight {
|
||||
return this.hasFacetHighlight ? this.config.highlight : null;
|
||||
}
|
||||
|
||||
protected getFinalQuery(): string {
|
||||
let query = '';
|
||||
|
||||
this.categories.forEach((facet) => {
|
||||
const customQuery = this.queryFragments[facet.id];
|
||||
if (customQuery) {
|
||||
if (query.length > 0) {
|
||||
query += ' AND ';
|
||||
}
|
||||
query += `(${customQuery})`;
|
||||
}
|
||||
});
|
||||
|
||||
let result = [this.userQuery, query]
|
||||
.filter((entry) => entry)
|
||||
.join(' AND ');
|
||||
|
||||
if (this.userFacetBuckets) {
|
||||
Object.keys(this.userFacetBuckets).forEach((key) => {
|
||||
const subQuery = (this.userFacetBuckets[key] || [])
|
||||
.filter((bucket) => bucket.filterQuery)
|
||||
.map((bucket) => bucket.filterQuery)
|
||||
.join(' OR ');
|
||||
if (subQuery) {
|
||||
if (result.length > 0) {
|
||||
result += ' AND ';
|
||||
}
|
||||
result += `(${subQuery})`;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
protected get facetFields(): RequestFacetFields {
|
||||
const facetFields = this.config.facetFields && this.config.facetFields.fields;
|
||||
|
||||
if (facetFields && facetFields.length > 0) {
|
||||
return {
|
||||
facets: facetFields.map((facet) => <RequestFacetField> {
|
||||
field: facet.field,
|
||||
mincount: facet.mincount,
|
||||
label: this.getSupportedLabel(facet.label),
|
||||
limit: facet.limit,
|
||||
offset: facet.offset,
|
||||
prefix: facet.prefix
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encloses a label name with double quotes if it contains whitespace characters.
|
||||
* @param configLabel Original label text
|
||||
* @returns Label, possibly with quotes if it contains spaces
|
||||
*/
|
||||
getSupportedLabel(configLabel: string): string {
|
||||
const spaceInsideLabelIndex = configLabel.search(/\s/g);
|
||||
if (spaceInsideLabelIndex > -1) {
|
||||
return `"${configLabel}"`;
|
||||
}
|
||||
return configLabel;
|
||||
public loadConfiguration(): SearchConfiguration {
|
||||
return this.appConfig.get<SearchConfiguration>('search');
|
||||
}
|
||||
}
|
||||
|
@@ -15,19 +15,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { DataColumn } from '@alfresco/adf-core';
|
||||
import { InjectionToken } from '@angular/core';
|
||||
import { BaseQueryBuilderService } from './base-query-builder.service';
|
||||
|
||||
@Component({
|
||||
selector: 'adf-filter-menu',
|
||||
templateUrl: './filter-menu.component.html'
|
||||
})
|
||||
export class FilterMenuComponent {
|
||||
|
||||
@Input()
|
||||
col: DataColumn;
|
||||
|
||||
onMenuButtonClick(event: Event) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
}
|
||||
export const SEARCH_QUERY_SERVICE_TOKEN = new InjectionToken<BaseQueryBuilderService>('QueryService');
|
@@ -22,4 +22,5 @@ export interface SearchWidget {
|
||||
id: string;
|
||||
settings?: SearchWidgetSettings;
|
||||
context?: SearchQueryBuilderService;
|
||||
reset();
|
||||
}
|
||||
|
@@ -35,6 +35,9 @@ import { SearchNumberRangeComponent } from './components/search-number-range/sea
|
||||
import { SearchCheckListComponent } from './components/search-check-list/search-check-list.component';
|
||||
import { SearchDateRangeComponent } from './components/search-date-range/search-date-range.component';
|
||||
import { SearchSortingPickerComponent } from './components/search-sorting-picker/search-sorting-picker.component';
|
||||
import { SearchHeaderComponent } from './components/search-header/search-header.component';
|
||||
import { SEARCH_QUERY_SERVICE_TOKEN } from './search-query-service.token';
|
||||
import { SearchQueryBuilderService } from './search-query-builder.service';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@@ -57,7 +60,8 @@ import { SearchSortingPickerComponent } from './components/search-sorting-picker
|
||||
SearchNumberRangeComponent,
|
||||
SearchCheckListComponent,
|
||||
SearchDateRangeComponent,
|
||||
SearchSortingPickerComponent
|
||||
SearchSortingPickerComponent,
|
||||
SearchHeaderComponent
|
||||
],
|
||||
exports: [
|
||||
SearchComponent,
|
||||
@@ -72,7 +76,8 @@ import { SearchSortingPickerComponent } from './components/search-sorting-picker
|
||||
SearchNumberRangeComponent,
|
||||
SearchCheckListComponent,
|
||||
SearchDateRangeComponent,
|
||||
SearchSortingPickerComponent
|
||||
SearchSortingPickerComponent,
|
||||
SearchHeaderComponent
|
||||
],
|
||||
entryComponents: [
|
||||
SearchWidgetContainerComponent,
|
||||
@@ -82,6 +87,9 @@ import { SearchSortingPickerComponent } from './components/search-sorting-picker
|
||||
SearchNumberRangeComponent,
|
||||
SearchCheckListComponent,
|
||||
SearchDateRangeComponent
|
||||
],
|
||||
providers: [
|
||||
{ provide: SEARCH_QUERY_SERVICE_TOKEN, useClass: SearchQueryBuilderService }
|
||||
]
|
||||
})
|
||||
export class SearchModule {}
|
||||
|
@@ -13,6 +13,7 @@
|
||||
@import '../search/components/search-sorting-picker/search-sorting-picker.component';
|
||||
@import '../search/components/search-filter/search-filter.component';
|
||||
@import '../search/components/search-chip-list/search-chip-list.component';
|
||||
@import '../search/components/search-header/search-header.component';
|
||||
|
||||
@import '../dialogs/folder.dialog';
|
||||
|
||||
@@ -39,6 +40,7 @@
|
||||
@include adf-search-control-theme($theme);
|
||||
@include adf-search-autocomplete-theme($theme);
|
||||
@include adf-search-sorting-picker-theme($theme);
|
||||
@include adf-filter-menu-theme($theme);
|
||||
@include adf-dialog-theme($theme);
|
||||
@include adf-content-node-selector-dialog-theme($theme);
|
||||
@include adf-content-metadata-module-theme($theme);
|
||||
|
Reference in New Issue
Block a user