[ADF-5219] Refactor Document list Filters (#6092)

* First commit

* Second commit

* Commit 4

* Add unit tests

* Fix unit tests

* Add documentation for breaking change

* Fix rebase

* Fix unit test
This commit is contained in:
davidcanonieto
2020-09-25 11:08:15 +02:00
committed by GitHub
parent df2e53110c
commit b0a46f7eac
31 changed files with 796 additions and 631 deletions

View File

@@ -31,7 +31,7 @@ describe('DropdownBreadcrumb', () => {
let component: DropdownBreadcrumbComponent;
let fixture: ComponentFixture<DropdownBreadcrumbComponent>;
let documentList: DocumentListComponent;
let documentListService: DocumentListService = jasmine.createSpyObj({'loadFolderByNodeId' : of(''), 'isCustomSourceService': false});
let documentListService: DocumentListService = jasmine.createSpyObj({ 'loadFolderByNodeId': of(''), 'isCustomSourceService': false });
setupTestBed({
imports: [
@@ -39,7 +39,7 @@ describe('DropdownBreadcrumb', () => {
ContentTestingModule
],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
providers : [{ provide: DocumentListService, useValue: documentListService }]
providers: [{ provide: DocumentListService, useValue: documentListService }]
});
beforeEach(async(() => {
@@ -133,7 +133,7 @@ describe('DropdownBreadcrumb', () => {
});
});
it('should update document list when clicking on an option', (done) => {
it('should update document list when clicking on an option', (done) => {
component.target = documentList;
const fakeNodeWithCreatePermissionInstance = JSON.parse(JSON.stringify(fakeNodeWithCreatePermission));
fakeNodeWithCreatePermissionInstance.path.elements = [{ id: '1', name: 'Stark Industries' }];
@@ -144,7 +144,7 @@ describe('DropdownBreadcrumb', () => {
fixture.whenStable().then(() => {
clickOnTheFirstOption();
expect(documentListService.loadFolderByNodeId).toHaveBeenCalledWith('1', documentList.DEFAULT_PAGINATION, undefined, undefined, ['name ASC']);
expect(documentListService.loadFolderByNodeId).toHaveBeenCalledWith('1', documentList.DEFAULT_PAGINATION, undefined, undefined, null);
done();
});
});

View File

@@ -25,13 +25,13 @@
(sorting-changed)="onSortingChanged($event)"
[class.adf-datatable-gallery-thumbnails]="data.thumbnails">
<adf-header-filter-template>
<ng-template let-col>
<ng-template [ngTemplateOutlet]="customHeaderFilterTemplate?.template"
[ngTemplateOutletContext]="{$implicit: col}">
</ng-template>
</ng-template>
</adf-header-filter-template>
<div *ngIf="headerFilters">
<adf-filter-header
[currentFolderId]="currentFolderId"
[value]="filterValue"
(filterSelection)="onFilterSelectionChange($event)">
</adf-filter-header>
</div>
<adf-no-content-template>
<ng-template>

View File

@@ -1456,7 +1456,7 @@ describe('DocumentList', () => {
where: undefined,
maxItems: 25,
skipCount: 0,
orderBy: ['isFolder DESC', 'name asc'],
orderBy: ['isFolder desc', 'name asc'],
rootFolderId: 'fake-id'
}, ['test-include']);
});
@@ -1472,14 +1472,14 @@ describe('DocumentList', () => {
where: '(isFolder=true)',
maxItems: 25,
skipCount: 0,
orderBy: ['isFolder DESC', 'name asc'],
orderBy: ['isFolder desc', 'name asc'],
rootFolderId: 'fake-id'
}, ['test-include']);
});
it('should add orderBy in the server request', () => {
documentList.includeFields = ['test-include'];
documentList.sorting = ['size', 'DESC'];
documentList.sorting = ['size', 'desc'];
documentList.where = null;
documentList.currentFolderId = 'fake-id';
@@ -1489,12 +1489,13 @@ describe('DocumentList', () => {
maxItems: 25,
skipCount: 0,
where: null,
orderBy: ['isFolder DESC', 'size DESC'],
orderBy: ['isFolder desc', 'size desc'],
rootFolderId: 'fake-id'
}, ['test-include']);
});
it('should reset the pagination when enter in a new folder', () => {
documentList.ngOnChanges({ currentFolderId: new SimpleChange(undefined, 'fake-id', true) });
const folder = new FolderNode();
documentList.navigationMode = DocumentListComponent.SINGLE_CLICK_NAVIGATION;
documentList.updatePagination({
@@ -1505,7 +1506,7 @@ describe('DocumentList', () => {
expect(documentListService.getFolder).toHaveBeenCalledWith(null, Object({
maxItems: 10,
skipCount: 10,
orderBy: ['name ASC'],
orderBy: ['isFolder desc', 'name asc'],
rootFolderId: 'no-node',
where: undefined
}), undefined);
@@ -1515,7 +1516,7 @@ describe('DocumentList', () => {
expect(documentListService.getFolder).toHaveBeenCalledWith(null, Object({
maxItems: 25,
skipCount: 0,
orderBy: ['name ASC'],
orderBy: ['isFolder desc', 'name asc'],
rootFolderId: 'folder-id',
where: undefined
}), undefined);

View File

@@ -41,7 +41,6 @@ import {
CustomLoadingContentTemplateDirective,
CustomNoPermissionTemplateDirective,
CustomEmptyContentTemplateDirective,
CustomHeaderFilterTemplateDirective,
RequestPaginationModel,
AlfrescoApiService,
UserPreferenceValues,
@@ -58,6 +57,7 @@ import { ContentActionModel } from './../models/content-action.model';
import { PermissionStyleModel } from './../models/permissions-style.model';
import { NodeEntityEvent, NodeEntryEvent } from './node.event';
import { NavigableComponentInterface } from '../../breadcrumb/navigable-component.interface';
import { FilterSearch } from './../../search/filter-search.interface';
import { RowFilter } from '../data/row-filter.model';
import { DocumentListService } from '../services/document-list.service';
import { DocumentLoaderNode } from '../models/document-folder.model';
@@ -68,7 +68,7 @@ import { takeUntil } from 'rxjs/operators';
styleUrls: ['./document-list.component.scss'],
templateUrl: './document-list.component.html',
encapsulation: ViewEncapsulation.None,
host: {class: 'adf-document-list'}
host: { class: 'adf-document-list' }
})
export class DocumentListComponent implements OnInit, OnChanges, OnDestroy, AfterContentInit, PaginatedComponent, NavigableComponentInterface {
@@ -82,6 +82,11 @@ export class DocumentListComponent implements OnInit, OnChanges, OnDestroy, Afte
totalItems: 0
});
DEFAULT_SORTING: DataSorting[] = [
new DataSorting('name', 'asc'),
new DataSorting('isFolder', 'desc')
];
@ContentChild(DataColumnListComponent)
columnList: DataColumnListComponent;
@@ -94,9 +99,6 @@ 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[];
@@ -183,14 +185,14 @@ export class DocumentListComponent implements OnInit, OnChanges, OnDestroy, Afte
* override the default sorting detected by the component based on columns.
*/
@Input()
sorting = ['name', 'asc'];
sorting: string[] | DataSorting = ['name', 'asc'];
/** Defines default sorting. The format is an array of strings `[key direction, otherKey otherDirection]`
* i.e. `['name desc', 'nodeType asc']` or `['name asc']`. Set this value if you want a base
* rule to be added to the sorting apart from the one driven by the header.
*/
@Input()
additionalSorting = ['isFolder DESC'];
additionalSorting: DataSorting = new DataSorting('isFolder', 'desc');
/** Defines sorting mode. Can be either `client` (items in the list
* are sorted client-side) or `server` (the ordering supplied by the
@@ -255,6 +257,14 @@ export class DocumentListComponent implements OnInit, OnChanges, OnDestroy, Afte
@Input()
stickyHeader: boolean = false;
/** Toggles the header filters mode. */
@Input()
headerFilters: boolean = false;
/** Initial value for filter. */
@Input()
filterValue: any;
/** The ID of the folder node to display or a reserved string alias for special sources */
@Input()
currentFolderId: string = null;
@@ -305,6 +315,10 @@ export class DocumentListComponent implements OnInit, OnChanges, OnDestroy, Afte
@Output()
nodeSelected: EventEmitter<NodeEntry[]> = new EventEmitter<NodeEntry[]>();
/** Emitted when a filter value is selected */
@Output()
filterSelection: EventEmitter<FilterSearch[]> = new EventEmitter();
@ViewChild('dataTable', { static: true })
dataTable: DataTableComponent;
@@ -315,13 +329,14 @@ export class DocumentListComponent implements OnInit, OnChanges, OnDestroy, Afte
selection = new Array<NodeEntry>();
$folderNode: Subject<Node> = new Subject<Node>();
allowFiltering: boolean = true;
orderBy: string[] = ['name ASC'];
orderBy: string[] = null;
// @deprecated 3.0.0
folderNode: Node;
private _pagination: PaginationModel = this.DEFAULT_PAGINATION;
pagination: BehaviorSubject<PaginationModel> = new BehaviorSubject<PaginationModel>(this.DEFAULT_PAGINATION);
sortingSubject: BehaviorSubject<DataSorting[]> = new BehaviorSubject<DataSorting[]>(this.DEFAULT_SORTING);
private layoutPresets = {};
private rowMenuCache: { [key: string]: ContentActionModel[] } = {};
@@ -367,10 +382,13 @@ export class DocumentListComponent implements OnInit, OnChanges, OnDestroy, Afte
private getDefaultSorting(): DataSorting {
let defaultSorting: DataSorting;
if (this.sorting) {
if (Array.isArray(this.sorting)) {
const [key, direction] = this.sorting;
defaultSorting = new DataSorting(key, direction);
} else {
defaultSorting = new DataSorting(this.sorting.key, this.sorting.direction);
}
return defaultSorting;
}
@@ -440,9 +458,11 @@ export class DocumentListComponent implements OnInit, OnChanges, OnDestroy, Afte
ngOnChanges(changes: SimpleChanges) {
this.resetSelection();
if (this.sorting) {
if (Array.isArray(this.sorting)) {
const [key, direction] = this.sorting;
this.orderBy = this.buildOrderByArray(key, direction);
} else {
this.orderBy = this.buildOrderByArray(this.sorting.key, this.sorting.direction);
}
if (this.data) {
@@ -493,6 +513,7 @@ export class DocumentListComponent implements OnInit, OnChanges, OnDestroy, Afte
if (this.node) {
this.data.loadPage(this.node, this._pagination.merge, null, this.getPreselectNodesBasedOnSelectionMode());
this.onPreselectNodes();
this.syncPagination();
this.onDataReady(this.node);
} else {
this.loadFolder();
@@ -586,14 +607,14 @@ export class DocumentListComponent implements OnInit, OnChanges, OnDestroy, Afte
if (typeof node === 'string') {
this.resetNewFolderPagination();
this.currentFolderId = node;
this.folderChange.emit(new NodeEntryEvent(<Node> {id: node}));
this.folderChange.emit(new NodeEntryEvent(<Node> { id: node }));
this.reload();
return true;
} else {
if (this.canNavigateFolder(node)) {
this.resetNewFolderPagination();
this.currentFolderId = this.getNodeFolderDestinationId(node);
this.folderChange.emit(new NodeEntryEvent(<Node> {id: this.currentFolderId}));
this.folderChange.emit(new NodeEntryEvent(<Node> { id: this.currentFolderId }));
this.reload();
return true;
}
@@ -690,14 +711,16 @@ export class DocumentListComponent implements OnInit, OnChanges, OnDestroy, Afte
}
onSortingChanged(event: CustomEvent) {
this.orderBy = this.buildOrderByArray(event.detail.sortingKey, event.detail.direction);
this.orderBy = this.buildOrderByArray(event.detail.key, event.detail.direction);
this.reload();
this.sortingSubject.next([this.additionalSorting, event.detail]);
}
private buildOrderByArray(currentKey: string, currentDirection: string ): string[] {
const orderArray = [...this.additionalSorting];
orderArray.push(''.concat(currentKey, ' ', currentDirection));
return orderArray;
private buildOrderByArray(currentKey: string, currentDirection: string): string[] {
return [
`${this.additionalSorting.key} ${this.additionalSorting.direction}`,
`${currentKey} ${currentDirection}`
];
}
/**
@@ -874,6 +897,15 @@ export class DocumentListComponent implements OnInit, OnChanges, OnDestroy, Afte
this.reload();
}
private syncPagination() {
this.node.list.pagination.maxItems = this._pagination.maxItems;
this.node.list.pagination.skipCount = this._pagination.skipCount;
}
onFilterSelectionChange(activeFilters: FilterSearch[]) {
this.filterSelection.emit(activeFilters);
}
private resetNewFolderPagination() {
this._pagination.skipCount = 0;
this._pagination.maxItems = this.maxItems;

View File

@@ -0,0 +1,10 @@
<div *ngIf="isFilterServiceActive">
<adf-header-filter-template>
<ng-template let-col>
<adf-search-filter-container [col]="col"
[value]="value"
(filterChange)="onFilterSelectionChange()">
</adf-search-filter-container>
</ng-template>
</adf-header-filter-template>
</div>

View File

@@ -0,0 +1,151 @@
/*!
* @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, BehaviorSubject } from 'rxjs';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { TranslateModule } from '@ngx-translate/core';
import { SearchService, setupTestBed, DataTableComponent, DataSorting } from '@alfresco/adf-core';
import { ContentTestingModule } from '../../../testing/content.testing.module';
import { SimpleChange } from '@angular/core';
import { SearchFilterQueryBuilderService } from './../../../search/search-filter-query-builder.service';
import { SEARCH_QUERY_SERVICE_TOKEN } from './../../../search/search-query-service.token';
import { DocumentListComponent } from './../document-list.component';
import { FilterHeaderComponent } from './filter-header.component';
import { Pagination } from '@alfresco/js-api';
describe('FilterHeaderComponent', () => {
let fixture: ComponentFixture<FilterHeaderComponent>;
let component: FilterHeaderComponent;
let queryBuilder: SearchFilterQueryBuilderService;
const searchMock: any = {
dataLoaded: new Subject()
};
const paginationMock = <Pagination> { maxItems: 10, skipCount: 0 };
const documentListMock = {
node: 'my-node',
sorting: ['name', 'asc'],
pagination: new BehaviorSubject<Pagination>(paginationMock),
sortingSubject: new BehaviorSubject<DataSorting[]>([]),
reload: () => jasmine.createSpy('reload')
};
setupTestBed({
imports: [
TranslateModule.forRoot(),
ContentTestingModule
],
providers: [
{ provide: SearchService, useValue: searchMock },
{ provide: SEARCH_QUERY_SERVICE_TOKEN, useClass: SearchFilterQueryBuilderService },
{ provide: DocumentListComponent, useValue: documentListMock },
DataTableComponent
]
});
beforeEach(() => {
fixture = TestBed.createComponent(FilterHeaderComponent);
component = fixture.componentInstance;
queryBuilder = fixture.componentInstance['searchFilterQueryBuilder'];
});
afterEach(() => {
fixture.destroy();
});
it('should subscribe to changes in document list pagination', async () => {
const setupCurrentPaginationSpy = spyOn(queryBuilder, 'setupCurrentPagination');
const currentFolderNodeIdChange = new SimpleChange('current-node-id', 'next-node-id', true);
component.ngOnChanges({ currentFolderId: currentFolderNodeIdChange });
fixture.detectChanges();
await fixture.whenStable();
expect(setupCurrentPaginationSpy).toHaveBeenCalled();
});
it('should subscribe to changes in document list sorting', async () => {
const setSortingSpy = spyOn(queryBuilder, 'setSorting');
const currentFolderNodeIdChange = new SimpleChange('current-node-id', 'next-node-id', true);
component.ngOnChanges({ currentFolderId: currentFolderNodeIdChange });
fixture.detectChanges();
await fixture.whenStable();
expect(setSortingSpy).toHaveBeenCalled();
});
it('should reset filters after changing the folder node', async () => {
const resetActiveFiltersSpy = spyOn(queryBuilder, 'resetActiveFilters');
spyOn(queryBuilder, 'isCustomSourceNode').and.returnValue(false);
const currentFolderNodeIdChange = new SimpleChange('current-node-id', 'next-node-id', true);
component.ngOnChanges({ currentFolderId: currentFolderNodeIdChange });
fixture.detectChanges();
await fixture.whenStable();
expect(resetActiveFiltersSpy).toHaveBeenCalled();
});
it('should init filters after changing the folder node', async () => {
const setCurrentRootFolderIdSpy = spyOn(queryBuilder, 'setCurrentRootFolderId');
spyOn(queryBuilder, 'isCustomSourceNode').and.returnValue(false);
const currentFolderNodeIdChange = new SimpleChange('current-node-id', 'next-node-id', true);
component.ngOnChanges({ currentFolderId: currentFolderNodeIdChange });
fixture.detectChanges();
await fixture.whenStable();
expect(setCurrentRootFolderIdSpy).toHaveBeenCalled();
});
it('should set active filters when an initial value is set', async () => {
spyOn(queryBuilder, 'setCurrentRootFolderId');
spyOn(queryBuilder, 'isCustomSourceNode').and.returnValue(false);
fixture.detectChanges();
await fixture.whenStable();
expect(queryBuilder.getActiveFilters().length).toBe(0);
const initialFilterValue = { name: 'pinocchio'};
component.value = initialFilterValue;
const currentFolderNodeIdChange = new SimpleChange('current-node-id', 'next-node-id', true);
component.ngOnChanges({ currentFolderId: currentFolderNodeIdChange });
fixture.detectChanges();
await fixture.whenStable();
expect(queryBuilder.getActiveFilters().length).toBe(1);
expect(queryBuilder.getActiveFilters()[0].key).toBe('name');
expect(queryBuilder.getActiveFilters()[0].value).toBe('pinocchio');
});
it('should emit filterSelection when a filter is changed', async (done) => {
spyOn(queryBuilder, 'getActiveFilters').and.returnValue([{ key: 'name', value: 'pinocchio' }]);
component.filterSelection.subscribe((selectedFilters) => {
expect(selectedFilters.length).toBe(1);
expect(selectedFilters[0].key).toBe('name');
expect(selectedFilters[0].value).toBe('pinocchio');
done();
});
component.onFilterSelectionChange();
fixture.detectChanges();
await fixture.whenStable();
});
});

View File

@@ -0,0 +1,126 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Component, Inject, OnInit, OnChanges, SimpleChanges, Input, Output, EventEmitter } from '@angular/core';
import { PaginationModel, DataSorting } from '@alfresco/adf-core';
import { DocumentListComponent } from '../document-list.component';
import { SEARCH_QUERY_SERVICE_TOKEN } from '../../../search/search-query-service.token';
import { SearchFilterQueryBuilderService } from '../../../search/search-filter-query-builder.service';
import { FilterSearch } from './../../../search/filter-search.interface';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { NodePaging, MinimalNode } from '@alfresco/js-api';
@Component({
selector: 'adf-filter-header',
templateUrl: './filter-header.component.html',
providers: [{ provide: SEARCH_QUERY_SERVICE_TOKEN, useClass: SearchFilterQueryBuilderService}]
})
export class FilterHeaderComponent implements OnInit, OnChanges {
/** (optional) Initial filter value to sort . */
@Input()
value: any = {};
/** The id of the current folder of the document list. */
@Input()
currentFolderId: string;
/** Emitted when a filter value is selected */
@Output()
filterSelection: EventEmitter<FilterSearch[]> = new EventEmitter();
isFilterServiceActive: boolean;
private onDestroy$ = new Subject<boolean>();
constructor(@Inject(DocumentListComponent) private documentList: DocumentListComponent,
@Inject(SEARCH_QUERY_SERVICE_TOKEN) private searchFilterQueryBuilder: SearchFilterQueryBuilderService) {
this.isFilterServiceActive = this.searchFilterQueryBuilder.isFilterServiceActive();
}
ngOnInit() {
this.searchFilterQueryBuilder.executed
.pipe(takeUntil(this.onDestroy$))
.subscribe((newNodePaging: NodePaging) => {
this.documentList.node = newNodePaging;
this.documentList.reload();
});
this.initDataPagination();
this.initDataSorting();
}
ngOnChanges(changes: SimpleChanges) {
if (changes['currentFolderId'] && changes['currentFolderId'].currentValue) {
this.resetFilterHeader();
this.configureSearchParent(changes['currentFolderId'].currentValue);
}
}
onFilterSelectionChange() {
this.filterSelection.emit(this.searchFilterQueryBuilder.getActiveFilters());
if (this.searchFilterQueryBuilder.isNoFilterActive()) {
this.documentList.node = null;
this.documentList.reload();
}
}
resetFilterHeader() {
this.searchFilterQueryBuilder.resetActiveFilters();
}
initDataPagination() {
this.documentList.pagination
.pipe(takeUntil(this.onDestroy$))
.subscribe((newPagination: PaginationModel) => {
this.searchFilterQueryBuilder.setupCurrentPagination(newPagination.maxItems, newPagination.skipCount);
});
}
initDataSorting() {
this.documentList.sortingSubject
.pipe(takeUntil(this.onDestroy$))
.subscribe((sorting: DataSorting[]) => {
this.searchFilterQueryBuilder.setSorting(sorting);
});
}
private configureSearchParent(currentFolderId: string) {
if (this.searchFilterQueryBuilder.isCustomSourceNode(currentFolderId)) {
this.searchFilterQueryBuilder.getNodeIdForCustomSource(currentFolderId).subscribe((node: MinimalNode) => {
this.initSearchHeader(node.id);
});
} else {
this.initSearchHeader(currentFolderId);
}
}
private initSearchHeader(currentFolderId: string) {
this.searchFilterQueryBuilder.setCurrentRootFolderId(currentFolderId);
if (this.value) {
Object.keys(this.value).forEach((columnKey) => {
this.searchFilterQueryBuilder.setActiveFilter(columnKey, this.value[columnKey]);
});
}
}
ngOnDestroy() {
this.onDestroy$.next(true);
this.onDestroy$.complete();
}
}

View File

@@ -32,6 +32,8 @@ 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 { FilterHeaderComponent } from './components/filter-header/filter-header.component';
import { SearchModule } from './../search/search.module';
@NgModule({
imports: [
@@ -40,7 +42,8 @@ import { NameColumnComponent } from './components/name-column/name-column.compon
FlexLayoutModule,
MaterialModule,
UploadModule,
EditJsonDialogModule
EditJsonDialogModule,
SearchModule
],
declarations: [
DocumentListComponent,
@@ -50,7 +53,8 @@ import { NameColumnComponent } from './components/name-column/name-column.compon
LibraryNameColumnComponent,
NameColumnComponent,
ContentActionComponent,
ContentActionListComponent
ContentActionListComponent,
FilterHeaderComponent
],
exports: [
DocumentListComponent,
@@ -60,7 +64,8 @@ import { NameColumnComponent } from './components/name-column/name-column.compon
LibraryNameColumnComponent,
NameColumnComponent,
ContentActionComponent,
ContentActionListComponent
ContentActionListComponent,
FilterHeaderComponent
]
})
export class DocumentListModule {}

View File

@@ -46,14 +46,14 @@ export abstract class BaseQueryBuilderService {
executed = new Subject<ResultSetPaging>();
error = new Subject();
categories: Array<SearchCategory> = [];
categories: SearchCategory[] = [];
queryFragments: { [id: string]: string } = {};
filterQueries: FilterQuery[] = [];
paging: { maxItems?: number; skipCount?: number } = null;
sorting: Array<SearchSortingDefinition> = [];
sortingOptions: Array<SearchSortingDefinition> = [];
sorting: SearchSortingDefinition[] = [];
sortingOptions: SearchSortingDefinition[] = [];
protected userFacetBuckets: { [key: string]: Array<FacetFieldBucket> } = {};
protected userFacetBuckets: { [key: string]: FacetFieldBucket[] } = {};
get userQuery(): string {
return this._userQuery;

View File

@@ -0,0 +1,49 @@
<div *ngIf="!!category"
class="adf-filter">
<button mat-icon-button
[matMenuTriggerFor]="filter"
id="filter-menu-button"
#menuTrigger="matMenuTrigger"
(click)="$event.stopPropagation()"
(menuOpened)="onMenuOpen()"
(keyup.enter)="$event.stopPropagation()"
class="adf-filter-button"
[matTooltip]="getTooltipTranslation(col?.title)">
<adf-icon value="adf:filter"
[ngClass]="{ 'adf-icon-active': isActive() || menuTrigger.menuOpen }"
matBadge="filter"
matBadgeColor="warn"
[matBadgeHidden]="!isActive()">
</adf-icon>
</button>
<mat-menu #filter="matMenu"
class="adf-filter-menu"
(closed)="onClosed()">
<div #filterContainer
(keydown.tab)="$event.stopPropagation();">
<div (click)="$event.stopPropagation()"
class="adf-filter-container">
<div class="adf-filter-title">{{ category?.name | translate }}</div>
<adf-search-widget-container (keypress)="onKeyPressed($event, menuTrigger)"
[id]="category?.id"
[selector]="category?.component?.selector"
[settings]="category?.component?.settings"
[value]="initialValue">
</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)="onApply()">{{ 'SEARCH.SEARCH_HEADER.APPLY' | translate | uppercase }}
</button>
</mat-dialog-actions>
</div>
</mat-menu>
</div>

View File

@@ -18,16 +18,16 @@ 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 { SearchFilterQueryBuilderService } from '../../search-filter-query-builder.service';
import { ContentTestingModule } from '../../../testing/content.testing.module';
import { fakeNodePaging } from '../../../mock';
import { fakeNodePaging } from './../../../mock/document-list.component.mock';
import { SEARCH_QUERY_SERVICE_TOKEN } from '../../search-query-service.token';
import { By } from '@angular/platform-browser';
import { SimpleChange } from '@angular/core';
import { SearchFilterContainerComponent } from './search-filter-container.component';
import { MatMenuTrigger } from '@angular/material/menu';
import { SearchCategory } from '../../search-category.interface';
const mockCategory: any = {
const mockCategory: SearchCategory = {
'id': 'queryName',
'name': 'Name',
'columnKey': 'name',
@@ -43,10 +43,10 @@ const mockCategory: any = {
}
};
describe('SearchHeaderComponent', () => {
let fixture: ComponentFixture<SearchHeaderComponent>;
let component: SearchHeaderComponent;
let queryBuilder: SearchHeaderQueryBuilderService;
describe('SearchFilterContainerComponent', () => {
let fixture: ComponentFixture<SearchFilterContainerComponent>;
let component: SearchFilterContainerComponent;
let queryBuilder: SearchFilterQueryBuilderService;
let alfrescoApiService: AlfrescoApiService;
const searchMock: any = {
@@ -60,14 +60,14 @@ describe('SearchHeaderComponent', () => {
],
providers: [
{ provide: SearchService, useValue: searchMock },
{ provide: SEARCH_QUERY_SERVICE_TOKEN, useClass: SearchHeaderQueryBuilderService }
{ provide: SEARCH_QUERY_SERVICE_TOKEN, useClass: SearchFilterQueryBuilderService }
]
});
beforeEach(() => {
fixture = TestBed.createComponent(SearchHeaderComponent);
fixture = TestBed.createComponent(SearchFilterContainerComponent);
component = fixture.componentInstance;
queryBuilder = fixture.componentInstance['searchHeaderQueryBuilder'];
queryBuilder = fixture.componentInstance['searchFilterQueryBuilder'];
alfrescoApiService = TestBed.inject(AlfrescoApiService);
component.col = {key: '123', type: 'text'};
spyOn(queryBuilder, 'getCategoryForColumn').and.returnValue(mockCategory);
@@ -79,17 +79,33 @@ describe('SearchHeaderComponent', () => {
});
it('should show the filter when a category is found', async () => {
await fixture.whenStable();
fixture.detectChanges();
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 Apply button is clicked', async (done) => {
it('should set new active filter after the Apply button is clicked', async () => {
const menuButton: HTMLButtonElement = fixture.nativeElement.querySelector('#filter-menu-button');
menuButton.click();
fixture.detectChanges();
await fixture.whenStable();
component.widgetContainer.componentRef.instance.value = 'searchText';
const applyButton = fixture.debugElement.query(By.css('#apply-filter-button'));
applyButton.triggerEventHandler('click', {});
fixture.detectChanges();
await fixture.whenStable();
expect(queryBuilder.getActiveFilters().length).toBe(1);
expect(queryBuilder.getActiveFilters()[0].key).toBe('name');
expect(queryBuilder.getActiveFilters()[0].value).toBe('searchText');
});
it('should emit filterChange after the Apply button is clicked', async (done) => {
spyOn(alfrescoApiService.searchApi, 'search').and.returnValue(Promise.resolve(fakeNodePaging));
spyOn(queryBuilder, 'buildQuery').and.returnValue({});
component.update.subscribe((newNodePaging) => {
expect(newNodePaging).toBe(fakeNodePaging);
component.filterChange.subscribe(() => {
done();
});
const menuButton: HTMLButtonElement = fixture.nativeElement.querySelector('#filter-menu-button');
@@ -103,11 +119,42 @@ describe('SearchHeaderComponent', () => {
await fixture.whenStable();
});
it('should emit the node paging received from the queryBuilder after the Enter key is pressed', async (done) => {
it('should remove active filter after the Clear button is clicked', async () => {
queryBuilder.setActiveFilter('name', 'searchText');
const menuButton: HTMLButtonElement = fixture.nativeElement.querySelector('#filter-menu-button');
menuButton.click();
fixture.detectChanges();
await fixture.whenStable();
component.widgetContainer.componentRef.instance.value = 'searchText';
const fakeEvent = jasmine.createSpyObj('event', ['stopPropagation']);
const clearButton = fixture.debugElement.query(By.css('#clear-filter-button'));
clearButton.triggerEventHandler('click', fakeEvent);
fixture.detectChanges();
await fixture.whenStable();
expect(queryBuilder.getActiveFilters().length).toBe(0);
});
it('should emit filterChange after the Clear button is clicked', async (done) => {
spyOn(alfrescoApiService.searchApi, 'search').and.returnValue(Promise.resolve(fakeNodePaging));
spyOn(queryBuilder, 'buildQuery').and.returnValue({});
component.update.subscribe((newNodePaging) => {
expect(newNodePaging).toBe(fakeNodePaging);
component.filterChange.subscribe(() => {
done();
});
const menuButton: HTMLButtonElement = fixture.nativeElement.querySelector('#filter-menu-button');
menuButton.click();
fixture.detectChanges();
await fixture.whenStable();
component.widgetContainer.componentRef.instance.value = 'searchText';
const fakeEvent = jasmine.createSpyObj('event', ['stopPropagation']);
const clearButton = fixture.debugElement.query(By.css('#clear-filter-button'));
clearButton.triggerEventHandler('click', fakeEvent);
fixture.detectChanges();
await fixture.whenStable();
});
it('should emit filterChange after the Enter key is pressed', async (done) => {
spyOn(queryBuilder, 'buildQuery').and.returnValue({});
component.filterChange.subscribe(() => {
done();
});
const menuButton: HTMLButtonElement = fixture.nativeElement.querySelector('#filter-menu-button');
@@ -121,134 +168,6 @@ describe('SearchHeaderComponent', () => {
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 execute a new query when a new sorting 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(null, '123-asc', false);
component.ngOnChanges({ 'sorting': 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();
spyOn(component, 'isActive').and.returnValue(true);
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();
await fixture.whenStable();
});
it('should execute the query again if there are more filter actives after a clear', async (done) => {
spyOn(queryBuilder, 'isNoFilterActive').and.returnValue(false);
spyOn(alfrescoApiService.searchApi, 'search').and.returnValue(Promise.resolve(fakeNodePaging));
spyOn(queryBuilder, 'buildQuery').and.returnValue({});
spyOn(component, 'isActive').and.returnValue(true);
queryBuilder.queryFragments['fake'] = 'test';
spyOn(component.widgetContainer, 'resetInnerWidget').and.callThrough();
const fakeEvent = jasmine.createSpyObj('event', ['stopPropagation']);
const menuButton: HTMLButtonElement = fixture.nativeElement.querySelector('#filter-menu-button');
component.update.subscribe((newNodePaging) => {
expect(newNodePaging).toBe(fakeNodePaging);
done();
});
menuButton.click();
fixture.detectChanges();
await fixture.whenStable();
const clearButton = fixture.debugElement.query(By.css('#clear-filter-button'));
clearButton.triggerEventHandler('click', fakeEvent);
fixture.detectChanges();
await fixture.whenStable();
});
it('should emit the clear event when no filter has valued applied', async (done) => {
spyOn(queryBuilder, 'isNoFilterActive').and.returnValue(true);
spyOn(alfrescoApiService.searchApi, 'search').and.returnValue(Promise.resolve(fakeNodePaging));
spyOn(queryBuilder, 'buildQuery').and.returnValue({});
spyOn(component, 'isActive').and.returnValue(true);
spyOn(component.widgetContainer, 'resetInnerWidget').and.stub();
component.widgetContainer.componentRef.instance.value = '';
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 applyButton = fixture.debugElement.query(By.css('#apply-filter-button'));
applyButton.triggerEventHandler('click', fakeEvent);
fixture.detectChanges();
await fixture.whenStable();
});
it('should not emit clear event when currentFolderNodeId changes and no filter was applied', async () => {
const currentFolderNodeIdChange = new SimpleChange('current-node-id', 'next-node-id', true);
spyOn(component, 'isActive').and.returnValue(false);
spyOn(component.clear, 'emit');
component.ngOnChanges({ currentFolderNodeId: currentFolderNodeIdChange });
fixture.detectChanges();
expect(component.clear.emit).not.toHaveBeenCalled();
});
it('should emit clear event when currentFolderNodeId changes and filter was applied', async () => {
const currentFolderNodeIdChange = new SimpleChange('current-node-id', 'next-node-id', true);
spyOn(component.clear, 'emit');
spyOn(component, 'isActive').and.returnValue(true);
component.ngOnChanges({ currentFolderNodeId: currentFolderNodeIdChange });
fixture.detectChanges();
expect(component.clear.emit).toHaveBeenCalled();
});
describe('Accessibility', () => {
it('should set up a focus trap on the filter when the menu is opened', async () => {

View File

@@ -0,0 +1,136 @@
/*!
* @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,
EventEmitter,
ViewEncapsulation,
ViewChild,
Inject,
OnDestroy,
ElementRef
} from '@angular/core';
import { ConfigurableFocusTrapFactory, ConfigurableFocusTrap } from '@angular/cdk/a11y';
import { DataColumn, TranslationService } from '@alfresco/adf-core';
import { SearchWidgetContainerComponent } from '../search-widget-container/search-widget-container.component';
import { SearchFilterQueryBuilderService } from '../../search-filter-query-builder.service';
import { SearchCategory } from '../../search-category.interface';
import { SEARCH_QUERY_SERVICE_TOKEN } from '../../search-query-service.token';
import { Subject } from 'rxjs';
import { MatMenuTrigger } from '@angular/material/menu';
@Component({
selector: 'adf-search-filter-container',
templateUrl: './search-filter-container.component.html',
styleUrls: ['./search-filter-container.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class SearchFilterContainerComponent implements OnInit, OnDestroy {
/** The column the filter will be applied on. */
@Input()
col: DataColumn;
/** The column the filter will be applied on. */
@Input()
value: any;
/** Emitted when a filter value is selected */
@Output()
filterChange: EventEmitter<any> = new EventEmitter();
@ViewChild(SearchWidgetContainerComponent)
widgetContainer: SearchWidgetContainerComponent;
@ViewChild('filterContainer')
filterContainer: ElementRef;
category: SearchCategory;
focusTrap: ConfigurableFocusTrap;
initialValue: any;
private onDestroy$ = new Subject<boolean>();
constructor(@Inject(SEARCH_QUERY_SERVICE_TOKEN) private searchFilterQueryBuilder: SearchFilterQueryBuilderService,
private translationService: TranslationService,
private focusTrapFactory: ConfigurableFocusTrapFactory) {
}
ngOnInit() {
this.category = this.searchFilterQueryBuilder.getCategoryForColumn(this.col.key);
this.initialValue = this.value && this.value[this.col.key] ? this.value[this.col.key] : undefined;
}
ngOnDestroy() {
this.onDestroy$.next(true);
this.onDestroy$.complete();
}
onKeyPressed(event: KeyboardEvent, menuTrigger: MatMenuTrigger) {
if (event.key === 'Enter' && this.widgetContainer.selector !== 'check-list') {
this.onApply();
menuTrigger.closeMenu();
}
}
onApply() {
if (this.widgetContainer.hasValueSelected()) {
this.searchFilterQueryBuilder.setActiveFilter(this.category.columnKey, this.widgetContainer.getCurrentValue());
this.filterChange.emit();
this.widgetContainer.applyInnerWidget();
} else {
this.resetSearchFilter();
}
}
onClearButtonClick(event: Event) {
event.stopPropagation();
this.resetSearchFilter();
}
resetSearchFilter() {
this.widgetContainer.resetInnerWidget();
this.searchFilterQueryBuilder.removeActiveFilter(this.category.columnKey);
this.filterChange.emit();
}
getTooltipTranslation(columnTitle: string): string {
if (!columnTitle) {
columnTitle = 'SEARCH.SEARCH_HEADER.TYPE';
}
return this.translationService.instant('SEARCH.SEARCH_HEADER.FILTER_BY', { category: this.translationService.instant(columnTitle) });
}
isActive(): boolean {
return this.widgetContainer && this.widgetContainer.componentRef && this.widgetContainer.componentRef.instance.isActive;
}
onMenuOpen() {
if (this.filterContainer && !this.focusTrap) {
this.focusTrap = this.focusTrapFactory.create(this.filterContainer.nativeElement);
this.focusTrap.focusInitialElement();
}
}
onClosed() {
this.focusTrap.destroy();
this.focusTrap = null;
}
}

View File

@@ -1,43 +0,0 @@
<div *ngIf="isFilterServiceActive">
<div *ngIf="!!category" class="adf-filter">
<button mat-icon-button [matMenuTriggerFor]="filter"
id="filter-menu-button"
#menuTrigger="matMenuTrigger"
(click)="$event.stopPropagation()"
(menuOpened)="onMenuOpen()"
(keyup.enter)="$event.stopPropagation()"
class="adf-filter-button"
[matTooltip]="getTooltipTranslation(col?.title)">
<adf-icon value="adf:filter"
[ngClass]="{ 'adf-icon-active': isActive() || menuTrigger.menuOpen }"
matBadge="filter"
matBadgeColor="warn"
[matBadgeHidden]="!isActive()"></adf-icon>
</button>
<mat-menu #filter="matMenu" class="adf-filter-menu" (closed)="onClosed()">
<div #filterContainer (keydown.tab)="$event.stopPropagation();">
<div (click)="$event.stopPropagation()" class="adf-filter-container">
<div class="adf-filter-title">{{ category?.name | translate }}</div>
<adf-search-widget-container
(keypress)="onKeyPressed($event, menuTrigger)"
[id]="category?.id"
[selector]="category?.component?.selector"
[settings]="category?.component?.settings"
[value]="initialValue">
</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)="onApply()">{{ 'SEARCH.SEARCH_HEADER.APPLY' | translate | uppercase }}
</button>
</mat-dialog-actions>
</div>
</mat-menu>
</div>
</div>

View File

@@ -1,225 +0,0 @@
/*!
* @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,
ElementRef
} from '@angular/core';
import { ConfigurableFocusTrapFactory, ConfigurableFocusTrap } from '@angular/cdk/a11y';
import { DataColumn, TranslationService } from '@alfresco/adf-core';
import { SearchWidgetContainerComponent } from '../search-widget-container/search-widget-container.component';
import { SearchHeaderQueryBuilderService } from '../../search-header-query-builder.service';
import { NodePaging, MinimalNode } 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';
import { MatMenuTrigger } from '@angular/material/menu';
@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 {
/** The column the filter will be applied on. */
@Input()
col: DataColumn;
/** (optional) Initial filter value to sort . */
@Input()
value: any;
/** The id of the current folder of the document list. */
@Input()
currentFolderNodeId: string;
/** Maximum number of search results to show in a page. */
@Input()
maxItems: number;
/** The offset of the start of the page within the results list. */
@Input()
skipCount: number;
/** The sorting to apply to the the filter. */
@Input()
sorting: string = null;
/** Emitted when the result of the filter is received from the API. */
@Output()
update: EventEmitter<NodePaging> = new EventEmitter();
/** Emitted when the last of all the filters is cleared. */
@Output()
clear: EventEmitter<any> = new EventEmitter();
/** Emitted when a filter value is selected */
@Output()
selection: EventEmitter<Map<string, string>> = new EventEmitter();
@ViewChild(SearchWidgetContainerComponent)
widgetContainer: SearchWidgetContainerComponent;
@ViewChild('filterContainer')
filterContainer: ElementRef;
category: SearchCategory;
isFilterServiceActive: boolean;
initialValue: any;
focusTrap: ConfigurableFocusTrap;
private onDestroy$ = new Subject<boolean>();
constructor(@Inject(SEARCH_QUERY_SERVICE_TOKEN) private searchHeaderQueryBuilder: SearchHeaderQueryBuilderService,
private translationService: TranslationService,
private focusTrapFactory: ConfigurableFocusTrapFactory) {
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) {
this.clearHeader();
this.configureSearchParent(changes['currentFolderNodeId'].currentValue);
}
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);
}
if (changes['sorting'] && changes['sorting'].currentValue) {
const [key, value] = changes['sorting'].currentValue.split('-');
if (key === this.col.key) {
this.searchHeaderQueryBuilder.setSorting(key, value);
}
}
}
ngOnDestroy() {
this.onDestroy$.next(true);
this.onDestroy$.complete();
}
onKeyPressed(event: KeyboardEvent, menuTrigger: MatMenuTrigger) {
if (event.key === 'Enter' && this.widgetContainer.selector !== 'check-list') {
this.onApply();
menuTrigger.closeMenu();
}
}
onApply() {
if (this.widgetContainer.hasValueSelected()) {
this.widgetContainer.applyInnerWidget();
this.searchHeaderQueryBuilder.setActiveFilter(this.category.columnKey, this.widgetContainer.getCurrentValue());
this.selection.emit(this.searchHeaderQueryBuilder.getActiveFilters());
} else {
this.clearHeader();
}
}
onClearButtonClick(event: Event) {
event.stopPropagation();
this.clearHeader();
}
clearHeader() {
if (this.widgetContainer && this.isActive()) {
this.widgetContainer.resetInnerWidget();
this.searchHeaderQueryBuilder.removeActiveFilter(this.category.columnKey);
this.selection.emit(this.searchHeaderQueryBuilder.getActiveFilters());
if (this.searchHeaderQueryBuilder.isNoFilterActive()) {
this.clear.emit();
}
}
}
getTooltipTranslation(columnTitle: string): string {
if (!columnTitle) {
columnTitle = 'SEARCH.SEARCH_HEADER.TYPE';
}
return this.translationService.instant('SEARCH.SEARCH_HEADER.FILTER_BY', { category: this.translationService.instant(columnTitle) });
}
isActive(): boolean {
return this.widgetContainer && this.widgetContainer.componentRef && this.widgetContainer.componentRef.instance.isActive;
}
private configureSearchParent(currentFolderNodeId: string) {
if (this.searchHeaderQueryBuilder.isCustomSourceNode(currentFolderNodeId)) {
this.searchHeaderQueryBuilder.getNodeIdForCustomSource(currentFolderNodeId).subscribe((node: MinimalNode) => {
this.initSearchHeader(node.id);
});
} else {
this.initSearchHeader(currentFolderNodeId);
}
}
private initSearchHeader(currentFolderId: string) {
this.searchHeaderQueryBuilder.setCurrentRootFolderId(currentFolderId);
if (this.value) {
this.searchHeaderQueryBuilder.setActiveFilter(this.category.columnKey, this.initialValue);
this.initialValue = this.value;
}
}
onMenuOpen() {
if (this.filterContainer && !this.focusTrap) {
this.focusTrap = this.focusTrapFactory.create(this.filterContainer.nativeElement);
this.focusTrap.focusInitialElement();
}
}
onClosed() {
this.focusTrap.destroy();
this.focusTrap = null;
}
}

View File

@@ -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.
*/
export interface FilterSearch {
key: string;
value: any;
}

View File

@@ -19,6 +19,7 @@ export * from './facet-field-bucket.interface';
export * from './facet-field.interface';
export * from './facet-query.interface';
export * from './filter-query.interface';
export * from './filter-search.interface';
export * from './search-category.interface';
export * from './search-widget-settings.interface';
export * from './search-widget.interface';
@@ -26,7 +27,7 @@ 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 './search-filter-query-builder.service';
export * from './components/search.component';
export * from './components/search-control.component';
@@ -38,7 +39,7 @@ export * from './components/search-chip-list/search-chip-list.component';
export * from './components/search-date-range/search-date-range.component';
export * from './components/search-filter/search-filter.component';
export * from './components/search-filter/search-filter.service';
export * from './components/search-header/search-header.component';
export * from './components/search-filter-container/search-filter-container.component';
export * from './components/search-number-range/search-number-range.component';
export * from './components/search-radio/search-radio.component';
export * from './components/search-slider/search-slider.component';

View File

@@ -17,9 +17,9 @@
import { SearchConfiguration } from './search-configuration.interface';
import { AppConfigService } from '@alfresco/adf-core';
import { SearchHeaderQueryBuilderService } from './search-header-query-builder.service';
import { SearchFilterQueryBuilderService } from './search-filter-query-builder.service';
describe('SearchHeaderQueryBuilder', () => {
describe('SearchFilterQueryBuilderService', () => {
const buildConfig = (searchSettings): AppConfigService => {
const config = new AppConfigService(null);
@@ -36,7 +36,7 @@ describe('SearchHeaderQueryBuilder', () => {
filterQueries: [{ query: 'query1' }, { query: 'query2' }]
};
const builder = new SearchHeaderQueryBuilderService(
const builder = new SearchFilterQueryBuilderService(
buildConfig(config),
null,
null
@@ -63,7 +63,7 @@ describe('SearchHeaderQueryBuilder', () => {
filterQueries: [{ query: 'query1' }, { query: 'query2' }]
};
const service = new SearchHeaderQueryBuilderService(
const service = new SearchFilterQueryBuilderService(
buildConfig(config),
null,
null
@@ -76,7 +76,7 @@ describe('SearchHeaderQueryBuilder', () => {
});
it('should have empty user query by default', () => {
const builder = new SearchHeaderQueryBuilderService(
const builder = new SearchFilterQueryBuilderService(
buildConfig({}),
null,
null
@@ -97,7 +97,7 @@ describe('SearchHeaderQueryBuilder', () => {
{ query: 'PARENT:"workspace://SpacesStore/fake-node-id"' }
];
const searchHeaderService = new SearchHeaderQueryBuilderService(
const searchHeaderService = new SearchFilterQueryBuilderService(
buildConfig(config),
null,
null
@@ -122,7 +122,7 @@ describe('SearchHeaderQueryBuilder', () => {
filterQueries: expectedResult
};
const searchHeaderService = new SearchHeaderQueryBuilderService(
const searchHeaderService = new SearchFilterQueryBuilderService(
buildConfig(config),
null,
null
@@ -148,17 +148,17 @@ describe('SearchHeaderQueryBuilder', () => {
]
};
const searchHeaderService = new SearchHeaderQueryBuilderService(
const searchHeaderService = new SearchFilterQueryBuilderService(
buildConfig(config),
null,
null
);
expect(searchHeaderService.activeFilters.size).toBe(0);
expect(searchHeaderService.activeFilters.length).toBe(0);
searchHeaderService.setActiveFilter(activeFilter, 'fake-value');
searchHeaderService.setActiveFilter(activeFilter, 'fake-value');
expect(searchHeaderService.activeFilters.size).toBe(1);
expect(searchHeaderService.activeFilters.length).toBe(1);
});
});

View File

@@ -16,7 +16,7 @@
*/
import { Injectable } from '@angular/core';
import { AlfrescoApiService, AppConfigService, NodesApiService } from '@alfresco/adf-core';
import { AlfrescoApiService, AppConfigService, NodesApiService, DataSorting } from '@alfresco/adf-core';
import { SearchConfiguration } from './search-configuration.interface';
import { BaseQueryBuilderService } from './base-query-builder.service';
import { SearchCategory } from './search-category.interface';
@@ -24,23 +24,26 @@ import { MinimalNode, QueryBody } from '@alfresco/js-api';
import { filter } from 'rxjs/operators';
import { Observable } from 'rxjs';
import { SearchSortingDefinition } from './search-sorting-definition.interface';
import { FilterSearch } from './filter-search.interface';
@Injectable({
providedIn: 'root'
})
export class SearchHeaderQueryBuilderService extends BaseQueryBuilderService {
export class SearchFilterQueryBuilderService extends BaseQueryBuilderService {
private customSources = ['-trashcan-', '-sharedlinks-', '-sites-', '-mysites-', '-favorites-', '-recent-', '-my-'];
activeFilters: Map<string, string> = new Map();
activeFilters: FilterSearch[] = [];
constructor(appConfig: AppConfigService, alfrescoApiService: AlfrescoApiService, private nodeApiService: NodesApiService) {
constructor(appConfig: AppConfigService,
alfrescoApiService: AlfrescoApiService,
private nodeApiService: NodesApiService) {
super(appConfig, alfrescoApiService);
this.updated.pipe(
filter((query: QueryBody) => !!query)).subscribe(() => {
this.execute();
});
this.execute();
});
}
public isFilterServiceActive(): boolean {
@@ -61,35 +64,53 @@ export class SearchHeaderQueryBuilderService extends BaseQueryBuilderService {
}
setActiveFilter(columnActivated: string, filterValue: string) {
this.activeFilters.set(columnActivated, filterValue);
const filterIndex = this.activeFilters.find((activeFilter) => activeFilter.key === columnActivated);
if (!filterIndex) {
this.activeFilters.push(<FilterSearch> {
key: columnActivated,
value: filterValue
});
}
}
getActiveFilters(): Map<string, string> {
resetActiveFilters() {
this.activeFilters = [];
}
getActiveFilters(): FilterSearch[] {
return this.activeFilters;
}
isNoFilterActive(): boolean {
return this.activeFilters.size === 0;
return this.activeFilters.length === 0;
}
removeActiveFilter(columnRemoved: string) {
if (this.activeFilters.get(columnRemoved) !== null) {
this.activeFilters.delete(columnRemoved);
const filterIndex = this.activeFilters.map((activeFilter) => activeFilter.key).indexOf(columnRemoved);
if (filterIndex >= 0) {
this.activeFilters.splice(filterIndex, 1);
}
}
setSorting(column: string, direction: string) {
const optionAscending = direction.toLocaleLowerCase() === 'asc' ? true : false;
const fieldValue = this.getSortingFieldFromColumnName(column);
const currentSort: SearchSortingDefinition = { key: column, label: 'current', type: 'FIELD', field: fieldValue, ascending: optionAscending};
this.sorting = [currentSort];
setSorting(dataSorting: DataSorting[]) {
this.sorting = [];
dataSorting.forEach((columnSorting: DataSorting) => {
const fieldValue = this.getSortingFieldFromColumnName(columnSorting.key);
if (fieldValue) {
const optionAscending = columnSorting.direction.toLocaleLowerCase() === 'asc' ? true : false;
const currentSort: SearchSortingDefinition = { key: columnSorting.key, label: 'current', type: 'FIELD', field: fieldValue, ascending: optionAscending };
this.sorting.push(currentSort);
}
});
this.execute();
}
private getSortingFieldFromColumnName(columnName: string) {
if (this.sortingOptions.length > 0) {
const sortOption: SearchSortingDefinition = this.sortingOptions.find((option: SearchSortingDefinition) => option.key === columnName);
return sortOption.field;
return sortOption ? sortOption.field : '';
}
return '';
}
@@ -116,6 +137,8 @@ export class SearchHeaderQueryBuilderService extends BaseQueryBuilderService {
this.filterQueries = [{
query: `PARENT:"workspace://SpacesStore/${currentFolderId}"`
}];
this.execute();
}
isCustomSourceNode(currentNodeId: string): boolean {

View File

@@ -35,9 +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';
import { SearchFilterContainerComponent } from './components/search-filter-container/search-filter-container.component';
@NgModule({
imports: [
@@ -61,7 +61,7 @@ import { SearchQueryBuilderService } from './search-query-builder.service';
SearchCheckListComponent,
SearchDateRangeComponent,
SearchSortingPickerComponent,
SearchHeaderComponent
SearchFilterContainerComponent
],
exports: [
SearchComponent,
@@ -77,7 +77,7 @@ import { SearchQueryBuilderService } from './search-query-builder.service';
SearchCheckListComponent,
SearchDateRangeComponent,
SearchSortingPickerComponent,
SearchHeaderComponent
SearchFilterContainerComponent
],
providers: [
{ provide: SEARCH_QUERY_SERVICE_TOKEN, useExisting: SearchQueryBuilderService },

View File

@@ -13,7 +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 '../search/components/search-filter-container/search-filter-container.component';
@import '../dialogs/folder.dialog';