move filters to classes, remove _primaryKey from listing component

This commit is contained in:
Dan Percic 2021-09-25 20:58:55 +03:00
parent 93b5ea7f89
commit 8d001e95b5
15 changed files with 111 additions and 86 deletions

View File

@ -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<boolean>((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<string, string>, filter: NestedFilter) => entity[key] === filter.key;
export const keyChecker = (key: string) => (entity: Record<string, string>, filter: INestedFilter) => entity[key] === filter.id;
export function getFilteredEntities<T>(entities: T[], filters: FilterGroup[]): T[] {
export function getFilteredEntities<T>(entities: T[], filters: IFilterGroup[]): T[] {
const filteredEntities: T[] = [];
entities.forEach(entity => {
let add = true;
@ -88,10 +88,10 @@ export function getFilteredEntities<T>(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)], []);
}

View File

@ -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<boolean>;
readonly filterGroups$: Observable<FilterGroup[]>;
private readonly _filterGroups$ = new BehaviorSubject<FilterGroup[]>([]);
readonly filterGroups$: Observable<IFilterGroup[]>;
private readonly _filterGroups$ = new BehaviorSubject<IFilterGroup[]>([]);
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<NestedFilter[] | undefined> {
getFilterModels$(filterGroupSlug: string): Observable<INestedFilter[] | undefined> {
return this.getGroup$(filterGroupSlug).pipe(map(f => f?.filters));
}
getGroup$(slug: string): Observable<FilterGroup | undefined> {
getGroup$(slug: string): Observable<IFilterGroup | undefined> {
return this.filterGroups$.pipe(get(group => group.slug === slug));
}

View File

@ -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';

View File

@ -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;

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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[];
}

View File

@ -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;
}
}

View File

@ -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<FilterGroup | undefined, boolean>(group => !!group?.filters.some(areExpandable)),
map<IFilterGroup | undefined, boolean>(group => !!group?.filters.some(areExpandable)),
distinctUntilChanged(),
shareReplay()
);
@ -44,13 +44,11 @@ export class PopupFilterComponent implements OnInit {
readonly expanded = new BehaviorSubject<boolean>(false);
readonly expanded$ = this.expanded.asObservable().pipe(delay(200));
primaryFilterGroup$!: Observable<FilterGroup | undefined>;
secondaryFilterGroup$!: Observable<FilterGroup | undefined>;
primaryFilters$!: Observable<Filter[] | undefined>;
primaryFilterGroup$!: Observable<IFilterGroup | undefined>;
secondaryFilterGroup$!: Observable<IFilterGroup | undefined>;
primaryFilters$!: Observable<IFilter[] | undefined>;
constructor(readonly filterService: FilterService, readonly searchService: SearchService<Filter>) {
this.searchService.setSearchKey('label');
}
constructor(readonly filterService: FilterService, readonly searchService: SearchService<IFilter>) {}
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<Filter[]> {
private get _primaryFilters$(): Observable<IFilter[]> {
return combineLatest([this.primaryFilterGroup$, this.searchService.valueChanges$]).pipe(
map(([group]) => this.searchService.searchIn(group?.filters ?? []))
);

View File

@ -1,5 +1,5 @@
<div
(click)="filterService.toggleFilter('quickFilters', filter.key)"
(click)="filterService.toggleFilter('quickFilters', filter.id)"
*ngFor="let filter of quickFilters$ | async"
[class.active]="filter.checked"
class="quick-filter"

View File

@ -2,8 +2,8 @@ import { Directive, Injector, OnDestroy } from '@angular/core';
import { BehaviorSubject, combineLatest, Observable } from 'rxjs';
import { distinctUntilChanged, map, switchMap } from 'rxjs/operators';
import { FilterService } from '../filtering';
import { SortingOrders, SortingService } from '../sorting';
import { AutoUnsubscribe, Bind, KeysOf } from '../utils';
import { SortingService } from '../sorting';
import { AutoUnsubscribe, KeysOf } from '../utils';
import { SearchService } from '../search';
import { EntitiesService } from './services';
import { IListable, TableColumnConfig } from './models';
@ -40,7 +40,6 @@ export abstract class ListingComponent<T extends IListable> 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<T extends IListable> 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<T extends IListable> extends AutoUnsubscr
isSelected(entity: T): boolean {
return this.entitiesService.isSelected(entity);
}
@Bind()
trackByPrimaryKey(index: number, item: T): unknown {
return item.searchKey ?? item[this._primaryKey];
}
}

View File

@ -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 = <T>(thisArg: TableColumnNameComponent<T>) => !!thisArg.rightIcon;
const ifHasRightIcon = <T extends IListable>(thisArg: TableColumnNameComponent<T>) => !!thisArg.rightIcon;
@Component({
selector: 'iqser-table-column-name',
@ -10,7 +11,7 @@ const ifHasRightIcon = <T>(thisArg: TableColumnNameComponent<T>) => !!thisArg.ri
styleUrls: ['./table-column-name.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class TableColumnNameComponent<T> {
export class TableColumnNameComponent<T extends IListable> {
readonly sortingOrders = SortingOrders;
@Input() @Required() label!: string;

View File

@ -22,13 +22,11 @@
<iqser-empty-state *ngIf="listingComponent.noMatch$ | async" [text]="noMatchText"></iqser-empty-state>
<cdk-virtual-scroll-viewport [class.no-data]="listingComponent.noContent$ | async"
[itemSize]="itemSize"
iqserHasScrollbar>
<cdk-virtual-scroll-viewport [class.no-data]="listingComponent.noContent$ | async" [itemSize]="itemSize" iqserHasScrollbar>
<div
(mouseenter)="itemMouseEnterFn && itemMouseEnterFn(entity)"
(mouseleave)="itemMouseLeaveFn && itemMouseLeaveFn(entity)"
*cdkVirtualFor="let entity of listingComponent.sortedDisplayedEntities$ | async; trackBy: listingComponent.trackByPrimaryKey"
*cdkVirtualFor="let entity of listingComponent.sortedDisplayedEntities$ | async"
[ngClass]="getTableItemClasses(entity)"
[routerLink]="routerLinkFn && routerLinkFn(entity)"
>

View File

@ -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<T> {
export class SearchService<T extends IListable> {
private readonly _query$ = new BehaviorSubject('');
readonly valueChanges$ = this._query$.asObservable();
private _searchKey!: KeysOf<T>;
get searchValue(): string {
return this._query$.getValue();
@ -17,23 +17,16 @@ export class SearchService<T> {
}
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<T>): void {
this._searchKey = value;
}
setSearchKey(value: KeysOf<T>): void {}
reset(): void {
this._query$.next('');
}
private _searchField(entity: T): string {
return ((<unknown>entity[this._searchKey]) as string).toString().toLowerCase();
}
}

View File

@ -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<T> {
private readonly _sortingOption$ = new BehaviorSubject<SortingOption<T> | undefined>(undefined);
export class SortingService<T extends IListable> {
private readonly _sortingOption$ = new BehaviorSubject<SortingOption<T>>({
column: 'searchKey',
order: SortingOrders.asc
});
readonly sortingOption$ = this._sortingOption$.asObservable();
get sortingOption(): SortingOption<T> | undefined {