mirror of
https://github.com/Alfresco/alfresco-ng2-components.git
synced 2025-06-30 18:15:11 +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:
parent
5a0ba6666d
commit
29d953e2d1
@ -56,6 +56,7 @@
|
||||
"APP_LAYOUT": {
|
||||
"APP": "App",
|
||||
"APP_NAME": "ADF Demo Application",
|
||||
"FILTERED_SEARCH": "FILTER HEADER",
|
||||
"HOME": "Home",
|
||||
"NODE-SELECTOR": "Node Selector",
|
||||
"SITES": "Sites",
|
||||
|
@ -434,6 +434,176 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"search-headers": {
|
||||
"app:fields": [
|
||||
"cm:name",
|
||||
"cm:title",
|
||||
"cm:description",
|
||||
"ia:whatEvent",
|
||||
"ia:descriptionEvent",
|
||||
"lnk:title",
|
||||
"lnk:description",
|
||||
"TEXT",
|
||||
"TAG"
|
||||
],
|
||||
"categories": [
|
||||
{
|
||||
"id": "queryName",
|
||||
"name": "Name",
|
||||
"columnKey": "name",
|
||||
"enabled": true,
|
||||
"expanded": true,
|
||||
"component": {
|
||||
"selector": "text",
|
||||
"settings": {
|
||||
"pattern": "cm:name:'(.*?)'",
|
||||
"field": "cm:name",
|
||||
"placeholder": "Enter the name"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "checkList",
|
||||
"name": "Check List",
|
||||
"columnKey":"$thumbnail",
|
||||
"enabled": true,
|
||||
"component": {
|
||||
"selector": "check-list",
|
||||
"settings": {
|
||||
"pageSize": 5,
|
||||
"operator": "OR",
|
||||
"options": [
|
||||
{
|
||||
"name": "Folder",
|
||||
"value": "TYPE:'cm:folder'"
|
||||
},
|
||||
{
|
||||
"name": "Document",
|
||||
"value": "TYPE:'cm:content'"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "contentSizeRange",
|
||||
"name": "Content Size (range)",
|
||||
"enabled": true,
|
||||
"component": {
|
||||
"selector": "number-range",
|
||||
"settings": {
|
||||
"field": "cm:content.size",
|
||||
"format": "[{FROM} TO {TO}]"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "contentSize",
|
||||
"name": "SEARCH.CATEGORIES.SIZE",
|
||||
"columnKey":"content.sizeInBytes",
|
||||
"enabled": true,
|
||||
"component": {
|
||||
"selector": "check-list",
|
||||
"settings": {
|
||||
"options": [
|
||||
{
|
||||
"name": "Small",
|
||||
"value": "content.size:[0 TO 1048576>"
|
||||
},
|
||||
{
|
||||
"name": "Medium",
|
||||
"value": "content.size:[1048576 TO 52428800]"
|
||||
},
|
||||
{
|
||||
"name": "Large",
|
||||
"value": "content.size:<52428800 TO 524288000]"
|
||||
},
|
||||
{
|
||||
"name": "Huge",
|
||||
"value": "content.size:<524288000 TO MAX]"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "createdDateRange",
|
||||
"name": "Created Date (range)",
|
||||
"columnKey": "createdAt",
|
||||
"enabled": true,
|
||||
"component": {
|
||||
"selector": "date-range",
|
||||
"settings": {
|
||||
"field": "cm:created",
|
||||
"dateFormat": "DD-MMM-YY",
|
||||
"maxDate": "today"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"highlight": {
|
||||
"prefix": " ",
|
||||
"postfix": " ",
|
||||
"mergeContiguous": true,
|
||||
"fields": [
|
||||
{
|
||||
"field": "cm:title"
|
||||
},
|
||||
{
|
||||
"field": "description",
|
||||
"prefix": "(",
|
||||
"postfix": ")"
|
||||
}
|
||||
]
|
||||
},
|
||||
"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": "FIELD",
|
||||
"field": "score",
|
||||
"ascending": false
|
||||
}
|
||||
],
|
||||
"defaults": [
|
||||
{
|
||||
"key": "score",
|
||||
"type": "FIELD",
|
||||
"field": "score",
|
||||
"ascending": false
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"pagination": {
|
||||
"size": 20,
|
||||
"supportedPageSizes": [
|
||||
|
@ -102,6 +102,7 @@ import localePl from '@angular/common/locales/pl';
|
||||
import localeFi from '@angular/common/locales/fi';
|
||||
import localeDa from '@angular/common/locales/da';
|
||||
import localeSv from '@angular/common/locales/sv';
|
||||
import { FilteredSearchComponent } from './components/files/filtered-search.component';
|
||||
|
||||
registerLocaleData(localeFr);
|
||||
registerLocaleData(localeDe);
|
||||
@ -158,6 +159,7 @@ registerLocaleData(localeSv);
|
||||
FormNodeViewerComponent,
|
||||
AppsViewComponent,
|
||||
FilesComponent,
|
||||
FilteredSearchComponent,
|
||||
FormComponent,
|
||||
FormListComponent,
|
||||
VersionManagerDialogAdapterComponent,
|
||||
|
@ -59,6 +59,7 @@ import { FormCloudDemoComponent } from './components/app-layout/cloud/form-demo/
|
||||
import { ConfirmDialogExampleComponent } from './components/confirm-dialog/confirm-dialog-example.component';
|
||||
import { DemoErrorComponent } from './components/error/demo-error.component';
|
||||
import { TaskHeaderCloudDemoComponent } from './components/cloud/task-header-cloud-demo.component';
|
||||
import { FilteredSearchComponent } from './components/files/filtered-search.component';
|
||||
export const appRoutes: Routes = [
|
||||
{ path: 'login', loadChildren: 'app/components/login/login.module#AppLoginModule' },
|
||||
{ path: 'logout', component: LogoutComponent },
|
||||
@ -251,6 +252,21 @@ export const appRoutes: Routes = [
|
||||
component: FilesComponent,
|
||||
canActivate: [AuthGuardEcm]
|
||||
},
|
||||
{
|
||||
path: 'filtered-search',
|
||||
component: FilteredSearchComponent,
|
||||
canActivate: [AuthGuardEcm]
|
||||
},
|
||||
{
|
||||
path: 'filtered-search/:id',
|
||||
component: FilteredSearchComponent,
|
||||
canActivate: [AuthGuardEcm]
|
||||
},
|
||||
{
|
||||
path: 'filtered-search/:id/display/:mode',
|
||||
component: FilteredSearchComponent,
|
||||
canActivate: [AuthGuardEcm]
|
||||
},
|
||||
{
|
||||
path: 'extensions/document-list/presets',
|
||||
canActivate: [AuthGuardEcm],
|
||||
|
@ -40,6 +40,7 @@ export class AppLayoutComponent implements OnInit, OnDestroy {
|
||||
]
|
||||
},
|
||||
{ href: '/files', icon: 'folder_open', title: 'APP_LAYOUT.CONTENT_SERVICES' },
|
||||
{ href: '/filtered-search', icon: 'rowing', title: 'APP_LAYOUT.FILTERED_SEARCH' },
|
||||
{ href: '/breadcrumb', icon: 'label', title: 'APP_LAYOUT.BREADCRUMB' },
|
||||
{ href: '/notifications', icon: 'alarm', title: 'APP_LAYOUT.NOTIFICATIONS' },
|
||||
{ href: '/card-view', icon: 'view_headline', title: 'APP_LAYOUT.CARD_VIEW' },
|
||||
|
@ -244,6 +244,17 @@
|
||||
(folderChange)="onFolderChange($event)"
|
||||
(permissionError)="handlePermissionError($event)"
|
||||
(name-click)="documentList.onNodeDblClick($event.detail?.node)">
|
||||
<adf-custom-header-filter-template *ngIf="enableCustomHeaderFilter">
|
||||
<ng-template let-col>
|
||||
<adf-search-header [col]="col"
|
||||
[currentFolderNodeId]="currentFolderId"
|
||||
[maxItems]="pagination?.maxItems"
|
||||
[skipCount]="pagination?.skipCount"
|
||||
(update)="onFilterUpdate($event)"
|
||||
(clear)="onAllFilterCleared()">
|
||||
</adf-search-header>
|
||||
</ng-template>
|
||||
</adf-custom-header-filter-template>
|
||||
<adf-custom-no-permission-template *ngIf="enableCustomPermissionMessage">
|
||||
<h1>You don't have permissions</h1>
|
||||
</adf-custom-no-permission-template>
|
||||
|
@ -28,7 +28,7 @@ import {
|
||||
AlfrescoApiService, AuthenticationService, AppConfigService, AppConfigValues, ContentService, TranslationService,
|
||||
FileUploadEvent, FolderCreatedEvent, LogService, NotificationService,
|
||||
UploadService, DataRow, UserPreferencesService,
|
||||
PaginationComponent, FormValues, DisplayMode, InfinitePaginationComponent, HighlightDirective,
|
||||
PaginationComponent, FormValues, DisplayMode, ShowHeaderMode, InfinitePaginationComponent, HighlightDirective,
|
||||
SharedLinksApiService
|
||||
} from '@alfresco/adf-core';
|
||||
|
||||
@ -104,7 +104,7 @@ export class FilesComponent implements OnInit, OnChanges, OnDestroy {
|
||||
showSettingsPanel = true;
|
||||
|
||||
@Input()
|
||||
showHeader = true;
|
||||
showHeader: string = ShowHeaderMode.Always;
|
||||
|
||||
@Input()
|
||||
selectionMode = 'multiple';
|
||||
@ -157,6 +157,12 @@ export class FilesComponent implements OnInit, OnChanges, OnDestroy {
|
||||
@Input()
|
||||
searchTerm = '';
|
||||
|
||||
@Input()
|
||||
navigationRoute = '/files';
|
||||
|
||||
@Input()
|
||||
enableCustomHeaderFilter = false;
|
||||
|
||||
@Output()
|
||||
documentListReady: EventEmitter<any> = new EventEmitter();
|
||||
|
||||
@ -195,10 +201,12 @@ export class FilesComponent implements OnInit, OnChanges, OnDestroy {
|
||||
stickyHeader: boolean;
|
||||
warnOnMultipleUploads = false;
|
||||
thumbnails = false;
|
||||
|
||||
enableCustomPermissionMessage = false;
|
||||
enableMediumTimeFormat = false;
|
||||
displayEmptyMetadata = false;
|
||||
hyperlinkNavigation = false;
|
||||
filtersStates: any[] = [];
|
||||
|
||||
constructor(private notificationService: NotificationService,
|
||||
private uploadService: UploadService,
|
||||
@ -361,7 +369,7 @@ export class FilesComponent implements OnInit, OnChanges, OnDestroy {
|
||||
}
|
||||
|
||||
onFolderChange($event) {
|
||||
this.router.navigate(['/files', $event.value.id, 'display', this.displayMode]);
|
||||
this.router.navigate([this.navigationRoute, $event.value.id, 'display', this.displayMode]);
|
||||
}
|
||||
|
||||
handlePermissionError(event: any) {
|
||||
@ -519,22 +527,32 @@ export class FilesComponent implements OnInit, OnChanges, OnDestroy {
|
||||
|
||||
onChangePageSize(event: Pagination): void {
|
||||
this.preference.paginationSize = event.maxItems;
|
||||
this.pagination.maxItems = event.maxItems;
|
||||
this.pagination.skipCount = event.skipCount;
|
||||
this.changedPageSize.emit(event);
|
||||
}
|
||||
|
||||
onChangePageNumber(event: Pagination): void {
|
||||
this.pagination.maxItems = event.maxItems;
|
||||
this.pagination.skipCount = event.skipCount;
|
||||
this.changedPageNumber.emit(event);
|
||||
}
|
||||
|
||||
onNextPage(event: Pagination): void {
|
||||
this.pagination.maxItems = event.maxItems;
|
||||
this.pagination.skipCount = event.skipCount;
|
||||
this.turnedNextPage.emit(event);
|
||||
}
|
||||
|
||||
loadNextBatch(event: Pagination): void {
|
||||
this.pagination.maxItems = event.maxItems;
|
||||
this.pagination.skipCount = event.skipCount;
|
||||
this.loadNext.emit(event);
|
||||
}
|
||||
|
||||
onPrevPage(event: Pagination): void {
|
||||
this.pagination.maxItems = event.maxItems;
|
||||
this.pagination.skipCount = event.skipCount;
|
||||
this.turnedPreviousPage.emit(event);
|
||||
}
|
||||
|
||||
@ -630,4 +648,14 @@ export class FilesComponent implements OnInit, OnChanges, OnDestroy {
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
onFilterUpdate(newNodePaging: NodePaging) {
|
||||
this.nodeResult = newNodePaging;
|
||||
}
|
||||
|
||||
onAllFilterCleared() {
|
||||
this.documentList.node = null;
|
||||
this.documentList.reload();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,8 @@
|
||||
<app-files-component [sortingMode]="'server'"
|
||||
[showRecentFiles]="false"
|
||||
[showSitePicker]="true"
|
||||
[showSettingsPanel]="false"
|
||||
[navigationRoute]="navigationRoute"
|
||||
[currentFolderId]="currentFolderId"
|
||||
[enableCustomHeaderFilter]="true">
|
||||
</app-files-component>
|
@ -0,0 +1,42 @@
|
||||
/*!
|
||||
* @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, Optional } from '@angular/core';
|
||||
import { SEARCH_QUERY_SERVICE_TOKEN, SearchHeaderQueryBuilderService } from '@alfresco/adf-content-services';
|
||||
import { ActivatedRoute, Params } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'app-filtered-search-component',
|
||||
templateUrl: './filtered-search.component.html',
|
||||
providers: [{ provide: SEARCH_QUERY_SERVICE_TOKEN, useClass: SearchHeaderQueryBuilderService}]
|
||||
})
|
||||
export class FilteredSearchComponent {
|
||||
|
||||
navigationRoute = '/filtered-search';
|
||||
currentFolderId = '-my-';
|
||||
|
||||
constructor(@Optional() private route: ActivatedRoute) {
|
||||
if (this.route) {
|
||||
this.route.params.forEach((params: Params) => {
|
||||
if (params['id'] && this.currentFolderId !== params['id']) {
|
||||
this.currentFolderId = params['id'];
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -15,10 +15,10 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Component, OnInit, Optional, OnDestroy } from '@angular/core';
|
||||
import { Component, OnInit, Optional, OnDestroy, Inject } from '@angular/core';
|
||||
import { Router, ActivatedRoute, Params } from '@angular/router';
|
||||
import { Pagination, ResultSetPaging } from '@alfresco/js-api';
|
||||
import { SearchQueryBuilderService } from '@alfresco/adf-content-services';
|
||||
import { SearchQueryBuilderService, SEARCH_QUERY_SERVICE_TOKEN } from '@alfresco/adf-content-services';
|
||||
import { UserPreferencesService, SearchService, AppConfigService } from '@alfresco/adf-core';
|
||||
import { Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
@ -27,7 +27,7 @@ import { takeUntil } from 'rxjs/operators';
|
||||
selector: 'app-search-result-component',
|
||||
templateUrl: './search-result.component.html',
|
||||
styleUrls: ['./search-result.component.scss'],
|
||||
providers: [SearchService, SearchQueryBuilderService]
|
||||
providers: [SearchService, { provide: SEARCH_QUERY_SERVICE_TOKEN, useClass: SearchQueryBuilderService}]
|
||||
})
|
||||
export class SearchResultComponent implements OnInit, OnDestroy {
|
||||
|
||||
@ -44,7 +44,7 @@ export class SearchResultComponent implements OnInit, OnDestroy {
|
||||
constructor(public router: Router,
|
||||
private config: AppConfigService,
|
||||
private preferences: UserPreferencesService,
|
||||
private queryBuilder: SearchQueryBuilderService,
|
||||
@Inject(SEARCH_QUERY_SERVICE_TOKEN) private queryBuilder: SearchQueryBuilderService,
|
||||
@Optional() private route: ActivatedRoute) {
|
||||
queryBuilder.paging = {
|
||||
maxItems: this.preferences.paginationSize,
|
||||
|
@ -75,7 +75,7 @@ Displays the documents from a repository.
|
||||
| rowStyle | `string` | | The inline style to apply to every row. See the Angular NgStyle docs for more details and usage examples. |
|
||||
| rowStyleClass | `string` | | The CSS class to apply to every row |
|
||||
| selectionMode | `string` | "single" | Row selection mode. Can be null, `single` or `multiple`. For `multiple` mode, you can use Cmd (macOS) or Ctrl (Win) modifier key to toggle selection for multiple rows. |
|
||||
| showHeader | `boolean` | true | Toggles the header |
|
||||
| showHeader | `ShowHeaderMode` | "data" | Indicate which is the desired behaviour for the header. Can have 3 values `always`, `never` and `data`. `data` won't display the header if the datatable is empty and will display it otherwise. |
|
||||
| sorting | `string[]` | ['name', 'asc'] | Defines default sorting. The format is an array of 2 strings `[key, direction]` i.e. `['name', 'desc']` or `['name', 'asc']`. Set this value only if you want to override the default sorting detected by the component based on columns. |
|
||||
| sortingMode | `string` | "client" | Defines sorting mode. Can be either `client` (items in the list are sorted client-side) or `server` (the ordering supplied by the server is used without further client-side sorting). Note that the `server` option _does not_ request the server to sort the data before delivering it. |
|
||||
| stickyHeader | `boolean` | false | Toggles the sticky header mode. |
|
||||
|
81
docs/content-services/components/search-header.component.md
Normal file
81
docs/content-services/components/search-header.component.md
Normal file
@ -0,0 +1,81 @@
|
||||
---
|
||||
Title: SearchHeader component
|
||||
Added: v3.9.0
|
||||
Status: Active
|
||||
Last reviewed: 2020-19-06
|
||||
---
|
||||
# [SearchHeader component](../../../lib/content-services/src/lib/search/components/search-header/search-header.component.ts "Defined in search-header.component.ts")
|
||||
|
||||
Displays a button opening a menu designed to filter a document list.
|
||||
|
||||

|
||||
|
||||
## Contents
|
||||
|
||||
- [Basic usage](#basic-usage)
|
||||
- [Class members](#class-members)
|
||||
- [Properties](#properties)
|
||||
- [Events](#events)
|
||||
- [Details](#details)
|
||||
- [example](#example)
|
||||
- [See also](#see-also)
|
||||
|
||||
## Basic usage
|
||||
|
||||
**app.component.html**
|
||||
|
||||
```html
|
||||
<adf-document-list
|
||||
...
|
||||
...>
|
||||
<adf-custom-header-filter-template>
|
||||
<ng-template let-col>
|
||||
<adf-search-header [col]="col"
|
||||
[currentFolderNodeId]="currentFolderId"
|
||||
[maxItems]="pagination?.maxItems"
|
||||
[skipCount]="pagination?.skipCount"
|
||||
(update)="onFilterUpdate($event)"
|
||||
(clear)="onAllFilterCleared()">
|
||||
</adf-search-header>
|
||||
</ng-template>
|
||||
</adf-custom-header-filter-template>
|
||||
</adf-document-list>
|
||||
```
|
||||
|
||||
**app.config.json**
|
||||
|
||||
```json
|
||||
```
|
||||
|
||||
This component is designed to be used as transcluded inside the document list component. With the good configurations it will allow the user to filter the data displayed by that component.
|
||||
|
||||
## Class members
|
||||
|
||||
### Properties
|
||||
|
||||
| Name | Type | Default value | Description |
|
||||
| ---- | ---- | ------------- | ----------- |
|
||||
| col | `DataColumn` | | The column the filter will be applied on. |
|
||||
| currentFolderNodeId | `string` | | The id of the current folder of the document list. |
|
||||
| maxItems | `number` | | Pagination parameter coming from the document list. |
|
||||
| skipCount | `number` | | An other pagination parameter coming from the document list. |
|
||||
| widgetContainer | `SearchWidgetContainerComponent` | | View of the child facet widget generated inside the menu. Used to control that widget through the apply and clear buttons. |
|
||||
| isActive | `boolean` | | A boolean telling if the current data displayed in the document list is affected by that filter. |
|
||||
| category | `SearchCategory` | | The category of the filter. It contains the information regarding the way the filter is filtering the data. This is get with the column information and the configuration of the search-header inside the config file |
|
||||
| isFilterServiceActive | `boolean` | | Boolean to check if the SearchHeaderQueryBuilderService is active |
|
||||
|
||||
### Events
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| update | `EventEmitter<NodePaging>` | Emitted when the result of the filter is received from the API, should be passed to the `node` member of the document list. |
|
||||
| clear | `EventEmitter<any>` | Emitted when the last of all the filters is cleared. This should be used to trigger a `reload()` of the document list with no `node` member. |
|
||||
|
||||
|
||||
## See also
|
||||
|
||||
- [Document list component](document-list.component.md)
|
||||
- [Search filter component](search-filter.component.md)
|
||||
- [Search component](search.component.md)
|
||||
- [Datatable component](../../core/components/datatable.component.md)
|
||||
- [Search Query Builder service](../services/search-query-builder.service.md)
|
@ -360,7 +360,7 @@ Learm more about styling your datatable: [Customizing the component's styles](#c
|
||||
| rowStyleClass | `string` | "" | The CSS class to apply to every row. |
|
||||
| rows | `any[]` | \[] | The rows that the datatable will show. |
|
||||
| selectionMode | `string` | "single" | Row selection mode. Can be none, `single` or `multiple`. For `multiple` mode, you can use Cmd (macOS) or Ctrl (Win) modifier key to toggle selection for multiple rows. |
|
||||
| showHeader | `boolean` | true | Toggles the header. |
|
||||
| showHeader | `ShowHeaderMode` | "data" | Indicate which is the desired behaviour for the header. Can have 3 values `always`, `never` and `data`. `data` won't display the header if the datatable is empty and will display it otherwise. |
|
||||
| sorting | `any[]` | \[] | Define the sort order of the datatable. Possible values are : [`created`, `desc`], [`created`, `asc`], [`due`, `desc`], [`due`, `asc`] |
|
||||
| stickyHeader | `boolean` | false | Toggles the sticky header mode. |
|
||||
|
||||
|
BIN
docs/docassets/images/search-header-demo.png
Normal file
BIN
docs/docassets/images/search-header-demo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 22 KiB |
@ -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');
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,21 @@
|
||||
/*!
|
||||
* @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 { InjectionToken } from '@angular/core';
|
||||
import { BaseQueryBuilderService } from './base-query-builder.service';
|
||||
|
||||
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);
|
||||
|
@ -4,11 +4,12 @@
|
||||
[class.adf-datatable-card]="display === 'gallery'"
|
||||
[class.adf-datatable-list]="display === 'list'"
|
||||
[class.adf-sticky-header]="isStickyHeaderEnabled()"
|
||||
[class.adf-datatable--empty]="!isHeaderVisible()">
|
||||
<div *ngIf="isHeaderVisible()" class="adf-datatable-header" role="rowgroup" [ngClass]="{ 'adf-sr-only': !showHeader }">
|
||||
[class.adf-datatable--empty]="(isEmpty() && !isHeaderVisible()) || loading"
|
||||
[class.adf-datatable--empty--header-visible]="isEmpty() && isHeaderVisible()">
|
||||
<div *ngIf="isHeaderVisible()" class="adf-datatable-header" role="rowgroup" [ngClass]="{ 'adf-sr-only': !isHeaderVisible() }">
|
||||
<adf-datatable-row
|
||||
data-automation-id="datatable-row-header"
|
||||
[disabled]="!showHeader"
|
||||
[disabled]="!isHeaderVisible()"
|
||||
class="adf-datatable-row"
|
||||
*ngIf="display === 'list'"
|
||||
role="row">
|
||||
@ -29,19 +30,19 @@
|
||||
(click)="onColumnHeaderClick(col)"
|
||||
(keyup.enter)="onColumnHeaderClick(col)"
|
||||
role="columnheader"
|
||||
[attr.tabindex]="showHeader ? 0 : null"
|
||||
[attr.tabindex]="isHeaderVisible() ? 0 : null"
|
||||
[attr.aria-sort]="col.sortable ? (getAriaSort(col) | translate) : null"
|
||||
adf-drop-zone dropTarget="header" [dropColumn]="col">
|
||||
<span *ngIf="col.title" class="adf-datatable-cell-value">{{ col.title | translate}}</span>
|
||||
<span *ngIf="col.title && col.sortable" class="adf-sr-only" aria-live="polite">{{ getSortLiveAnnouncement(col) | translate: { string: col.title | translate } }}</span>
|
||||
<ng-template *ngIf="allowFiltering" [ngTemplateOutlet]="filterTemplateRef" [ngTemplateOutletContext]="{$implicit: col}"></ng-template>
|
||||
<ng-template *ngIf="allowFiltering" [ngTemplateOutlet]="headerFilterTemplate" [ngTemplateOutletContext]="{$implicit: col}"></ng-template>
|
||||
</div>
|
||||
<!-- Actions (right) -->
|
||||
<div *ngIf="actions && actionsPosition === 'right'" class="adf-actions-column adf-datatable-cell-header adf-datatable__actions-cell">
|
||||
<span class="adf-sr-only">{{ 'ADF-DATATABLE.ACCESSIBILITY.ACTIONS' | translate }}</span>
|
||||
</div>
|
||||
</adf-datatable-row>
|
||||
<mat-form-field *ngIf="display === 'gallery' && showHeader">
|
||||
<mat-form-field *ngIf="display === 'gallery' && isHeaderVisible()">
|
||||
<mat-select [value]="getSortingKey()" [attr.data-automation-id]="'grid-view-sorting'">
|
||||
<mat-option *ngFor="let col of getSortableColumns()"
|
||||
[value]="col.key"
|
||||
|
@ -334,6 +334,7 @@
|
||||
color: $data-table-header-color;
|
||||
padding-bottom: 8px;
|
||||
box-sizing: border-box;
|
||||
padding-top: 12px !important;
|
||||
|
||||
&.adf-sortable {
|
||||
@include adf-no-select;
|
||||
@ -653,4 +654,28 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.adf-datatable--empty--header-visible {
|
||||
|
||||
.adf-datatable-header {
|
||||
border: $data-table-dividers-wrapper-border;
|
||||
}
|
||||
|
||||
.adf-datatable-body {
|
||||
@include flex-column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
.adf-datatable-row {
|
||||
height: 100%;
|
||||
background-color: mat-color($background, card);
|
||||
border: none !important;
|
||||
|
||||
&:hover, &:focus {
|
||||
background-color: unset;
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -23,7 +23,7 @@ import { DataRow } from '../../data/data-row.model';
|
||||
import { DataSorting } from '../../data/data-sorting.model';
|
||||
import { ObjectDataColumn } from '../../data/object-datacolumn.model';
|
||||
import { ObjectDataTableAdapter } from '../../data/object-datatable-adapter';
|
||||
import { DataTableComponent } from './datatable.component';
|
||||
import { DataTableComponent, ShowHeaderMode } from './datatable.component';
|
||||
import { setupTestBed } from '../../../testing/setup-test-bed';
|
||||
import { CoreTestingModule } from '../../../testing/core.testing.module';
|
||||
import { DataColumnListComponent } from '../../../data-column/data-column-list.component';
|
||||
@ -160,7 +160,8 @@ describe('DataTable', () => {
|
||||
expect(element.querySelector('.adf-datatable-list')).not.toBeNull();
|
||||
});
|
||||
|
||||
it('should hide the header if showHeader is false', () => {
|
||||
describe('Header modes', () => {
|
||||
|
||||
const newData = new ObjectDataTableAdapter(
|
||||
[
|
||||
{ name: '1' },
|
||||
@ -168,65 +169,113 @@ describe('DataTable', () => {
|
||||
],
|
||||
[new ObjectDataColumn({ key: 'name' })]
|
||||
);
|
||||
const emptyData = new ObjectDataTableAdapter();
|
||||
|
||||
dataTable.showHeader = false;
|
||||
dataTable.loading = false;
|
||||
dataTable.ngOnChanges({
|
||||
data: new SimpleChange(null, newData, false)
|
||||
it('should show the header if showHeader is `Data` and there is data', () => {
|
||||
|
||||
dataTable.showHeader = ShowHeaderMode.Data;
|
||||
dataTable.loading = false;
|
||||
dataTable.ngOnChanges({
|
||||
data: new SimpleChange(null, newData, false)
|
||||
});
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(element.querySelector('.adf-datatable-header')).toBeDefined();
|
||||
});
|
||||
|
||||
fixture.detectChanges();
|
||||
it('should hide the header if showHeader is `Data` and there is no data', () => {
|
||||
|
||||
expect(element.querySelector('.adf-datatable-header')).toBe(null);
|
||||
});
|
||||
dataTable.showHeader = ShowHeaderMode.Data;
|
||||
dataTable.loading = false;
|
||||
dataTable.ngOnChanges({
|
||||
data: new SimpleChange(null, emptyData, false)
|
||||
});
|
||||
|
||||
it('should hide the header if there are no elements inside', () => {
|
||||
const newData = new ObjectDataTableAdapter(
|
||||
);
|
||||
fixture.detectChanges();
|
||||
|
||||
dataTable.ngOnChanges({
|
||||
data: new SimpleChange(null, newData, false)
|
||||
expect(element.querySelector('.adf-datatable-header')).toBeNull();
|
||||
});
|
||||
|
||||
fixture.detectChanges();
|
||||
it('should always show the header if showHeader is `Always`', () => {
|
||||
|
||||
expect(element.querySelector('.adf-datatable-header')).toBe(null);
|
||||
});
|
||||
dataTable.showHeader = ShowHeaderMode.Always;
|
||||
dataTable.loading = false;
|
||||
|
||||
it('should hide the header if noPermission is true', () => {
|
||||
const newData = new ObjectDataTableAdapter(
|
||||
);
|
||||
dataTable.ngOnChanges({
|
||||
data: new SimpleChange(null, newData, false)
|
||||
});
|
||||
fixture.detectChanges();
|
||||
expect(element.querySelector('.adf-datatable-header')).toBeDefined();
|
||||
|
||||
dataTable.noPermission = true;
|
||||
dataTable.loading = false;
|
||||
|
||||
dataTable.ngOnChanges({
|
||||
data: new SimpleChange(null, newData, false)
|
||||
dataTable.ngOnChanges({
|
||||
data: new SimpleChange(null, emptyData, false)
|
||||
});
|
||||
fixture.detectChanges();
|
||||
expect(element.querySelector('.adf-datatable-header')).toBeDefined();
|
||||
});
|
||||
|
||||
fixture.detectChanges();
|
||||
it('should never show the header if showHeader is `Never`', () => {
|
||||
|
||||
expect(element.querySelector('.adf-datatable-header')).toBe(null);
|
||||
});
|
||||
dataTable.showHeader = ShowHeaderMode.Never;
|
||||
dataTable.loading = false;
|
||||
dataTable.ngOnChanges({
|
||||
data: new SimpleChange(null, newData, false)
|
||||
});
|
||||
|
||||
it('should show the header if showHeader is true', () => {
|
||||
const newData = new ObjectDataTableAdapter(
|
||||
[
|
||||
{ name: '1' },
|
||||
{ name: '2' }
|
||||
],
|
||||
[new ObjectDataColumn({ key: 'name' })]
|
||||
);
|
||||
dataTable.showHeader = true;
|
||||
dataTable.loading = false;
|
||||
fixture.detectChanges();
|
||||
|
||||
dataTable.ngOnChanges({
|
||||
data: new SimpleChange(null, newData, false)
|
||||
expect(element.querySelector('.adf-datatable-header')).toBeNull();
|
||||
|
||||
dataTable.ngOnChanges({
|
||||
data: new SimpleChange(null, emptyData, false)
|
||||
});
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(element.querySelector('.adf-datatable-header')).toBeNull();
|
||||
});
|
||||
|
||||
fixture.detectChanges();
|
||||
it('should never show the header if noPermission is true', () => {
|
||||
|
||||
expect(element.querySelector('.adf-datatable-header')).toBeDefined();
|
||||
dataTable.loading = false;
|
||||
dataTable.noPermission = true;
|
||||
dataTable.ngOnChanges({
|
||||
data: new SimpleChange(null, emptyData, false)
|
||||
});
|
||||
|
||||
dataTable.showHeader = ShowHeaderMode.Data;
|
||||
fixture.detectChanges();
|
||||
expect(element.querySelector('.adf-datatable-header')).toBeNull();
|
||||
|
||||
dataTable.showHeader = ShowHeaderMode.Always;
|
||||
fixture.detectChanges();
|
||||
expect(element.querySelector('.adf-datatable-header')).toBeNull();
|
||||
|
||||
dataTable.showHeader = ShowHeaderMode.Never;
|
||||
fixture.detectChanges();
|
||||
expect(element.querySelector('.adf-datatable-header')).toBeNull();
|
||||
});
|
||||
|
||||
it('should never show the header if loading is true', () => {
|
||||
|
||||
dataTable.loading = true;
|
||||
dataTable.ngOnChanges({
|
||||
data: new SimpleChange(null, emptyData, false)
|
||||
});
|
||||
|
||||
dataTable.showHeader = ShowHeaderMode.Data;
|
||||
fixture.detectChanges();
|
||||
expect(element.querySelector('.adf-datatable-header')).toBeNull();
|
||||
|
||||
dataTable.showHeader = ShowHeaderMode.Always;
|
||||
fixture.detectChanges();
|
||||
expect(element.querySelector('.adf-datatable-header')).toBeNull();
|
||||
|
||||
dataTable.showHeader = ShowHeaderMode.Never;
|
||||
fixture.detectChanges();
|
||||
expect(element.querySelector('.adf-datatable-header')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
it('should emit "sorting-changed" DOM event', (done) => {
|
||||
@ -1394,7 +1443,7 @@ describe('Accesibility', () => {
|
||||
expect(document.activeElement.getAttribute('data-automation-id')).toBe('datatable-row-0');
|
||||
});
|
||||
|
||||
it('should select header row when `showHeader` is true', () => {
|
||||
it('should select header row when `showHeader` is `Always`', () => {
|
||||
const event = new KeyboardEvent('keyup', {
|
||||
code: 'ArrowUp',
|
||||
key: 'ArrowUp',
|
||||
@ -1408,7 +1457,7 @@ describe('Accesibility', () => {
|
||||
[new ObjectDataColumn({ key: 'name' })]
|
||||
);
|
||||
|
||||
dataTable.showHeader = true;
|
||||
dataTable.showHeader = ShowHeaderMode.Always;
|
||||
|
||||
dataTable.ngOnChanges({
|
||||
rows: new SimpleChange(null, dataRows, false)
|
||||
@ -1426,7 +1475,7 @@ describe('Accesibility', () => {
|
||||
expect(document.activeElement.getAttribute('data-automation-id')).toBe('datatable-row-header');
|
||||
});
|
||||
|
||||
it('should not select header row when `showHeader` is false', () => {
|
||||
it('should not select header row when `showHeader` is `Never`', () => {
|
||||
const event = new KeyboardEvent('keyup', {
|
||||
code: 'ArrowUp',
|
||||
key: 'ArrowUp',
|
||||
@ -1440,7 +1489,7 @@ describe('Accesibility', () => {
|
||||
[new ObjectDataColumn({ key: 'name' })]
|
||||
);
|
||||
|
||||
dataTable.showHeader = false;
|
||||
dataTable.showHeader = ShowHeaderMode.Never;
|
||||
|
||||
dataTable.ngOnChanges({
|
||||
rows: new SimpleChange(null, dataRows, false)
|
||||
@ -1459,7 +1508,7 @@ describe('Accesibility', () => {
|
||||
});
|
||||
|
||||
it('should remove cell focus when [focus] is set to false', () => {
|
||||
dataTable.showHeader = false;
|
||||
dataTable.showHeader = ShowHeaderMode.Never;
|
||||
const dataRows = [ { name: 'name1' } ];
|
||||
|
||||
dataTable.data = new ObjectDataTableAdapter([],
|
||||
@ -1478,7 +1527,7 @@ describe('Accesibility', () => {
|
||||
});
|
||||
|
||||
it('should allow element focus when [focus] is set to true', () => {
|
||||
dataTable.showHeader = false;
|
||||
dataTable.showHeader = ShowHeaderMode.Never;
|
||||
const dataRows = [ { name: 'name1' } ];
|
||||
|
||||
dataTable.data = new ObjectDataTableAdapter([],
|
||||
|
@ -43,6 +43,12 @@ export enum DisplayMode {
|
||||
Gallery = 'gallery'
|
||||
}
|
||||
|
||||
export enum ShowHeaderMode {
|
||||
Never = 'never',
|
||||
Always = 'always',
|
||||
Data = 'data'
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'adf-datatable',
|
||||
styleUrls: ['./datatable.component.scss'],
|
||||
@ -119,7 +125,7 @@ export class DataTableComponent implements AfterContentInit, OnChanges, DoCheck,
|
||||
|
||||
/** Toggles the header. */
|
||||
@Input()
|
||||
showHeader: boolean = true;
|
||||
showHeader: string = ShowHeaderMode.Data;
|
||||
|
||||
/** Toggles the sticky header mode. */
|
||||
@Input()
|
||||
@ -175,9 +181,7 @@ export class DataTableComponent implements AfterContentInit, OnChanges, DoCheck,
|
||||
@Input()
|
||||
allowFiltering: boolean = false;
|
||||
|
||||
@ContentChild(TemplateRef)
|
||||
filterTemplateRef: TemplateRef<any>;
|
||||
|
||||
headerFilterTemplate: TemplateRef<any>;
|
||||
noContentTemplate: TemplateRef<any>;
|
||||
noPermissionTemplate: TemplateRef<any>;
|
||||
loadingTemplate: TemplateRef<any>;
|
||||
@ -738,7 +742,16 @@ export class DataTableComponent implements AfterContentInit, OnChanges, DoCheck,
|
||||
}
|
||||
|
||||
isHeaderVisible() {
|
||||
return !this.loading && !this.isEmpty() && !this.noPermission;
|
||||
let headerVisibility: boolean;
|
||||
|
||||
if (this.showHeader === ShowHeaderMode.Data) {
|
||||
headerVisibility = !this.loading && !this.noPermission && !this.isEmpty();
|
||||
} else if (this.showHeader === ShowHeaderMode.Always) {
|
||||
headerVisibility = !this.loading && !this.noPermission;
|
||||
} else if (this.showHeader === ShowHeaderMode.Never) {
|
||||
headerVisibility = false;
|
||||
}
|
||||
return headerVisibility;
|
||||
}
|
||||
|
||||
isStickyHeaderEnabled() {
|
||||
|
@ -38,9 +38,11 @@ import { LocationCellComponent } from './components/location-cell/location-cell.
|
||||
import { LoadingContentTemplateDirective } from './directives/loading-template.directive';
|
||||
import { NoContentTemplateDirective } from './directives/no-content-template.directive';
|
||||
import { NoPermissionTemplateDirective } from './directives/no-permission-template.directive';
|
||||
import { HeaderFilterTemplateDirective } from './directives/header-filter-template.directive';
|
||||
import { CustomEmptyContentTemplateDirective } from './directives/custom-empty-content-template.directive';
|
||||
import { CustomLoadingContentTemplateDirective } from './directives/custom-loading-template.directive';
|
||||
import { CustomNoPermissionTemplateDirective } from './directives/custom-no-permission-template.directive';
|
||||
import { CustomHeaderFilterTemplateDirective } from './directives/custom-header-filter-template.directive';
|
||||
import { JsonCellComponent } from './components/json-cell/json-cell.component';
|
||||
import { ClipboardModule } from '../clipboard/clipboard.module';
|
||||
import { DropZoneDirective } from './directives/drop-zone.directive';
|
||||
@ -73,9 +75,11 @@ import { DataColumnModule } from '../data-column/data-column.module';
|
||||
NoContentTemplateDirective,
|
||||
NoPermissionTemplateDirective,
|
||||
LoadingContentTemplateDirective,
|
||||
HeaderFilterTemplateDirective,
|
||||
CustomEmptyContentTemplateDirective,
|
||||
CustomLoadingContentTemplateDirective,
|
||||
CustomNoPermissionTemplateDirective,
|
||||
CustomHeaderFilterTemplateDirective,
|
||||
DropZoneDirective
|
||||
],
|
||||
exports: [
|
||||
@ -93,9 +97,11 @@ import { DataColumnModule } from '../data-column/data-column.module';
|
||||
NoContentTemplateDirective,
|
||||
NoPermissionTemplateDirective,
|
||||
LoadingContentTemplateDirective,
|
||||
HeaderFilterTemplateDirective,
|
||||
CustomEmptyContentTemplateDirective,
|
||||
CustomLoadingContentTemplateDirective,
|
||||
CustomNoPermissionTemplateDirective,
|
||||
CustomHeaderFilterTemplateDirective,
|
||||
DropZoneDirective
|
||||
]
|
||||
|
||||
|
@ -15,19 +15,14 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { DataColumn } from '@alfresco/adf-core';
|
||||
import { Directive, ContentChild, TemplateRef } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'adf-filter-menu',
|
||||
templateUrl: './filter-menu.component.html'
|
||||
@Directive({
|
||||
selector: 'adf-custom-header-filter-template'
|
||||
})
|
||||
export class FilterMenuComponent {
|
||||
export class CustomHeaderFilterTemplateDirective {
|
||||
|
||||
@Input()
|
||||
col: DataColumn;
|
||||
@ContentChild(TemplateRef)
|
||||
template: any;
|
||||
|
||||
onMenuButtonClick(event: Event) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
/*!
|
||||
* @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, ComponentFixture } from '@angular/core/testing';
|
||||
import { DataTableComponent } from '../components/datatable/datatable.component';
|
||||
import { HeaderFilterTemplateDirective } from './header-filter-template.directive';
|
||||
import { setupTestBed } from '../../testing/setup-test-bed';
|
||||
import { CoreTestingModule } from '../../testing/core.testing.module';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
|
||||
describe('HeaderFilterTemplateDirective', () => {
|
||||
|
||||
let fixture: ComponentFixture<DataTableComponent>;
|
||||
let dataTable: DataTableComponent;
|
||||
let directive: HeaderFilterTemplateDirective;
|
||||
|
||||
setupTestBed({
|
||||
imports: [
|
||||
TranslateModule.forRoot(),
|
||||
CoreTestingModule
|
||||
]
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(DataTableComponent);
|
||||
dataTable = fixture.componentInstance;
|
||||
directive = new HeaderFilterTemplateDirective(dataTable);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fixture.destroy();
|
||||
});
|
||||
|
||||
it('applies template to the datatable', () => {
|
||||
const template: any = 'test template';
|
||||
directive.template = template;
|
||||
directive.ngAfterContentInit();
|
||||
expect(dataTable.headerFilterTemplate).toBe(template);
|
||||
});
|
||||
});
|
@ -0,0 +1,37 @@
|
||||
/*!
|
||||
* @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 { AfterContentInit, ContentChild, Directive, TemplateRef } from '@angular/core';
|
||||
import { DataTableComponent } from '../components/datatable/datatable.component';
|
||||
|
||||
@Directive({
|
||||
selector: 'adf-header-filter-template'
|
||||
})
|
||||
export class HeaderFilterTemplateDirective implements AfterContentInit {
|
||||
|
||||
@ContentChild(TemplateRef)
|
||||
template: any;
|
||||
|
||||
constructor(private dataTable: DataTableComponent) {
|
||||
}
|
||||
|
||||
ngAfterContentInit() {
|
||||
if (this.dataTable) {
|
||||
this.dataTable.headerFilterTemplate = this.template;
|
||||
}
|
||||
}
|
||||
}
|
@ -41,8 +41,10 @@ export * from './data/data-table.schema';
|
||||
export * from './directives/loading-template.directive';
|
||||
export * from './directives/no-content-template.directive';
|
||||
export * from './directives/no-permission-template.directive';
|
||||
export * from './directives/header-filter-template.directive';
|
||||
export * from './directives/custom-empty-content-template.directive';
|
||||
export * from './directives/custom-loading-template.directive';
|
||||
export * from './directives/custom-no-permission-template.directive';
|
||||
export * from './directives/custom-header-filter-template.directive';
|
||||
|
||||
export * from './datatable.module';
|
||||
|
Loading…
x
Reference in New Issue
Block a user