From 8d001e95b541b38d063f57e1a01737f5b3ff66a2 Mon Sep 17 00:00:00 2001 From: Dan Percic Date: Sat, 25 Sep 2021 20:58:55 +0300 Subject: [PATCH] move filters to classes, remove _primaryKey from listing component --- src/lib/filtering/filter-utils.ts | 28 +++++++++---------- src/lib/filtering/filter.service.ts | 22 +++++++-------- src/lib/filtering/index.ts | 2 ++ .../filtering/models/filter-group.model.ts | 6 ++-- src/lib/filtering/models/filter.model.ts | 5 ++-- src/lib/filtering/models/filter.ts | 27 ++++++++++++++++++ .../filtering/models/nested-filter.model.ts | 6 ++-- src/lib/filtering/models/nested-filter.ts | 15 ++++++++++ .../popup-filter/popup-filter.component.ts | 28 +++++++++---------- .../quick-filters.component.html | 2 +- .../listing/listing-component.directive.ts | 18 ++---------- .../table-column-name.component.ts | 5 ++-- src/lib/listing/table/table.component.html | 6 ++-- src/lib/search/search.service.ts | 19 ++++--------- src/lib/sorting/sorting.service.ts | 8 ++++-- 15 files changed, 111 insertions(+), 86 deletions(-) create mode 100644 src/lib/filtering/models/filter.ts create mode 100644 src/lib/filtering/models/nested-filter.ts diff --git a/src/lib/filtering/filter-utils.ts b/src/lib/filtering/filter-utils.ts index 1226de9..aebf5f9 100644 --- a/src/lib/filtering/filter-utils.ts +++ b/src/lib/filtering/filter-utils.ts @@ -1,15 +1,15 @@ /* eslint-disable no-param-reassign */ -import { NestedFilter } from './models/nested-filter.model'; -import { FilterGroup } from './models/filter-group.model'; -import { Filter } from './models/filter.model'; +import { INestedFilter } from "./models/nested-filter.model"; +import { IFilterGroup } from "./models/filter-group.model"; +import { IFilter } from "./models/filter.model"; -function copySettings(oldFilters: NestedFilter[], newFilters: NestedFilter[]) { +function copySettings(oldFilters: INestedFilter[], newFilters: INestedFilter[]) { if (!oldFilters || !newFilters) { return; } oldFilters.forEach(filter => { - const newFilter = newFilters.find(f => f.key === filter.key); + const newFilter = newFilters.find(f => f.id === filter.id); if (newFilter) { newFilter.checked = filter.checked; newFilter.indeterminate = filter.indeterminate; @@ -20,7 +20,7 @@ function copySettings(oldFilters: NestedFilter[], newFilters: NestedFilter[]) { }); } -export function handleCheckedValue(filter: NestedFilter): void { +export function handleCheckedValue(filter: INestedFilter): void { if (filter.children && filter.children.length) { filter.checked = filter.children.reduce((acc, next) => acc && !!next.checked, true); if (filter.checked) { @@ -33,7 +33,7 @@ export function handleCheckedValue(filter: NestedFilter): void { } } -export function processFilters(oldFilters: NestedFilter[], newFilters: NestedFilter[]): NestedFilter[] { +export function processFilters(oldFilters: INestedFilter[], newFilters: INestedFilter[]): INestedFilter[] { copySettings(oldFilters, newFilters); if (newFilters) { newFilters.forEach(filter => { @@ -45,7 +45,7 @@ export function processFilters(oldFilters: NestedFilter[], newFilters: NestedFil export function checkFilter( entity: unknown, - filters: NestedFilter[], + filters: INestedFilter[], validate?: (...args: unknown[]) => boolean, // eslint-disable-next-line @typescript-eslint/no-explicit-any validateArgs: any = [], @@ -72,9 +72,9 @@ export function checkFilter( } // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types -export const keyChecker = (key: string) => (entity: Record, filter: NestedFilter) => entity[key] === filter.key; +export const keyChecker = (key: string) => (entity: Record, filter: INestedFilter) => entity[key] === filter.id; -export function getFilteredEntities(entities: T[], filters: FilterGroup[]): T[] { +export function getFilteredEntities(entities: T[], filters: IFilterGroup[]): T[] { const filteredEntities: T[] = []; entities.forEach(entity => { let add = true; @@ -88,10 +88,10 @@ export function getFilteredEntities(entities: T[], filters: FilterGroup[]): T return filteredEntities; } -export function flatChildren(filters: NestedFilter[]): Filter[] { - return filters.reduce((acc: Filter[], f) => [...acc, ...(f?.children ?? [])], []); +export function flatChildren(filters: INestedFilter[]): IFilter[] { + return filters.reduce((acc: IFilter[], f) => [...acc, ...(f?.children ?? [])], []); } -export function toFlatFilters(groups: FilterGroup[]): Filter[] { - return groups.reduce((acc: Filter[], f) => [...acc, ...f.filters, ...flatChildren(f.filters)], []); +export function toFlatFilters(groups: IFilterGroup[]): IFilter[] { + return groups.reduce((acc: IFilter[], f) => [...acc, ...f.filters, ...flatChildren(f.filters)], []); } diff --git a/src/lib/filtering/filter.service.ts b/src/lib/filtering/filter.service.ts index 77ab66b..f9524b3 100644 --- a/src/lib/filtering/filter.service.ts +++ b/src/lib/filtering/filter.service.ts @@ -2,15 +2,15 @@ import { Injectable } from '@angular/core'; import { BehaviorSubject, Observable, Subject } from 'rxjs'; import { distinctUntilChanged, map, startWith, switchMap } from 'rxjs/operators'; import { processFilters, toFlatFilters } from './filter-utils'; -import { FilterGroup } from './models/filter-group.model'; -import { NestedFilter } from './models/nested-filter.model'; +import { IFilterGroup } from './models/filter-group.model'; +import { INestedFilter } from './models/nested-filter.model'; import { get } from '../utils'; @Injectable() export class FilterService { readonly showResetFilters$: Observable; - readonly filterGroups$: Observable; - private readonly _filterGroups$ = new BehaviorSubject([]); + readonly filterGroups$: Observable; + private readonly _filterGroups$ = new BehaviorSubject([]); private readonly _refresh$ = new Subject(); constructor() { @@ -22,7 +22,7 @@ export class FilterService { this.showResetFilters$ = this._showResetFilters$; } - get filterGroups(): FilterGroup[] { + get filterGroups(): IFilterGroup[] { return Object.values(this._filterGroups$.getValue()); } @@ -44,9 +44,9 @@ export class FilterService { return console.error(`Cannot find filter group "${filterGroupSlug}"`); } - let found = filters.find(f => f.key === key); + let found = filters.find(f => f.id === key); if (!found) { - [found] = filters.map(f => f.children?.find(ff => ff.key === key)); + [found] = filters.map(f => f.children?.find(ff => ff.id === key)); } if (!found) { return console.error(`Cannot find filter with key "${key}" in group "${filterGroupSlug}"`); @@ -59,7 +59,7 @@ export class FilterService { this.refresh(); } - addFilterGroup(value: FilterGroup): void { + addFilterGroup(value: IFilterGroup): void { const oldFilters = this.getGroup(value.slug)?.filters; if (!oldFilters) { return this._filterGroups$.next([...this.filterGroups, value]); @@ -69,15 +69,15 @@ export class FilterService { this._filterGroups$.next([...this.filterGroups.filter(f => f.slug !== newGroup.slug), newGroup]); } - getGroup(slug: string): FilterGroup | undefined { + getGroup(slug: string): IFilterGroup | undefined { return this.filterGroups.find(group => group.slug === slug); } - getFilterModels$(filterGroupSlug: string): Observable { + getFilterModels$(filterGroupSlug: string): Observable { return this.getGroup$(filterGroupSlug).pipe(map(f => f?.filters)); } - getGroup$(slug: string): Observable { + getGroup$(slug: string): Observable { return this.filterGroups$.pipe(get(group => group.slug === slug)); } diff --git a/src/lib/filtering/index.ts b/src/lib/filtering/index.ts index 0a01d7a..75e5952 100644 --- a/src/lib/filtering/index.ts +++ b/src/lib/filtering/index.ts @@ -3,8 +3,10 @@ export * from './filters.module'; export * from './filter-utils'; export * from './filter.service'; +export * from './models/filter'; export * from './models/filter.model'; export * from './models/filter-group.model'; +export * from './models/nested-filter'; export * from './models/nested-filter.model'; export * from './popup-filter/popup-filter.component'; diff --git a/src/lib/filtering/models/filter-group.model.ts b/src/lib/filtering/models/filter-group.model.ts index 8deb35a..8bd5571 100644 --- a/src/lib/filtering/models/filter-group.model.ts +++ b/src/lib/filtering/models/filter-group.model.ts @@ -1,8 +1,8 @@ import { TemplateRef } from '@angular/core'; -import { NestedFilter } from './nested-filter.model'; +import { INestedFilter } from './nested-filter.model'; -export interface FilterGroup { - filters: NestedFilter[]; +export interface IFilterGroup { + filters: INestedFilter[]; readonly slug: string; readonly label?: string; readonly icon?: string; diff --git a/src/lib/filtering/models/filter.model.ts b/src/lib/filtering/models/filter.model.ts index a7a6dc6..8030b52 100644 --- a/src/lib/filtering/models/filter.model.ts +++ b/src/lib/filtering/models/filter.model.ts @@ -1,5 +1,6 @@ -export interface Filter { - readonly key: string; +import { IListable } from '../../listing'; + +export interface IFilter extends IListable { checked?: boolean; matches?: number; readonly label: string; diff --git a/src/lib/filtering/models/filter.ts b/src/lib/filtering/models/filter.ts new file mode 100644 index 0000000..e1e0909 --- /dev/null +++ b/src/lib/filtering/models/filter.ts @@ -0,0 +1,27 @@ +import { IFilter } from './filter.model'; + +export class Filter implements IFilter { + readonly id: string; + readonly label: string; + checked: boolean; + readonly required: boolean; + readonly topLevelFilter: boolean; + matches?: number; + readonly icon?: string; + readonly checker?: (obj?: unknown) => boolean; + + constructor(filter: IFilter) { + this.id = filter.id; + this.label = filter.label; + this.checked = !!filter.checked; + this.matches = filter.matches; + this.icon = filter.icon; + this.topLevelFilter = !!filter.topLevelFilter; + this.checker = filter.checker; + this.required = !!filter.required; + } + + get searchKey(): string { + return this.label; + } +} diff --git a/src/lib/filtering/models/nested-filter.model.ts b/src/lib/filtering/models/nested-filter.model.ts index d553c17..33168b3 100644 --- a/src/lib/filtering/models/nested-filter.model.ts +++ b/src/lib/filtering/models/nested-filter.model.ts @@ -1,7 +1,7 @@ -import { Filter } from './filter.model'; +import { IFilter } from './filter.model'; -export interface NestedFilter extends Filter { +export interface INestedFilter extends IFilter { expanded?: boolean; indeterminate?: boolean; - readonly children?: Filter[]; + readonly children?: IFilter[]; } diff --git a/src/lib/filtering/models/nested-filter.ts b/src/lib/filtering/models/nested-filter.ts new file mode 100644 index 0000000..409c3e5 --- /dev/null +++ b/src/lib/filtering/models/nested-filter.ts @@ -0,0 +1,15 @@ +import { Filter } from './filter'; +import { IFilter, INestedFilter } from '@iqser/common-ui'; + +export class NestedFilter extends Filter implements INestedFilter { + expanded: boolean; + indeterminate: boolean; + readonly children?: IFilter[]; + + constructor(nestedFilter: INestedFilter) { + super(nestedFilter); + this.expanded = !!nestedFilter.expanded; + this.indeterminate = !!nestedFilter.indeterminate; + this.children = nestedFilter.children; + } +} diff --git a/src/lib/filtering/popup-filter/popup-filter.component.ts b/src/lib/filtering/popup-filter/popup-filter.component.ts index 325c38c..9d6b440 100644 --- a/src/lib/filtering/popup-filter/popup-filter.component.ts +++ b/src/lib/filtering/popup-filter/popup-filter.component.ts @@ -5,14 +5,14 @@ import { delay, distinctUntilChanged, map, shareReplay } from 'rxjs/operators'; import { any } from '../../utils'; import { handleCheckedValue } from '../filter-utils'; import { FilterService } from '../filter.service'; -import { FilterGroup } from '../models/filter-group.model'; -import { NestedFilter } from '../models/nested-filter.model'; +import { IFilterGroup } from '../models/filter-group.model'; +import { INestedFilter } from '../models/nested-filter.model'; import { SearchService } from '../../search'; -import { Filter } from '..'; +import { IFilter } from '..'; -const areExpandable = (nestedFilter: NestedFilter) => !!nestedFilter?.children?.length; +const areExpandable = (nestedFilter: INestedFilter) => !!nestedFilter?.children?.length; const atLeastOneIsExpandable = pipe( - map(group => !!group?.filters.some(areExpandable)), + map(group => !!group?.filters.some(areExpandable)), distinctUntilChanged(), shareReplay() ); @@ -44,13 +44,11 @@ export class PopupFilterComponent implements OnInit { readonly expanded = new BehaviorSubject(false); readonly expanded$ = this.expanded.asObservable().pipe(delay(200)); - primaryFilterGroup$!: Observable; - secondaryFilterGroup$!: Observable; - primaryFilters$!: Observable; + primaryFilterGroup$!: Observable; + secondaryFilterGroup$!: Observable; + primaryFilters$!: Observable; - constructor(readonly filterService: FilterService, readonly searchService: SearchService) { - this.searchService.setSearchKey('label'); - } + constructor(readonly filterService: FilterService, readonly searchService: SearchService) {} private get _hasActiveFilters$() { return combineLatest([this.primaryFilterGroup$, this.secondaryFilterGroup$]).pipe( @@ -70,7 +68,7 @@ export class PopupFilterComponent implements OnInit { this.atLeastOneSecondaryFilterIsExpandable$ = atLeastOneIsExpandable(this.secondaryFilterGroup$); } - filterCheckboxClicked($event: MouseEvent, nestedFilter: NestedFilter, parent?: NestedFilter): void { + filterCheckboxClicked($event: MouseEvent, nestedFilter: INestedFilter, parent?: INestedFilter): void { $event.stopPropagation(); // eslint-disable-next-line no-param-reassign @@ -86,7 +84,7 @@ export class PopupFilterComponent implements OnInit { // eslint-disable-next-line no-param-reassign nestedFilter.indeterminate = false; // eslint-disable-next-line no-return-assign,no-param-reassign - nestedFilter.children?.forEach(f => (f.checked = nestedFilter.checked)); + nestedFilter.children?.forEach(f => (f.checked = !!nestedFilter.checked)); } this.filterService.refresh(); @@ -103,7 +101,7 @@ export class PopupFilterComponent implements OnInit { } } - toggleFilterExpanded($event: MouseEvent, nestedFilter: NestedFilter): void { + toggleFilterExpanded($event: MouseEvent, nestedFilter: INestedFilter): void { $event.stopPropagation(); // eslint-disable-next-line no-param-reassign nestedFilter.expanded = !nestedFilter.expanded; @@ -123,7 +121,7 @@ export class PopupFilterComponent implements OnInit { this.filterService.refresh(); } - private get _primaryFilters$(): Observable { + private get _primaryFilters$(): Observable { return combineLatest([this.primaryFilterGroup$, this.searchService.valueChanges$]).pipe( map(([group]) => this.searchService.searchIn(group?.filters ?? [])) ); diff --git a/src/lib/filtering/quick-filters/quick-filters.component.html b/src/lib/filtering/quick-filters/quick-filters.component.html index 533c9d1..37bcd4f 100644 --- a/src/lib/filtering/quick-filters/quick-filters.component.html +++ b/src/lib/filtering/quick-filters/quick-filters.component.html @@ -1,5 +1,5 @@
extends AutoUnsubscr protected constructor(protected readonly _injector: Injector) { super(); this.listingMode$ = this._listingMode$.asObservable(); - setTimeout(() => this.setInitialConfig()); } get allEntities(): T[] { @@ -71,14 +70,6 @@ export abstract class ListingComponent extends AutoUnsubscr ); } - setInitialConfig(): void { - this.sortingService.setSortingOption({ - column: this._primaryKey, - order: SortingOrders.asc - }); - this.searchService.setSearchKey(this._primaryKey); - } - toggleEntitySelected(event: MouseEvent, entity: T): void { event.stopPropagation(); this.entitiesService.select(entity); @@ -87,9 +78,4 @@ export abstract class ListingComponent extends AutoUnsubscr isSelected(entity: T): boolean { return this.entitiesService.isSelected(entity); } - - @Bind() - trackByPrimaryKey(index: number, item: T): unknown { - return item.searchKey ?? item[this._primaryKey]; - } } diff --git a/src/lib/listing/table-column-name/table-column-name.component.ts b/src/lib/listing/table-column-name/table-column-name.component.ts index 8977bf2..dc1e1c4 100644 --- a/src/lib/listing/table-column-name/table-column-name.component.ts +++ b/src/lib/listing/table-column-name/table-column-name.component.ts @@ -1,8 +1,9 @@ import { ChangeDetectionStrategy, Component, Input, Optional } from '@angular/core'; import { SortingOrders, SortingService } from '../../sorting'; import { KeysOf, Required } from '../../utils'; +import { IListable } from '../models'; -const ifHasRightIcon = (thisArg: TableColumnNameComponent) => !!thisArg.rightIcon; +const ifHasRightIcon = (thisArg: TableColumnNameComponent) => !!thisArg.rightIcon; @Component({ selector: 'iqser-table-column-name', @@ -10,7 +11,7 @@ const ifHasRightIcon = (thisArg: TableColumnNameComponent) => !!thisArg.ri styleUrls: ['./table-column-name.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush }) -export class TableColumnNameComponent { +export class TableColumnNameComponent { readonly sortingOrders = SortingOrders; @Input() @Required() label!: string; diff --git a/src/lib/listing/table/table.component.html b/src/lib/listing/table/table.component.html index a80f4d4..7f915fd 100644 --- a/src/lib/listing/table/table.component.html +++ b/src/lib/listing/table/table.component.html @@ -22,13 +22,11 @@ - +
diff --git a/src/lib/search/search.service.ts b/src/lib/search/search.service.ts index 9efdf19..8c006ae 100644 --- a/src/lib/search/search.service.ts +++ b/src/lib/search/search.service.ts @@ -1,12 +1,12 @@ import { Injectable } from '@angular/core'; import { BehaviorSubject } from 'rxjs'; import { KeysOf } from '../utils'; +import { IListable } from '../listing'; @Injectable() -export class SearchService { +export class SearchService { private readonly _query$ = new BehaviorSubject(''); readonly valueChanges$ = this._query$.asObservable(); - private _searchKey!: KeysOf; get searchValue(): string { return this._query$.getValue(); @@ -17,23 +17,16 @@ export class SearchService { } searchIn(entities: T[]): T[] { - if (!this._searchKey) { + const searchValue = this.searchValue.toLowerCase(); + if (!searchValue) { return entities; } - - const searchValue = this.searchValue.toLowerCase(); - return entities.filter(entity => this._searchField(entity).includes(searchValue)); + return entities.filter(entity => entity.searchKey?.includes(searchValue)); } - setSearchKey(value: KeysOf): void { - this._searchKey = value; - } + setSearchKey(value: KeysOf): void {} reset(): void { this._query$.next(''); } - - private _searchField(entity: T): string { - return ((entity[this._searchKey]) as string).toString().toLowerCase(); - } } diff --git a/src/lib/sorting/sorting.service.ts b/src/lib/sorting/sorting.service.ts index e2dd389..6d6870f 100644 --- a/src/lib/sorting/sorting.service.ts +++ b/src/lib/sorting/sorting.service.ts @@ -4,10 +4,14 @@ import { BehaviorSubject } from 'rxjs'; import { SortingOption } from './models/sorting-option.model'; import { SortingOrder, SortingOrders } from './models/sorting-order.type'; import { KeysOf } from '../utils'; +import { IListable } from '../listing'; @Injectable() -export class SortingService { - private readonly _sortingOption$ = new BehaviorSubject | undefined>(undefined); +export class SortingService { + private readonly _sortingOption$ = new BehaviorSubject>({ + column: 'searchKey', + order: SortingOrders.asc + }); readonly sortingOption$ = this._sortingOption$.asObservable(); get sortingOption(): SortingOption | undefined {