From 9e0c31992a8185a90b161e7030272e48451761e7 Mon Sep 17 00:00:00 2001 From: George Date: Mon, 10 Jul 2023 15:30:56 +0300 Subject: [PATCH] RED-6408, rewrite filters service with signals. TODO: Modify its usage everywhere in the codebase. --- .../filter-card/filter-card.component.ts | 94 +++++------- src/lib/filtering/filter.service.ts | 144 +++++------------- .../quick-filters.component.html | 2 +- .../quick-filters/quick-filters.component.ts | 4 +- .../page-header/page-header.component.html | 8 +- .../page-header/page-header.component.ts | 31 ++-- .../table-header/table-header.component.html | 4 +- .../table-header/table-header.component.ts | 12 +- .../progress-bar/progress-bar.component.html | 2 +- .../progress-bar/progress-bar.component.ts | 23 +-- 10 files changed, 113 insertions(+), 211 deletions(-) diff --git a/src/lib/filtering/filter-card/filter-card.component.ts b/src/lib/filtering/filter-card/filter-card.component.ts index 9b97328..fe03df9 100644 --- a/src/lib/filtering/filter-card/filter-card.component.ts +++ b/src/lib/filtering/filter-card/filter-card.component.ts @@ -1,22 +1,19 @@ -import { ChangeDetectionStrategy, Component, ElementRef, Input, OnInit, TemplateRef } from '@angular/core'; -import { combineLatest, Observable, pipe } from 'rxjs'; +import { ChangeDetectionStrategy, Component, computed, ElementRef, inject, Input, OnInit, TemplateRef } from '@angular/core'; +import { Observable, pipe } from 'rxjs'; import { IFilter } from '../models/filter.model'; import { INestedFilter } from '../models/nested-filter.model'; import { IFilterGroup } from '../models/filter-group.model'; import { extractFilterValues, handleCheckedValue } from '../filter-utils'; import { FilterService } from '../filter.service'; import { SearchService } from '../../search'; -import { Filter } from '../models/filter'; import { map } from 'rxjs/operators'; -import { shareDistinctLast, shareLast } from '../../utils'; +import { shareDistinctLast } from '../../utils'; import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; import { MAT_CHECKBOX_DEFAULT_OPTIONS } from '@angular/material/checkbox'; +import { toSignal } from '@angular/core/rxjs-interop'; const areExpandable = (nestedFilter: INestedFilter) => !!nestedFilter?.children?.length; -const atLeastOneIsExpandable = pipe( - map(group => !!group?.filters.some(areExpandable)), - shareDistinctLast(), -); +const atLeastOneIsExpandable = (group: IFilterGroup | undefined): boolean => !!group?.filters.some(areExpandable); export interface LocalStorageFilter { id: string; @@ -29,6 +26,17 @@ export interface LocalStorageFilters { secondaryFilters: LocalStorageFilter[] | null; } +const setFilters = (fGs: IFilterGroup[], slug: string, checked: boolean, exceptedFilterId?: string) => { + const filters = fGs.find(fg => fg.slug === slug)?.filters; + filters?.forEach(f => { + if (f.id !== exceptedFilterId) { + f.checked = checked; + f.indeterminate = false; + f.children?.forEach(ff => (ff.checked = checked)); + } + }); +}; + @Component({ selector: 'iqser-filter-card [primaryFiltersSlug]', templateUrl: './filter-card.component.html', @@ -53,35 +61,23 @@ export class FilterCardComponent implements OnInit { @Input() primaryFiltersLabel: string = _('filter-menu.filter-types'); @Input() minWidth = 350; - primaryFilterGroup$!: Observable; - secondaryFilterGroup$!: Observable; - primaryFilters$!: Observable; + readonly #filterService = inject(FilterService); + readonly #searchService = inject(SearchService); + readonly #elementRef = inject(ElementRef); - atLeastOneFilterIsExpandable$?: Observable; - atLeastOneSecondaryFilterIsExpandable$?: Observable; + readonly #searchValueChanged = toSignal(this.#searchService.valueChanges$); + readonly primaryFilterGroup = computed(() => this.#filterService.getGroup(this.primaryFiltersSlug)); + readonly secondaryFilterGroup = computed(() => this.#filterService.getGroup(this.secondaryFiltersSlug)); + primaryFilters = computed(() => { + this.#searchValueChanged(); + return this.#searchService.searchIn(this.primaryFilterGroup()?.filters ?? []); + }); - constructor( - readonly filterService: FilterService, - readonly searchService: SearchService, - private readonly _elementRef: ElementRef, - ) {} - - private get _primaryFilters$(): Observable { - return combineLatest([this.primaryFilterGroup$, this.searchService.valueChanges$]).pipe( - map(([group]) => this.searchService.searchIn(group?.filters ?? [])), - shareLast(), - ); - } + atLeastOneFilterIsExpandable = computed(() => atLeastOneIsExpandable(this.primaryFilterGroup())); + atLeastOneSecondaryFilterIsExpandable = computed(() => atLeastOneIsExpandable(this.secondaryFilterGroup())); ngOnInit() { - this.primaryFilterGroup$ = this.filterService.getGroup$(this.primaryFiltersSlug).pipe(shareLast()); - this.secondaryFilterGroup$ = this.filterService.getGroup$(this.secondaryFiltersSlug).pipe(shareLast()); - this.primaryFilters$ = this._primaryFilters$; - - this.atLeastOneFilterIsExpandable$ = atLeastOneIsExpandable(this.primaryFilterGroup$); - this.atLeastOneSecondaryFilterIsExpandable$ = atLeastOneIsExpandable(this.secondaryFilterGroup$); - - (this._elementRef.nativeElement as HTMLElement).style.setProperty('--filter-card-min-width', `${this.minWidth}px`); + (this.#elementRef.nativeElement as HTMLElement).style.setProperty('--filter-card-min-width', `${this.minWidth}px`); } filterCheckboxClicked(nestedFilter: INestedFilter, filterGroup: IFilterGroup, parent?: INestedFilter): void { @@ -105,46 +101,28 @@ export class FilterCardComponent implements OnInit { nestedFilter.children?.forEach(f => (f.checked = !!nestedFilter.checked)); } - this.filterService.refresh(); this.#updateFiltersInLocalStorage(); } activatePrimaryFilters(): void { - this._setFilters(this.primaryFiltersSlug, true); + this.#setFilters(this.primaryFiltersSlug, true); } deactivateFilters(exceptedFilterId?: string): void { - this._setFilters(this.primaryFiltersSlug, false, exceptedFilterId); + this.#setFilters(this.primaryFiltersSlug, false, exceptedFilterId); if (this.secondaryFiltersSlug) { - this._setFilters(this.secondaryFiltersSlug, false, exceptedFilterId); + this.#setFilters(this.secondaryFiltersSlug, false, exceptedFilterId); } } toggleFilterExpanded(nestedFilter: INestedFilter): void { - // eslint-disable-next-line no-param-reassign nestedFilter.expanded = !nestedFilter.expanded; - this.filterService.refresh(); - } - - private _setFilters(filterGroup: string, checked = false, exceptedFilterId?: string) { - const filters = this.filterService.getGroup(filterGroup)?.filters; - filters?.forEach(f => { - if (f.id !== exceptedFilterId) { - // eslint-disable-next-line no-param-reassign - f.checked = checked; - // eslint-disable-next-line no-param-reassign - f.indeterminate = false; - // eslint-disable-next-line no-return-assign,no-param-reassign - f.children?.forEach(ff => (ff.checked = checked)); - } - }); - this.filterService.refresh(); } #updateFiltersInLocalStorage(): void { if (this.fileId) { - const primaryFilters = this.filterService.getGroup('primaryFilters'); - const secondaryFilters = this.filterService.getGroup('secondaryFilters'); + const primaryFilters = this.#filterService.getGroup('primaryFilters'); + const secondaryFilters = this.#filterService.getGroup('secondaryFilters'); const filters: LocalStorageFilters = { primaryFilters: extractFilterValues(primaryFilters?.filters), @@ -157,4 +135,8 @@ export class FilterCardComponent implements OnInit { localStorage.setItem('workload-filters', JSON.stringify(workloadFilters)); } } + + #setFilters(slug: string, checked = false, exceptedFilterId?: string) { + this.#filterService.mutateFilterGroup(setFilters, [slug, checked, exceptedFilterId]); + } } diff --git a/src/lib/filtering/filter.service.ts b/src/lib/filtering/filter.service.ts index f5c4f80..9258dfd 100644 --- a/src/lib/filtering/filter.service.ts +++ b/src/lib/filtering/filter.service.ts @@ -1,57 +1,22 @@ -import { Injectable } from '@angular/core'; -import { BehaviorSubject, Observable, Subject } from 'rxjs'; -import { map, startWith, switchMap } from 'rxjs/operators'; +import { computed, Injectable, signal } from '@angular/core'; import { processFilters, toFlatFilters } from './filter-utils'; import { IFilterGroup } from './models/filter-group.model'; import { INestedFilter } from './models/nested-filter.model'; -import { get, shareDistinctLast, shareLast, some } from '../utils'; import { NestedFilter } from './models/nested-filter'; import { Filter } from './models/filter'; import { IFilter } from './models/filter.model'; @Injectable() export class FilterService { - readonly #singleFilters = new Map>(); - readonly #filterGroups$ = new BehaviorSubject([]); - readonly #refresh$ = new Subject(); - readonly showResetFilters$: Observable; - readonly filterGroups$: Observable; - - constructor() { - this.filterGroups$ = this.#refresh$.pipe( - startWith(''), - switchMap(() => this.#filterGroups$.asObservable()), - shareLast(), - ); - this.showResetFilters$ = this._showResetFilters$; - } - - get filterGroups(): IFilterGroup[] { - return Object.values(this.#filterGroups$.getValue()); - } - - get enabledFlatFilters() { - return toFlatFilters(this.filterGroups, filters => filters.filter(f => f.checked)); - } - - get singleFilters() { - return Array.from(this.#singleFilters.values()); - } - - private get _showResetFilters$(): Observable { - return this.filterGroups$.pipe( - map(value => toFlatFilters(value)), - some(filter => !!filter.checked), - shareDistinctLast(), - ); - } - - refresh(): void { - this.#refresh$.next(true); - } + readonly #singleFilters = signal({} as Record); + readonly #filterGroups = signal([] as IFilterGroup[]); + readonly filterGroups = this.#filterGroups.asReadonly(); + readonly singleFilters = computed(() => Object.values(this.#singleFilters)); + readonly enabledFlatFilters = computed(() => toFlatFilters(this.filterGroups(), filters => filters.filter(f => f.checked))); + readonly showResetFilters = computed(() => !!this.enabledFlatFilters().length); toggleFilter(filterGroupSlug: string, key: string, checkChildren = false): void { - const filters = this.filterGroups.find(group => group.slug === filterGroupSlug)?.filters; + const filters = this.filterGroups().find(group => group.slug === filterGroupSlug)?.filters; if (!filters) { return console.error(`Cannot find filter group "${filterGroupSlug}"`); } @@ -71,18 +36,16 @@ export class FilterService { found.children.forEach(c => (c.checked = true)); } } - - this.refresh(); } addFilterGroup(value: IFilterGroup): void { const oldFilters = this.getGroup(value.slug)?.filters; if (!oldFilters) { - return this.#filterGroups$.next([...this.filterGroups, value]); + return this.#filterGroups.update(fGs => [...fGs, value]); } const newGroup = { ...value, filters: processFilters(oldFilters, value.filters) }; - this.#filterGroups$.next([...this.filterGroups.filter(f => f.slug !== newGroup.slug), newGroup]); + this.#filterGroups.update(fG => [...fG.filter(f => f.slug !== newGroup.slug), newGroup]); } addFilterGroups(values: IFilterGroup[], removeOldFilters = false): void { @@ -96,54 +59,26 @@ export class FilterService { }); const filterSlugs = newFilters.map(f => f.slug); - this.#filterGroups$.next( - removeOldFilters ? newFilters : [...this.filterGroups.filter(f => !filterSlugs.includes(f.slug)), ...newFilters], + this.#filterGroups.update(fG => + removeOldFilters ? newFilters : [...fG.filter(f => !filterSlugs.includes(f.slug)), ...newFilters], ); } - updateFilterGroups(newFilters: IFilterGroup[]): void { - const filters = this.filterGroups.map(oldFilter => { - const newFilter = newFilters.find(f => f.slug === oldFilter.slug); - return newFilter ? newFilter : oldFilter; - }); - - this.replaceOldFilters(filters); - } - - replaceOldFilters(filterGroups: IFilterGroup[]) { - this.#filterGroups$.next(filterGroups); - } - addSingleFilter(filter: IFilter) { - if (!this.#singleFilters.has(filter.id)) { - return this.#singleFilters.set(filter.id, new BehaviorSubject(filter)); - } - - return this.#singleFilters.get(filter.id)?.next(filter); + this.#singleFilters.update(sF => ({ ...sF, [filter.id]: filter })); + return filter; } getSingleFilter(filterId: string) { - if (!this.#singleFilters.has(filterId)) { - this.#singleFilters.set(filterId, new BehaviorSubject(undefined)); - } - - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - return this.#singleFilters.get(filterId)!.asObservable(); + return this.#singleFilters()[filterId]; } getGroup(slug: string): IFilterGroup | undefined { - return this.filterGroups.find(group => group.slug === slug); + return this.filterGroups().find(group => group.slug === slug); } - getFilterModels$(filterGroupSlug: string): Observable { - return this.getGroup$(filterGroupSlug).pipe(map(f => f?.filters)); - } - - getGroup$(slug: string): Observable { - return this.filterGroups$.pipe( - get(group => group.slug === slug), - shareLast(), - ); + getFilterModels(filterGroupSlug: string): INestedFilter[] | undefined { + return this.getGroup(filterGroupSlug)?.filters; } filtersEnabled(filterGroupSlug: string): boolean { @@ -157,43 +92,46 @@ export class FilterService { } reset(): void { - this.filterGroups.forEach(group => { - group.filters.forEach(filter => { - // eslint-disable-next-line no-param-reassign - filter.checked = false; - // eslint-disable-next-line no-param-reassign - filter.indeterminate = false; - filter.children?.forEach(f => { - // eslint-disable-next-line no-param-reassign - f.checked = false; + this.#filterGroups.mutate(fGs => { + fGs.forEach(group => { + group.filters.forEach(filter => { + filter.checked = false; + filter.indeterminate = false; + filter.children?.forEach(f => { + f.checked = false; + }); }); }); }); this.resetSingleFilters(); - - this.refresh(); } resetSingleFilters() { - this.#singleFilters.forEach(filter$ => { - const filter = filter$.value; - if (filter) { - filter$.next({ ...filter$.value, checked: !filter$.value?.checked }); - } - }); + this.#singleFilters.update(filters => + Object.entries(filters).reduce( + (acc, [key, value]) => ({ ...acc, [key]: value ? { ...value, checked: !value?.checked } : undefined }), + {}, + ), + ); } toggleSingleFilter(filterId: string) { - const filter$ = this.#singleFilters.get(filterId); - if (!filter$) { + const filter = this.#singleFilters()[filterId]; + if (!filter) { return; } - const filter = filter$.value; if (filter) { filter.checked = !filter.checked; this.addSingleFilter(filter); } } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + mutateFilterGroup void>(predicate: T, ...args: unknown[]) { + this.#filterGroups.mutate(fGs => { + predicate(fGs, ...args); + }); + } } diff --git a/src/lib/filtering/quick-filters/quick-filters.component.html b/src/lib/filtering/quick-filters/quick-filters.component.html index 0352a69..2c99be5 100644 --- a/src/lib/filtering/quick-filters/quick-filters.component.html +++ b/src/lib/filtering/quick-filters/quick-filters.component.html @@ -1,4 +1,4 @@ - +
this.filterService.getFilterModels('quickFilters')); constructor(readonly filterService: FilterService) {} } diff --git a/src/lib/listing/page-header/page-header.component.html b/src/lib/listing/page-header/page-header.component.html index 4952155..7167461 100644 --- a/src/lib/listing/page-header/page-header.component.html +++ b/src/lib/listing/page-header/page-header.component.html @@ -1,7 +1,7 @@