move filters to classes, remove _primaryKey from listing component
This commit is contained in:
parent
93b5ea7f89
commit
8d001e95b5
@ -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)], []);
|
||||
}
|
||||
|
||||
@ -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));
|
||||
}
|
||||
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
27
src/lib/filtering/models/filter.ts
Normal file
27
src/lib/filtering/models/filter.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -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[];
|
||||
}
|
||||
|
||||
15
src/lib/filtering/models/nested-filter.ts
Normal file
15
src/lib/filtering/models/nested-filter.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -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 ?? []))
|
||||
);
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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];
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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)"
|
||||
>
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user