[ACA-3506] - Filter are kept when reloaded (#5885)

* [ADF] - saving in the url the filter values

* Fixed filter status on refresh

* Fixed filter status on refresh

* [ACA-3506] - added url filtering save

* [ACA-3506] - fixed spellcheck

* improve log

* more log

* fix scripts

* Added documentation for allowUpdateOnChange setting

* Added default value in description for docs

Co-authored-by: Vito Albano <vitoalbano@Vitos-MacBook-Pro.local>
Co-authored-by: Eugenio Romano <eugenio.romano@alfresco.com>
This commit is contained in:
Vito
2020-07-20 11:39:51 +01:00
committed by GitHub
parent 44c5472fa2
commit 3b7f3a5762
33 changed files with 316 additions and 122 deletions

View File

@@ -15,7 +15,7 @@
</mat-checkbox>
</div>
<div class="adf-facet-buttons" *ngIf="options.fitsPage">
<button mat-button color="primary" (click)="reset()">
{{ 'SEARCH.FILTER.ACTIONS.CLEAR-ALL' | translate }}

View File

@@ -42,8 +42,10 @@ export class SearchCheckListComponent implements SearchWidget, OnInit {
context?: SearchQueryBuilderService;
options: SearchFilterList<SearchListOption>;
operator: string = 'OR';
startValue: SearchListOption = null;
pageSize = 5;
isActive = false;
enableChangeUpdate = true;
constructor() {
this.options = new SearchFilterList<SearchListOption>();
@@ -57,6 +59,15 @@ export class SearchCheckListComponent implements SearchWidget, OnInit {
if (this.settings.options && this.settings.options.length > 0) {
this.options = new SearchFilterList(this.settings.options, this.pageSize);
}
if (this.settings.allowUpdateOnChange !== undefined &&
this.settings.allowUpdateOnChange !== null) {
this.enableChangeUpdate = this.settings.allowUpdateOnChange;
}
}
if (this.startValue) {
this.setValue(this.startValue);
}
}
@@ -74,7 +85,11 @@ export class SearchCheckListComponent implements SearchWidget, OnInit {
changeHandler(event: MatCheckboxChange, option: any) {
option.checked = event.checked;
this.submitValues();
const checkedValues = this.getCheckedValues();
this.isActive = !!checkedValues.length;
if (this.enableChangeUpdate) {
this.submitValues();
}
}
hasValidValue() {
@@ -82,6 +97,16 @@ export class SearchCheckListComponent implements SearchWidget, OnInit {
return !!checkedValues.length;
}
getCurrentValue() {
return this.getCheckedValues();
}
setValue(value: any) {
this.options.items.filter((item) => value.includes(item.value))
.map((item) => item.checked = true);
this.submitValues();
}
private getCheckedValues() {
return this.options.items
.filter((option) => option.checked)
@@ -90,11 +115,7 @@ export class SearchCheckListComponent implements SearchWidget, OnInit {
submitValues() {
const checkedValues = this.getCheckedValues();
this.isActive = !!checkedValues.length;
const query = checkedValues.join(` ${this.operator} `);
if (this.id && this.context) {
this.context.queryFragments[this.id] = query;
this.context.update();

View File

@@ -57,6 +57,7 @@ export class SearchDateRangeComponent implements SearchWidget, OnInit, OnDestroy
datePickerDateFormat = DEFAULT_FORMAT_DATE;
maxDate: any;
isActive = false;
startValue: any;
private onDestroy$ = new Subject<boolean>();
@@ -95,14 +96,6 @@ export class SearchDateRangeComponent implements SearchWidget, OnInit, OnDestroy
Validators.required
]);
this.from = new FormControl('', validators);
this.to = new FormControl('', validators);
this.form = new FormGroup({
from: this.from,
to: this.to
});
if (this.settings && this.settings.maxDate) {
if (this.settings.maxDate === 'today') {
this.maxDate = this.dateAdapter.today().endOf('day');
@@ -110,6 +103,22 @@ export class SearchDateRangeComponent implements SearchWidget, OnInit, OnDestroy
this.maxDate = moment(this.settings.maxDate).endOf('day');
}
}
if (this.startValue) {
const splitValue = this.startValue.split('||');
const fromValue = this.dateAdapter.parse(splitValue[0], this.datePickerDateFormat);
const toValue = this.dateAdapter.parse(splitValue[1], this.datePickerDateFormat);
this.from = new FormControl(fromValue, validators);
this.to = new FormControl(toValue, validators);
} else {
this.from = new FormControl('', validators);
this.to = new FormControl('', validators);
}
this.form = new FormGroup({
from: this.from,
to: this.to
});
}
ngOnDestroy() {
@@ -137,6 +146,24 @@ export class SearchDateRangeComponent implements SearchWidget, OnInit, OnDestroy
return this.form.valid;
}
getCurrentValue() {
return { from : this.dateAdapter.format(this.form.value.from, this.datePickerDateFormat),
to: this.dateAdapter.format(this.form.value.from, this.datePickerDateFormat) };
}
setValue(parsedDate: string) {
const splitValue = parsedDate.split('||');
const fromValue = this.dateAdapter.parse(splitValue[0], this.datePickerDateFormat);
const toValue = this.dateAdapter.parse(splitValue[1], this.datePickerDateFormat);
this.from.setValue(fromValue);
this.from.markAsDirty();
this.from.markAsTouched();
this.to.setValue(toValue);
this.to.markAsDirty();
this.to.markAsTouched();
this.submitValues();
}
reset() {
this.isActive = false;
this.form.reset({
@@ -150,6 +177,7 @@ export class SearchDateRangeComponent implements SearchWidget, OnInit, OnDestroy
}
onChangedHandler(event: any, formControl: FormControl) {
const inputValue = event.srcElement.value;
const formatDate = this.dateAdapter.parse(inputValue, this.datePickerDateFormat);

View File

@@ -8,7 +8,7 @@
(keyup.enter)="$event.stopPropagation()"
class="adf-filter-button"
[matTooltip]="getTooltipTranslation(col?.title)">
<adf-icon value="adf:filter"
<adf-icon value="adf:filter"
[ngClass]="{ 'adf-icon-active': isActive() || menuTrigger.menuOpen }"
matBadge="filter"
matBadgeColor="warn"
@@ -23,7 +23,8 @@
(keypress)="onKeyPressed($event, menuTrigger)"
[id]="category?.id"
[selector]="category?.component?.selector"
[settings]="category?.component?.settings">
[settings]="category?.component?.settings"
[value]="initialValue">
</adf-search-widget-container>
</div>
<mat-dialog-actions class="adf-filter-actions">

View File

@@ -173,7 +173,8 @@ describe('SearchHeaderComponent', () => {
spyOn(queryBuilder, 'isNoFilterActive').and.returnValue(false);
spyOn(alfrescoApiService.searchApi, 'search').and.returnValue(Promise.resolve(fakeNodePaging));
spyOn(queryBuilder, 'buildQuery').and.returnValue({});
spyOn(component.widgetContainer, 'resetInnerWidget').and.stub();
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) => {

View File

@@ -33,7 +33,7 @@ import { ConfigurableFocusTrapFactory, ConfigurableFocusTrap } from '@angular/cd
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 } from '@alfresco/js-api';
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';
@@ -52,6 +52,9 @@ export class SearchHeaderComponent implements OnInit, OnChanges, OnDestroy {
@Input()
col: DataColumn;
@Input()
value: any;
/** The id of the current folder of the document list. */
@Input()
currentFolderNodeId: string;
@@ -72,6 +75,10 @@ export class SearchHeaderComponent implements OnInit, OnChanges, OnDestroy {
@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;
@@ -80,6 +87,7 @@ export class SearchHeaderComponent implements OnInit, OnChanges, OnDestroy {
category: SearchCategory;
isFilterServiceActive: boolean;
initialValue: any;
focusTrap: ConfigurableFocusTrap;
private onDestroy$ = new Subject<boolean>();
@@ -100,11 +108,18 @@ export class SearchHeaderComponent implements OnInit, OnChanges, OnDestroy {
.subscribe((newNodePaging: NodePaging) => {
this.update.emit(newNodePaging);
});
if (this.searchHeaderQueryBuilder.isCustomSourceNode(this.currentFolderNodeId)) {
this.searchHeaderQueryBuilder.getNodeIdForCustomSource(this.currentFolderNodeId).subscribe((node: MinimalNode) => {
this.initSearchHeader(node.id);
});
} else {
this.initSearchHeader(this.currentFolderNodeId);
}
}
ngOnChanges(changes: SimpleChanges) {
if (changes['currentFolderNodeId'] && changes['currentFolderNodeId'].currentValue !== changes['currentFolderNodeId'].previousValue) {
this.searchHeaderQueryBuilder.setCurrentRootFolderId(changes['currentFolderNodeId'].currentValue);
if (changes['currentFolderNodeId'] && changes['currentFolderNodeId'].currentValue) {
this.clearHeader();
}
@@ -138,8 +153,8 @@ export class SearchHeaderComponent implements OnInit, OnChanges, OnDestroy {
onApply() {
if (this.widgetContainer.hasValueSelected()) {
this.widgetContainer.applyInnerWidget();
this.searchHeaderQueryBuilder.setActiveFilter(this.category.columnKey);
this.searchHeaderQueryBuilder.execute();
this.searchHeaderQueryBuilder.setActiveFilter(this.category.columnKey, this.widgetContainer.getCurrentValue());
this.selection.emit(this.searchHeaderQueryBuilder.getActiveFilters());
} else {
this.clearHeader();
}
@@ -154,10 +169,9 @@ export class SearchHeaderComponent implements OnInit, OnChanges, OnDestroy {
if (this.widgetContainer) {
this.widgetContainer.resetInnerWidget();
this.searchHeaderQueryBuilder.removeActiveFilter(this.category.columnKey);
this.selection.emit(this.searchHeaderQueryBuilder.getActiveFilters());
if (this.searchHeaderQueryBuilder.isNoFilterActive()) {
this.clear.emit();
} else {
this.searchHeaderQueryBuilder.execute();
}
}
}
@@ -173,6 +187,14 @@ export class SearchHeaderComponent implements OnInit, OnChanges, OnDestroy {
return this.widgetContainer && this.widgetContainer.componentRef && this.widgetContainer.componentRef.instance.isActive;
}
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);

View File

@@ -45,6 +45,7 @@ export class SearchNumberRangeComponent implements SearchWidget, OnInit {
format = '[{FROM} TO {TO}]';
isActive = false;
startValue: any;
validators: Validators;
@@ -61,8 +62,13 @@ export class SearchNumberRangeComponent implements SearchWidget, OnInit {
Validators.min(0)
]);
this.from = new FormControl('', this.validators);
this.to = new FormControl('', this.validators);
if (this.startValue) {
this.from = new FormControl(this.startValue['from'], this.validators);
this.to = new FormControl(this.startValue['to'], this.validators);
} else {
this.from = new FormControl('', this.validators);
this.to = new FormControl('', this.validators);
}
this.form = new FormGroup({
from: this.from,
@@ -108,6 +114,15 @@ export class SearchNumberRangeComponent implements SearchWidget, OnInit {
return this.form.valid;
}
getCurrentValue() {
return this.form.value;
}
setValue(value: any) {
this.form['from'].setValue(value);
this.form['to'].setValue(value);
}
reset() {
this.isActive = false;

View File

@@ -41,10 +41,10 @@ describe('SearchRadioComponent', () => {
describe('Pagination', () => {
it('should show 5 items when pageSize not defined', () => {
component.id = 'checklist';
component.id = 'radio';
component.context = <any> {
queryFragments: {
'checklist': 'query'
'radio': 'query'
},
update() {}
};
@@ -60,10 +60,10 @@ describe('SearchRadioComponent', () => {
});
it('should show all items when pageSize is high', () => {
component.id = 'checklist';
component.id = 'radio';
component.context = <any> {
queryFragments: {
'checklist': 'query'
'radio': 'query'
},
update() {}
};
@@ -78,21 +78,22 @@ describe('SearchRadioComponent', () => {
});
});
it('should able to check the radio button', () => {
component.id = 'checklist';
it('should able to check the radio button', async () => {
component.id = 'radio';
component.context = <any> {
queryFragments: {
'checklist': 'query'
'radio': 'query'
},
update: () => {}
};
component.settings = <any> { options: sizeOptions };
spyOn(component.context, 'update').and.stub();
component.ngOnInit();
fixture.detectChanges();
await fixture.whenStable();
const optionElements = fixture.debugElement.query(By.css('mat-radio-button'));
optionElements.triggerEventHandler('change', { checked: true });
fixture.detectChanges();
expect(component.context.update).toHaveBeenCalled();
expect(component.context.queryFragments[component.id]).toBe(sizeOptions[0].value);

View File

@@ -47,6 +47,7 @@ export class SearchRadioComponent implements SearchWidget, OnInit {
options: SearchFilterList<SearchRadioOption>;
pageSize = 5;
isActive = false;
startValue: any;
constructor() {
this.options = new SearchFilterList<SearchRadioOption>();
@@ -64,8 +65,11 @@ export class SearchRadioComponent implements SearchWidget, OnInit {
}
const initialValue = this.getSelectedValue();
if (initialValue !== null) {
this.setValue(initialValue);
} else if (this.startValue !== null) {
this.setValue(initialValue);
}
}
@@ -93,12 +97,16 @@ export class SearchRadioComponent implements SearchWidget, OnInit {
return !!currentValue;
}
private setValue(newValue: string) {
setValue(newValue: string) {
this.value = newValue;
this.context.queryFragments[this.id] = newValue;
this.context.update();
}
getCurrentValue() {
return this.getSelectedValue();
}
changeHandler(event: MatRadioChange) {
this.setValue(event.value);
}

View File

@@ -29,6 +29,8 @@ import { MatSliderChange } from '@angular/material/slider';
host: { class: 'adf-search-slider' }
})
export class SearchSliderComponent implements SearchWidget, OnInit {
isActive?: boolean;
startValue: any;
id: string;
settings: SearchWidgetSettings;
@@ -58,6 +60,10 @@ export class SearchSliderComponent implements SearchWidget, OnInit {
this.thumbLabel = this.settings['thumbLabel'] ? true : false;
}
if (this.startValue) {
this.setValue(this.startValue);
}
}
reset() {
@@ -78,6 +84,15 @@ export class SearchSliderComponent implements SearchWidget, OnInit {
return !!this.value;
}
getCurrentValue() {
return this.value;
}
setValue(value: any) {
this.value = value;
this.submitValues();
}
private updateQuery(value: number | null) {
if (this.id && this.context && this.settings && this.settings.field) {
if (value === null) {

View File

@@ -36,16 +36,26 @@ export class SearchTextComponent implements SearchWidget, OnInit {
id: string;
settings: SearchWidgetSettings;
context: SearchQueryBuilderService;
startValue: string;
isActive = false;
enableChangeUpdate = true;
ngOnInit() {
if (this.context && this.settings && this.settings.pattern) {
const pattern = new RegExp(this.settings.pattern, 'g');
const match = pattern.exec(this.context.queryFragments[this.id] || '');
if (this.settings.allowUpdateOnChange !== undefined &&
this.settings.allowUpdateOnChange !== null) {
this.enableChangeUpdate = this.settings.allowUpdateOnChange;
}
if (match && match.length > 1) {
this.value = match[1];
}
if (this.startValue) {
this.setValue(this.startValue);
}
}
}
@@ -58,12 +68,13 @@ export class SearchTextComponent implements SearchWidget, OnInit {
onChangedHandler(event) {
this.value = event.target.value;
this.updateQuery(this.value);
this.isActive = !!this.value;
if (this.enableChangeUpdate) {
this.updateQuery(this.value);
}
}
private updateQuery(value: string) {
this.isActive = !!value;
if (this.context && this.settings && this.settings.field) {
this.context.queryFragments[this.id] = value ? `${this.settings.field}:'${this.getSearchPrefix()}${value}${this.getSearchSuffix()}'` : '';
this.context.update();
@@ -79,6 +90,15 @@ export class SearchTextComponent implements SearchWidget, OnInit {
return !!this.value;
}
getCurrentValue() {
return this.value;
}
setValue(value: string) {
this.value = value;
this.submitValues();
}
private getSearchPrefix(): string {
return this.settings.searchPrefix ? this.settings.searchPrefix : '';
}

View File

@@ -41,6 +41,9 @@ export class SearchWidgetContainerComponent implements OnInit, OnDestroy {
@Input()
config: any;
@Input()
value: any;
componentRef: ComponentRef<any>;
constructor(
@@ -66,6 +69,10 @@ export class SearchWidgetContainerComponent implements OnInit, OnDestroy {
ref.instance.id = this.id;
ref.instance.settings = { ...this.settings };
ref.instance.context = this.queryBuilder;
if (this.value) {
ref.instance.isActive = true;
ref.instance.startValue = this.value;
}
}
}
@@ -80,10 +87,19 @@ export class SearchWidgetContainerComponent implements OnInit, OnDestroy {
this.componentRef.instance.submitValues();
}
setValue(currentValue: string | Object) {
this.componentRef.instance.isActive = true;
this.componentRef.instance.setValue(currentValue);
}
hasValueSelected() {
return this.componentRef.instance.hasValidValue();
}
getCurrentValue() {
return this.componentRef.instance.getCurrentValue();
}
resetInnerWidget() {
if (this.componentRef && this.componentRef.instance) {
this.componentRef.instance.reset();

View File

@@ -136,39 +136,6 @@ describe('SearchHeaderQueryBuilder', () => {
);
});
it('should replace the new query filter for the old parent node with the new one', () => {
const expectedResult = [
{ query: 'PARENT:"workspace://SpacesStore/fake-next-node-id"' }
];
const config: SearchConfiguration = {
categories: [
<any> { id: 'cat1', enabled: true },
<any> { id: 'cat2', enabled: true }
],
filterQueries: [
{ query: 'PARENT:"workspace://SpacesStore/fake-node-id' }
]
};
const searchHeaderService = new SearchHeaderQueryBuilderService(
buildConfig(config),
null,
null
);
searchHeaderService.currentParentFolderId = 'fake-node-id';
searchHeaderService.setCurrentRootFolderId(
'fake-next-node-id'
);
expect(searchHeaderService.filterQueries).toEqual(
expectedResult,
'Filters are not as expected'
);
});
it('should not add duplicate column names in activeFilters', () => {
const activeFilter = 'FakeColumn';
@@ -187,11 +154,11 @@ describe('SearchHeaderQueryBuilder', () => {
null
);
expect(searchHeaderService.activeFilters.length).toBe(0);
expect(searchHeaderService.activeFilters.size).toBe(0);
searchHeaderService.setActiveFilter(activeFilter);
searchHeaderService.setActiveFilter(activeFilter);
searchHeaderService.setActiveFilter(activeFilter, 'fake-value');
searchHeaderService.setActiveFilter(activeFilter, 'fake-value');
expect(searchHeaderService.activeFilters.length).toBe(1);
expect(searchHeaderService.activeFilters.size).toBe(1);
});
});

View File

@@ -20,7 +20,9 @@ import { AlfrescoApiService, AppConfigService, NodesApiService } from '@alfresco
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';
import { MinimalNode, QueryBody } from '@alfresco/js-api';
import { filter } from 'rxjs/operators';
import { Observable } from 'rxjs';
@Injectable({
providedIn: 'root'
@@ -29,11 +31,15 @@ export class SearchHeaderQueryBuilderService extends BaseQueryBuilderService {
private customSources = ['-trashcan-', '-sharedlinks-', '-sites-', '-mysites-', '-favorites-', '-recent-', '-my-'];
activeFilters: string[] = [];
currentParentFolderId: string;
activeFilters: Map<string, string> = new Map();
constructor(appConfig: AppConfigService, alfrescoApiService: AlfrescoApiService, private nodeApiService: NodesApiService) {
super(appConfig, alfrescoApiService);
this.updated.pipe(
filter((query: QueryBody) => !!query)).subscribe(() => {
this.execute();
});
}
public isFilterServiceActive(): boolean {
@@ -53,18 +59,22 @@ export class SearchHeaderQueryBuilderService extends BaseQueryBuilderService {
}
}
setActiveFilter(columnActivated: string) {
if (!this.activeFilters.includes(columnActivated)) {
this.activeFilters.push(columnActivated);
}
setActiveFilter(columnActivated: string, filterValue: string) {
this.activeFilters.set(columnActivated, filterValue);
}
getActiveFilters(): Map<string, string> {
return this.activeFilters;
}
isNoFilterActive(): boolean {
return this.activeFilters.length === 0;
return this.activeFilters.size === 0;
}
removeActiveFilter(columnRemoved: string) {
this.activeFilters = this.activeFilters.filter((column) => column !== columnRemoved);
if (this.activeFilters.get(columnRemoved) !== null) {
this.activeFilters.delete(columnRemoved);
}
}
getCategoryForColumn(columnKey: string): SearchCategory {
@@ -78,19 +88,6 @@ export class SearchHeaderQueryBuilderService extends BaseQueryBuilderService {
}
setCurrentRootFolderId(currentFolderId: string) {
if (currentFolderId !== this.currentParentFolderId) {
if (this.customSources.includes(currentFolderId)) {
this.nodeApiService.getNode(currentFolderId).subscribe((nodeEntity: MinimalNode) => {
this.updateCurrentParentFilter(nodeEntity.id);
});
} else {
this.currentParentFolderId = currentFolderId;
this.updateCurrentParentFilter(currentFolderId);
}
}
}
private updateCurrentParentFilter(currentFolderId: string) {
const alreadyAddedFilter = this.filterQueries.find(filterQueries =>
filterQueries.query.includes(currentFolderId)
);
@@ -104,4 +101,12 @@ export class SearchHeaderQueryBuilderService extends BaseQueryBuilderService {
}];
}
isCustomSourceNode(currentNodeId: string): boolean {
return this.customSources.includes(currentNodeId);
}
getNodeIdForCustomSource(customSourceId: string): Observable<MinimalNode> {
return this.nodeApiService.getNode(customSourceId);
}
}

View File

@@ -23,7 +23,10 @@ export interface SearchWidget {
settings?: SearchWidgetSettings;
context?: SearchQueryBuilderService;
isActive?: boolean;
startValue: any;
reset();
submitValues();
hasValidValue();
getCurrentValue();
setValue(value: any);
}