add filter service and utils functions

This commit is contained in:
Dan Percic 2021-08-04 00:29:23 +03:00
parent 1633911e13
commit 3625e29516
8 changed files with 206 additions and 5 deletions

View File

@ -5,4 +5,8 @@ export * from './lib/base/auto-unsubscribe.component';
export * from './lib/utils/decorators/required.decorator';
export * from './lib/buttons/circle-button/circle-button.component';
export * from './lib/buttons/circle-button/circle-button.type';
export * from './lib/types/tooltip-positions.type';
export * from './lib/utils/types/tooltip-positions.type';
export * from './lib/filtering/filter.service';
export * from './lib/filtering/filter-utils';
export * from './lib/filtering/models/filter-group.model';
export * from './lib/filtering/models/filter.model';

View File

@ -2,7 +2,7 @@ import { ChangeDetectionStrategy, Component, ElementRef, EventEmitter, Input, On
import { MatTooltip } from '@angular/material/tooltip';
import { CircleButtonType, CircleButtonTypes } from './circle-button.type';
import { Required } from '../../utils/decorators/required.decorator';
import { TooltipPositionsType, TooltipPositionsTypes } from '../../types/tooltip-positions.type';
import { TooltipPositionsType, TooltipPositionsTypes } from '../../utils/types/tooltip-positions.type';
@Component({
selector: 'iqser-circle-button',
@ -24,7 +24,7 @@ export class CircleButtonComponent implements OnInit {
@Input() isSubmit = false;
@Input() size = 34;
@Input() iconSize = 14;
@Output() action = new EventEmitter<any>();
@Output() action = new EventEmitter<MouseEvent>();
@ViewChild(MatTooltip) private readonly _matTooltip!: MatTooltip;
@ -35,7 +35,7 @@ export class CircleButtonComponent implements OnInit {
this._elementRef.nativeElement.style.setProperty('--iconSize', this.iconSize + 'px');
}
performAction($event: any) {
performAction($event: MouseEvent) {
if (this.disabled) return;
if (this.removeTooltip) {

View File

@ -16,5 +16,5 @@ export class IconButtonComponent {
@Input() showDot = false;
@Input() disabled = false;
@Input() type: IconButtonType = IconButtonTypes.default;
@Output() action = new EventEmitter<any>();
@Output() action = new EventEmitter<MouseEvent>();
}

View File

@ -0,0 +1,82 @@
import { FilterModel } from './models/filter.model';
import { FilterGroup } from './models/filter-group.model';
export function processFilters(oldFilters: FilterModel[], newFilters: FilterModel[]) {
copySettings(oldFilters, newFilters);
if (newFilters) {
newFilters.forEach(filter => {
handleCheckedValue(filter);
});
}
return newFilters;
}
function copySettings(oldFilters: FilterModel[], newFilters: FilterModel[]) {
if (oldFilters && newFilters) {
for (const oldFilter of oldFilters) {
const newFilter = newFilters.find(f => f.key === oldFilter.key);
if (newFilter) {
newFilter.checked = oldFilter.checked;
newFilter.indeterminate = oldFilter.indeterminate;
if (oldFilter.filters && newFilter.filters) copySettings(oldFilter.filters, newFilter.filters);
}
}
}
}
export function handleCheckedValue(filter: FilterModel) {
if (filter.filters && filter.filters.length) {
filter.checked = filter.filters.reduce<boolean>((acc, next) => acc && !!next.checked, true);
if (filter.checked) {
filter.indeterminate = false;
} else {
filter.indeterminate = filter.filters.reduce<boolean>((acc, next) => acc || !!next.checked, false);
}
} else {
filter.indeterminate = false;
}
}
export function checkFilter(entity: any, filters: FilterModel[], validate: Function, validateArgs: any = [], matchAll: boolean = false) {
const hasChecked = filters.find(f => f.checked);
if (validateArgs) {
if (!Array.isArray(validateArgs)) {
validateArgs = [validateArgs];
}
} else {
validateArgs = [];
}
if (!hasChecked) {
return true;
}
let filterMatched = matchAll;
for (const filter of filters) {
if (filter.checked) {
if (matchAll) {
filterMatched = filterMatched && validate(entity, filter, ...validateArgs);
} else {
filterMatched = filterMatched || validate(entity, filter, ...validateArgs);
}
}
}
return filterMatched;
}
export const keyChecker = (key: string) => (entity: any, filter: FilterModel) => entity[key] === filter.key;
export function getFilteredEntities<T>(entities: T[], filters: FilterGroup[]) {
const filteredEntities: T[] = [];
for (const entity of entities) {
let add = true;
for (const filter of filters) {
add = add && checkFilter(entity, filter.filters, filter.checker, filter.checkerArgs, filter.matchAll);
}
if (add) {
filteredEntities.push(entity);
}
}
return filteredEntities;
}

View File

@ -0,0 +1,88 @@
import { Injectable } from '@angular/core';
import { processFilters } from './filter-utils';
import { BehaviorSubject, Observable, Subject } from 'rxjs';
import { distinctUntilChanged, map, startWith, switchMap } from 'rxjs/operators';
import { FilterGroup } from './models/filter-group.model';
import { FilterModel } from './models/filter.model';
@Injectable()
export class FilterService {
private readonly _filterGroups$ = new BehaviorSubject<FilterGroup[]>([]);
private readonly _refresh$ = new Subject();
readonly filterGroups$ = this._refresh$.pipe(
startWith(''),
switchMap(() => this._filterGroups$.asObservable())
);
readonly showResetFilters$ = this._showResetFilters$;
get filterGroups(): FilterGroup[] {
return Object.values(this._filterGroups$.getValue());
}
private get _showResetFilters$(): Observable<boolean> {
return this.filterGroups$.pipe(
map(all => this._toFlatFilters(all)),
map(f => !!f.find(el => el.checked)),
distinctUntilChanged()
);
}
refresh(): void {
this._refresh$.next();
}
toggleFilter(filterGroupSlug: string, key: string): void {
const filters = this.filterGroups.find(group => group.slug === filterGroupSlug)?.filters;
if (!filters) return console.error(`Cannot find filter group "${filterGroupSlug}"`);
let found = filters.find(f => f.key === key);
if (!found) found = filters.map(f => f.filters?.find(ff => ff.key === key))[0];
if (!found) return console.error(`Cannot find filter with key "${key}" in group "${filterGroupSlug}"`);
found.checked = !found.checked;
this.refresh();
}
addFilterGroup(value: FilterGroup): void {
const oldFilters = this.getFilterGroup(value.slug)?.filters;
if (!oldFilters) return this._filterGroups$.next([...this.filterGroups, value]);
value.filters = processFilters(oldFilters, value.filters);
this._filterGroups$.next([...this.filterGroups.filter(f => f.slug !== value.slug), value]);
}
getFilterGroup(slug: string): FilterGroup | undefined {
return this.filterGroups.find(group => group.slug === slug);
}
getFilterModels$(filterGroupSlug: string): Observable<FilterModel[] | undefined> {
return this.getFilterGroup$(filterGroupSlug).pipe(map(f => f?.filters));
}
getFilterGroup$(slug: string): Observable<FilterGroup | undefined> {
return this.filterGroups$.pipe(map(all => all.find(f => f.slug === slug)));
}
reset(): void {
this.filterGroups.forEach(item => {
item.filters.forEach(child => {
child.checked = false;
child.indeterminate = false;
child.filters?.forEach(f => {
f.checked = false;
f.indeterminate = false;
});
});
});
this.refresh();
}
private _toFlatFilters(entities: FilterGroup[]): FilterModel[] {
const flatChildren = (filters: FilterModel[]) =>
(filters ?? []).reduce((acc: FilterModel[], f) => [...acc, ...(f?.filters ?? [])], []);
return entities.reduce((acc: FilterModel[], f) => [...acc, ...f.filters, ...flatChildren(f.filters)], []);
}
}

View File

@ -0,0 +1,14 @@
import { FilterModel } from './filter.model';
import { TemplateRef } from '@angular/core';
export interface FilterGroup {
filters: FilterModel[];
readonly slug: string;
readonly label?: string;
readonly icon?: string;
readonly filterTemplate?: TemplateRef<any>;
readonly hide?: boolean;
readonly checker: Function;
readonly matchAll?: boolean;
readonly checkerArgs?: any;
}

View File

@ -0,0 +1,13 @@
export interface FilterModel {
readonly key: string;
checked?: boolean;
indeterminate?: boolean;
expanded?: boolean;
matches?: number;
readonly label?: string;
readonly icon?: string;
readonly topLevelFilter?: boolean;
readonly filters?: FilterModel[];
readonly checker?: (obj?: unknown) => boolean;
readonly required?: boolean;
}