Compare commits

...

2 Commits

14 changed files with 143 additions and 269 deletions

View File

@ -1,4 +1,4 @@
<ng-container *ngIf="primaryFilterGroup$ | async as primaryGroup"> <ng-container *ngIf="primaryFilterGroup() as primaryGroup">
<iqser-input-with-action <iqser-input-with-action
*ngIf="primaryGroup.filterceptionPlaceholder" *ngIf="primaryGroup.filterceptionPlaceholder"
[(value)]="searchService.searchValue" [(value)]="searchService.searchValue"
@ -9,19 +9,19 @@
<ng-container *ngTemplateOutlet="filterHeader"></ng-container> <ng-container *ngTemplateOutlet="filterHeader"></ng-container>
<div *ngIf="primaryFilters$ | async as filters" class="filter-content"> <div *ngIf="primaryFilters() as filters" class="filter-content">
<ng-container <ng-container
*ngFor="let filter of filters" *ngFor="let filter of filters"
[ngTemplateOutletContext]="{ [ngTemplateOutletContext]="{
filter: filter, filter: filter,
filterGroup: primaryGroup, filterGroup: primaryGroup,
atLeastOneIsExpandable: atLeastOneFilterIsExpandable$ | async atLeastOneIsExpandable: atLeastOneFilterIsExpandable()
}" }"
[ngTemplateOutlet]="defaultFilterTemplate" [ngTemplateOutlet]="defaultFilterTemplate"
></ng-container> ></ng-container>
</div> </div>
<div *ngIf="secondaryFilterGroup$ | async as secondaryGroup" class="filter-options"> <div *ngIf="secondaryFilterGroup() as secondaryGroup" class="filter-options">
<div class="filter-menu-options"> <div class="filter-menu-options">
<div class="all-caps-label" translate="filter-menu.filter-options"></div> <div class="all-caps-label" translate="filter-menu.filter-options"></div>
</div> </div>
@ -31,7 +31,7 @@
[ngTemplateOutletContext]="{ [ngTemplateOutletContext]="{
filter: filter, filter: filter,
filterGroup: secondaryGroup, filterGroup: secondaryGroup,
atLeastOneIsExpandable: atLeastOneSecondaryFilterIsExpandable$ | async atLeastOneIsExpandable: atLeastOneSecondaryFilterIsExpandable()
}" }"
[ngTemplateOutlet]="defaultFilterTemplate" [ngTemplateOutlet]="defaultFilterTemplate"
></ng-container> ></ng-container>
@ -44,7 +44,7 @@
<!--TODO: move to separate component--> <!--TODO: move to separate component-->
<ng-template #filterHeader> <ng-template #filterHeader>
<div *ngIf="primaryFilterGroup$ | async as primaryGroup" class="filter-menu-header"> <div *ngIf="primaryFilterGroup() as primaryGroup" class="filter-menu-header">
<div [translateParams]="{ count: primaryGroup.filters.length }" [translate]="primaryFiltersLabel" class="all-caps-label"></div> <div [translateParams]="{ count: primaryGroup.filters.length }" [translate]="primaryFiltersLabel" class="all-caps-label"></div>
<div class="actions"> <div class="actions">
<div <div

View File

@ -1,22 +1,15 @@
import { ChangeDetectionStrategy, Component, ElementRef, Input, OnInit, TemplateRef } from '@angular/core'; import { ChangeDetectionStrategy, Component, computed, ElementRef, inject, Input, OnInit, TemplateRef } from '@angular/core';
import { combineLatest, Observable, pipe } from 'rxjs';
import { IFilter } from '../models/filter.model';
import { INestedFilter } from '../models/nested-filter.model'; import { INestedFilter } from '../models/nested-filter.model';
import { IFilterGroup } from '../models/filter-group.model'; import { IFilterGroup } from '../models/filter-group.model';
import { extractFilterValues, handleCheckedValue } from '../filter-utils'; import { extractFilterValues, handleCheckedValue } from '../filter-utils';
import { FilterService } from '../filter.service'; import { FilterService } from '../filter.service';
import { SearchService } from '../../search'; import { SearchService } from '../../search';
import { Filter } from '../models/filter';
import { map } from 'rxjs/operators';
import { shareDistinctLast, shareLast } from '../../utils';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { MAT_CHECKBOX_DEFAULT_OPTIONS } from '@angular/material/checkbox'; import { MAT_CHECKBOX_DEFAULT_OPTIONS } from '@angular/material/checkbox';
import { toSignal } from '@angular/core/rxjs-interop';
const areExpandable = (nestedFilter: INestedFilter) => !!nestedFilter?.children?.length; const areExpandable = (nestedFilter: INestedFilter) => !!nestedFilter?.children?.length;
const atLeastOneIsExpandable = pipe( const atLeastOneIsExpandable = (group: IFilterGroup | undefined): boolean => !!group?.filters.some(areExpandable);
map<IFilterGroup | undefined, boolean>(group => !!group?.filters.some(areExpandable)),
shareDistinctLast(),
);
export interface LocalStorageFilter { export interface LocalStorageFilter {
id: string; id: string;
@ -29,6 +22,17 @@ export interface LocalStorageFilters {
secondaryFilters: LocalStorageFilter[] | null; 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({ @Component({
selector: 'iqser-filter-card [primaryFiltersSlug]', selector: 'iqser-filter-card [primaryFiltersSlug]',
templateUrl: './filter-card.component.html', templateUrl: './filter-card.component.html',
@ -53,35 +57,23 @@ export class FilterCardComponent implements OnInit {
@Input() primaryFiltersLabel: string = _('filter-menu.filter-types'); @Input() primaryFiltersLabel: string = _('filter-menu.filter-types');
@Input() minWidth = 350; @Input() minWidth = 350;
primaryFilterGroup$!: Observable<IFilterGroup | undefined>; protected readonly searchService = inject(SearchService);
secondaryFilterGroup$!: Observable<IFilterGroup | undefined>; readonly #filterService = inject(FilterService);
primaryFilters$!: Observable<IFilter[] | undefined>; readonly #elementRef = inject(ElementRef);
atLeastOneFilterIsExpandable$?: Observable<boolean>; readonly #searchValueChanged = toSignal(this.searchService.valueChanges$);
atLeastOneSecondaryFilterIsExpandable$?: Observable<boolean>; 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( atLeastOneFilterIsExpandable = computed(() => atLeastOneIsExpandable(this.primaryFilterGroup()));
readonly filterService: FilterService, atLeastOneSecondaryFilterIsExpandable = computed(() => atLeastOneIsExpandable(this.secondaryFilterGroup()));
readonly searchService: SearchService<Filter>,
private readonly _elementRef: ElementRef,
) {}
private get _primaryFilters$(): Observable<IFilter[]> {
return combineLatest([this.primaryFilterGroup$, this.searchService.valueChanges$]).pipe(
map(([group]) => this.searchService.searchIn(group?.filters ?? [])),
shareLast(),
);
}
ngOnInit() { ngOnInit() {
this.primaryFilterGroup$ = this.filterService.getGroup$(this.primaryFiltersSlug).pipe(shareLast()); (this.#elementRef.nativeElement as HTMLElement).style.setProperty('--filter-card-min-width', `${this.minWidth}px`);
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`);
} }
filterCheckboxClicked(nestedFilter: INestedFilter, filterGroup: IFilterGroup, parent?: INestedFilter): void { filterCheckboxClicked(nestedFilter: INestedFilter, filterGroup: IFilterGroup, parent?: INestedFilter): void {
@ -105,46 +97,28 @@ export class FilterCardComponent implements OnInit {
nestedFilter.children?.forEach(f => (f.checked = !!nestedFilter.checked)); nestedFilter.children?.forEach(f => (f.checked = !!nestedFilter.checked));
} }
this.filterService.refresh();
this.#updateFiltersInLocalStorage(); this.#updateFiltersInLocalStorage();
} }
activatePrimaryFilters(): void { activatePrimaryFilters(): void {
this._setFilters(this.primaryFiltersSlug, true); this.#setFilters(this.primaryFiltersSlug, true);
} }
deactivateFilters(exceptedFilterId?: string): void { deactivateFilters(exceptedFilterId?: string): void {
this._setFilters(this.primaryFiltersSlug, false, exceptedFilterId); this.#setFilters(this.primaryFiltersSlug, false, exceptedFilterId);
if (this.secondaryFiltersSlug) { if (this.secondaryFiltersSlug) {
this._setFilters(this.secondaryFiltersSlug, false, exceptedFilterId); this.#setFilters(this.secondaryFiltersSlug, false, exceptedFilterId);
} }
} }
toggleFilterExpanded(nestedFilter: INestedFilter): void { toggleFilterExpanded(nestedFilter: INestedFilter): void {
// eslint-disable-next-line no-param-reassign
nestedFilter.expanded = !nestedFilter.expanded; 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 { #updateFiltersInLocalStorage(): void {
if (this.fileId) { if (this.fileId) {
const primaryFilters = this.filterService.getGroup('primaryFilters'); const primaryFilters = this.#filterService.getGroup('primaryFilters');
const secondaryFilters = this.filterService.getGroup('secondaryFilters'); const secondaryFilters = this.#filterService.getGroup('secondaryFilters');
const filters: LocalStorageFilters = { const filters: LocalStorageFilters = {
primaryFilters: extractFilterValues(primaryFilters?.filters), primaryFilters: extractFilterValues(primaryFilters?.filters),
@ -157,4 +131,8 @@ export class FilterCardComponent implements OnInit {
localStorage.setItem('workload-filters', JSON.stringify(workloadFilters)); localStorage.setItem('workload-filters', JSON.stringify(workloadFilters));
} }
} }
#setFilters(slug: string, checked = false, exceptedFilterId?: string) {
this.#filterService.mutateFilterGroup(setFilters, [slug, checked, exceptedFilterId]);
}
} }

View File

@ -1,57 +1,22 @@
import { Injectable } from '@angular/core'; import { computed, Injectable, signal } from '@angular/core';
import { BehaviorSubject, Observable, Subject } from 'rxjs';
import { map, startWith, switchMap } from 'rxjs/operators';
import { processFilters, toFlatFilters } from './filter-utils'; import { processFilters, toFlatFilters } from './filter-utils';
import { IFilterGroup } from './models/filter-group.model'; import { IFilterGroup } from './models/filter-group.model';
import { INestedFilter } from './models/nested-filter.model'; import { INestedFilter } from './models/nested-filter.model';
import { get, shareDistinctLast, shareLast, some } from '../utils';
import { NestedFilter } from './models/nested-filter'; import { NestedFilter } from './models/nested-filter';
import { Filter } from './models/filter'; import { Filter } from './models/filter';
import { IFilter } from './models/filter.model'; import { IFilter } from './models/filter.model';
@Injectable() @Injectable()
export class FilterService { export class FilterService {
readonly #singleFilters = new Map<string, BehaviorSubject<IFilter | undefined>>(); readonly #singleFilters = signal({} as Record<string, IFilter | undefined>);
readonly #filterGroups$ = new BehaviorSubject<IFilterGroup[]>([]); readonly #filterGroups = signal([] as IFilterGroup[]);
readonly #refresh$ = new Subject(); readonly filterGroups = this.#filterGroups.asReadonly();
readonly showResetFilters$: Observable<boolean>; readonly singleFilters = computed(() => Object.values(this.#singleFilters));
readonly filterGroups$: Observable<IFilterGroup[]>; readonly enabledFlatFilters = computed(() => toFlatFilters(this.filterGroups(), filters => filters.filter(f => f.checked)));
readonly showResetFilters = computed(() => !!this.enabledFlatFilters().length);
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<boolean> {
return this.filterGroups$.pipe(
map(value => toFlatFilters(value)),
some(filter => !!filter.checked),
shareDistinctLast(),
);
}
refresh(): void {
this.#refresh$.next(true);
}
toggleFilter(filterGroupSlug: string, key: string, checkChildren = false): void { 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) { if (!filters) {
return console.error(`Cannot find filter group "${filterGroupSlug}"`); return console.error(`Cannot find filter group "${filterGroupSlug}"`);
} }
@ -71,18 +36,16 @@ export class FilterService {
found.children.forEach(c => (c.checked = true)); found.children.forEach(c => (c.checked = true));
} }
} }
this.refresh();
} }
addFilterGroup(value: IFilterGroup): void { addFilterGroup(value: IFilterGroup): void {
const oldFilters = this.getGroup(value.slug)?.filters; const oldFilters = this.getGroup(value.slug)?.filters;
if (!oldFilters) { if (!oldFilters) {
return this.#filterGroups$.next([...this.filterGroups, value]); return this.#filterGroups.update(fGs => [...fGs, value]);
} }
const newGroup = { ...value, filters: processFilters(oldFilters, value.filters) }; 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 { addFilterGroups(values: IFilterGroup[], removeOldFilters = false): void {
@ -96,54 +59,26 @@ export class FilterService {
}); });
const filterSlugs = newFilters.map(f => f.slug); const filterSlugs = newFilters.map(f => f.slug);
this.#filterGroups$.next( this.#filterGroups.update(fG =>
removeOldFilters ? newFilters : [...this.filterGroups.filter(f => !filterSlugs.includes(f.slug)), ...newFilters], 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) { addSingleFilter(filter: IFilter) {
if (!this.#singleFilters.has(filter.id)) { this.#singleFilters.update(sF => ({ ...sF, [filter.id]: filter }));
return this.#singleFilters.set(filter.id, new BehaviorSubject<IFilter | undefined>(filter)); return filter;
}
return this.#singleFilters.get(filter.id)?.next(filter);
} }
getSingleFilter(filterId: string) { getSingleFilter(filterId: string) {
if (!this.#singleFilters.has(filterId)) { return this.#singleFilters()[filterId];
this.#singleFilters.set(filterId, new BehaviorSubject<IFilter | undefined>(undefined));
}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return this.#singleFilters.get(filterId)!.asObservable();
} }
getGroup(slug: string): IFilterGroup | undefined { 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<INestedFilter[] | undefined> { getFilterModels(filterGroupSlug: string): INestedFilter[] | undefined {
return this.getGroup$(filterGroupSlug).pipe(map(f => f?.filters)); return this.getGroup(filterGroupSlug)?.filters;
}
getGroup$(slug: string): Observable<IFilterGroup | undefined> {
return this.filterGroups$.pipe(
get(group => group.slug === slug),
shareLast(),
);
} }
filtersEnabled(filterGroupSlug: string): boolean { filtersEnabled(filterGroupSlug: string): boolean {
@ -157,43 +92,46 @@ export class FilterService {
} }
reset(): void { reset(): void {
this.filterGroups.forEach(group => { this.#filterGroups.mutate(fGs => {
group.filters.forEach(filter => { fGs.forEach(group => {
// eslint-disable-next-line no-param-reassign group.filters.forEach(filter => {
filter.checked = false; filter.checked = false;
// eslint-disable-next-line no-param-reassign filter.indeterminate = false;
filter.indeterminate = false; filter.children?.forEach(f => {
filter.children?.forEach(f => { f.checked = false;
// eslint-disable-next-line no-param-reassign });
f.checked = false;
}); });
}); });
}); });
this.resetSingleFilters(); this.resetSingleFilters();
this.refresh();
} }
resetSingleFilters() { resetSingleFilters() {
this.#singleFilters.forEach(filter$ => { this.#singleFilters.update(filters =>
const filter = filter$.value; Object.entries(filters).reduce(
if (filter) { (acc, [key, value]) => ({ ...acc, [key]: value ? { ...value, checked: !value?.checked } : undefined }),
filter$.next({ ...filter$.value, checked: !filter$.value?.checked }); {},
} ),
}); );
} }
toggleSingleFilter(filterId: string) { toggleSingleFilter(filterId: string) {
const filter$ = this.#singleFilters.get(filterId); const filter = this.#singleFilters()[filterId];
if (!filter$) { if (!filter) {
return; return;
} }
const filter = filter$.value;
if (filter) { if (filter) {
filter.checked = !filter.checked; filter.checked = !filter.checked;
this.addSingleFilter(filter); this.addSingleFilter(filter);
} }
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any
mutateFilterGroup<T extends (fGs: IFilterGroup[], ...args: any[]) => void>(predicate: T, ...args: unknown[]) {
this.#filterGroups.mutate(fGs => {
predicate(fGs, ...args);
});
}
} }

View File

@ -1,32 +1,32 @@
<ng-container *ngIf="primaryFilterGroup$ | async as primaryGroup"> <ng-container *ngIf="primaryFilterGroup() as primaryGroup">
<ng-container *ngIf="primaryGroup.icon"> <ng-container *ngIf="primaryGroup.icon">
<iqser-icon-button <iqser-icon-button
[attr.aria-expanded]="expanded$ | async" [attr.aria-expanded]="expanded()"
[class.disabled]="primaryFiltersDisabled$ | async" [class.disabled]="primaryFiltersDisabled()"
[disabled]="primaryFiltersDisabled$ | async" [disabled]="primaryFiltersDisabled()"
[icon]="primaryGroup.icon" [icon]="primaryGroup.icon"
[label]="primaryGroup.label?.capitalize() || ('filter-menu.label' | translate)" [label]="primaryGroup.label?.capitalize() || ('filter-menu.label' | translate)"
[matMenuTriggerFor]="filterMenu" [matMenuTriggerFor]="filterMenu"
[showDot]="hasActiveFilters$ | async" [showDot]="hasActiveFilters()"
buttonId="{{ primaryGroup.slug }}" buttonId="{{ primaryGroup.slug }}"
></iqser-icon-button> ></iqser-icon-button>
</ng-container> </ng-container>
<ng-container *ngIf="!primaryGroup.icon"> <ng-container *ngIf="!primaryGroup.icon">
<iqser-chevron-button <iqser-chevron-button
[attr.aria-expanded]="expanded$ | async" [attr.aria-expanded]="expanded()"
[class.disabled]="primaryFiltersDisabled$ | async" [class.disabled]="primaryFiltersDisabled()"
[disabled]="primaryFiltersDisabled$ | async" [disabled]="primaryFiltersDisabled()"
[label]="primaryGroup.label?.capitalize() || ('filter-menu.label' | translate)" [label]="primaryGroup.label?.capitalize() || ('filter-menu.label' | translate)"
[matMenuTriggerFor]="filterMenu" [matMenuTriggerFor]="filterMenu"
[showDot]="hasActiveFilters$ | async" [showDot]="hasActiveFilters()"
></iqser-chevron-button> ></iqser-chevron-button>
</ng-container> </ng-container>
<mat-menu <mat-menu
#filterMenu="matMenu" #filterMenu="matMenu"
(close)="expanded.next(false)" (close)="expanded.set(false)"
[class]="(secondaryFilterGroup$ | async)?.filters.length > 0 ? 'padding-bottom-0' : ''" [class]="secondaryFilterGroup()?.filters.length > 0 ? 'padding-bottom-0' : ''"
xPosition="before" xPosition="before"
> >
<div id="workload-filters"> <div id="workload-filters">

View File

@ -1,9 +1,5 @@
import { ChangeDetectionStrategy, Component, Input, OnInit, TemplateRef } from '@angular/core'; import { ChangeDetectionStrategy, Component, computed, inject, Input, OnInit, signal, Signal, TemplateRef } from '@angular/core';
import { BehaviorSubject, combineLatest, Observable } from 'rxjs';
import { delay, map } from 'rxjs/operators';
import { shareDistinctLast, shareLast, some } from '../../utils';
import { FilterService } from '../filter.service'; import { FilterService } from '../filter.service';
import { IFilterGroup } from '../models/filter-group.model';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
@Component({ @Component({
@ -12,43 +8,22 @@ import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
styleUrls: ['./popup-filter.component.scss'], styleUrls: ['./popup-filter.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class PopupFilterComponent implements OnInit { export class PopupFilterComponent {
@Input() primaryFiltersSlug!: string; @Input() primaryFiltersSlug!: string;
@Input() fileId?: string; @Input() fileId?: string;
@Input() actionsTemplate?: TemplateRef<unknown>; @Input() actionsTemplate?: TemplateRef<unknown>;
@Input() secondaryFiltersSlug = ''; @Input() secondaryFiltersSlug = '';
@Input() primaryFiltersLabel: string = _('filter-menu.filter-types'); @Input() primaryFiltersLabel: string = _('filter-menu.filter-types');
readonly expanded = new BehaviorSubject<boolean>(false); readonly #filterService = inject(FilterService);
readonly expanded$ = this.expanded.asObservable().pipe(delay(200)); protected readonly expanded = signal(false);
hasActiveFilters$!: Observable<boolean>; protected readonly primaryFilterGroup = computed(() => this.#filterService.getGroup(this.primaryFiltersSlug));
primaryFilterGroup$!: Observable<IFilterGroup | undefined>; protected readonly secondaryFilterGroup = computed(() => this.#filterService.getGroup(this.secondaryFiltersSlug));
secondaryFilterGroup$!: Observable<IFilterGroup | undefined>; protected readonly primaryFiltersDisabled = computed(() => this.primaryFilterGroup()?.filters?.length === 0);
primaryFiltersDisabled$!: Observable<boolean>; protected readonly hasActiveFilters = computed(() =>
[...(this.primaryFilterGroup()?.filters || []), ...(this.secondaryFilterGroup()?.filters || [])].some(
constructor(readonly filterService: FilterService) {} f => f.checked || !!f.indeterminate,
),
private get _hasActiveFilters$() { );
return combineLatest([this.primaryFilterGroup$, this.secondaryFilterGroup$]).pipe(
map(([primary, secondary]) => [...(primary?.filters || []), ...(secondary?.filters || [])]),
some(f => f.checked || !!f.indeterminate),
shareDistinctLast(),
);
}
private get _primaryFiltersDisabled$(): Observable<boolean> {
return this.primaryFilterGroup$.pipe(
map(group => group?.filters?.length === 0),
shareDistinctLast(),
);
}
ngOnInit(): void {
this.primaryFilterGroup$ = this.filterService.getGroup$(this.primaryFiltersSlug).pipe(shareLast());
this.secondaryFilterGroup$ = this.filterService.getGroup$(this.secondaryFiltersSlug).pipe(shareLast());
this.hasActiveFilters$ = this._hasActiveFilters$;
this.primaryFiltersDisabled$ = this._primaryFiltersDisabled$;
}
} }

View File

@ -1,4 +1,4 @@
<ng-container *ngIf="quickFilters$ | async as filters"> <ng-container *ngIf="quickFilters() as filters">
<div <div
(click)="filterService.toggleFilter('quickFilters', filter.id)" (click)="filterService.toggleFilter('quickFilters', filter.id)"
*ngFor="let filter of filters" *ngFor="let filter of filters"

View File

@ -1,4 +1,4 @@
import { ChangeDetectionStrategy, Component } from '@angular/core'; import { ChangeDetectionStrategy, Component, computed } from '@angular/core';
import { FilterService } from '../filter.service'; import { FilterService } from '../filter.service';
@Component({ @Component({
@ -8,7 +8,7 @@ import { FilterService } from '../filter.service';
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class QuickFiltersComponent { export class QuickFiltersComponent {
readonly quickFilters$ = this.filterService.getFilterModels$('quickFilters'); readonly quickFilters = computed(() => this.filterService.getFilterModels('quickFilters'));
constructor(readonly filterService: FilterService) {} constructor(readonly filterService: FilterService) {}
} }

View File

@ -1,7 +1,7 @@
<div class="page-header"> <div class="page-header">
<div *ngIf="pageLabel" class="breadcrumb">{{ pageLabel }}</div> <div *ngIf="pageLabel" class="breadcrumb">{{ pageLabel }}</div>
<div *ngIf="filters$ | async as filters" class="filters"> <div *ngIf="filters" class="filters">
<ng-content select="[slot=beforeFilters]"></ng-content> <ng-content select="[slot=beforeFilters]"></ng-content>
<div <div
@ -20,15 +20,15 @@
></iqser-popup-filter> ></iqser-popup-filter>
</ng-container> </ng-container>
<ng-container *ngFor="let filter$ of filterService.singleFilters"> <ng-container *ngFor="let filter of filterService.singleFilters()">
<iqser-single-filter *ngIf="filter$ | async as filter" [filter]="filter"></iqser-single-filter> <iqser-single-filter *ngIf="filter" [filter]="filter"></iqser-single-filter>
</ng-container> </ng-container>
<ng-container *ngIf="searchPosition === searchPositions.afterFilters" [ngTemplateOutlet]="searchBar"></ng-container> <ng-container *ngIf="searchPosition === searchPositions.afterFilters" [ngTemplateOutlet]="searchBar"></ng-container>
<div <div
(click)="resetFilters()" (click)="resetFilters()"
*ngIf="!hideResetButton && (showResetFilters$ | async) === true" *ngIf="!hideResetButton && showResetFilters()"
[attr.help-mode-key]="'filter_' + helpModeKey + '_list'" [attr.help-mode-key]="'filter_' + helpModeKey + '_list'"
class="reset-filters" class="reset-filters"
translate="reset-filters" translate="reset-filters"

View File

@ -1,12 +1,11 @@
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Optional, Output, TemplateRef } from '@angular/core'; import { ChangeDetectionStrategy, Component, computed, EventEmitter, Input, Optional, Output, TemplateRef } from '@angular/core';
import { ActionConfig, ButtonConfig, SearchPosition, SearchPositions } from './models'; import { ActionConfig, ButtonConfig, SearchPosition, SearchPositions } from './models';
import { distinctUntilChanged, map } from 'rxjs/operators';
import { combineLatest, Observable, of } from 'rxjs';
import { IListable } from '../models'; import { IListable } from '../models';
import { IconButtonTypes } from '../../buttons'; import { IconButtonTypes } from '../../buttons';
import { SearchService } from '../../search'; import { SearchService } from '../../search';
import { FilterService } from '../../filtering'; import { FilterService } from '../../filtering';
import { filterEach, List } from '../../utils'; import { List } from '../../utils';
import { toSignal } from '@angular/core/rxjs-interop';
@Component({ @Component({
selector: 'iqser-page-header', selector: 'iqser-page-header',
@ -31,26 +30,22 @@ export class PageHeaderComponent<T extends IListable> {
@Input() searchPosition: SearchPosition = SearchPositions.afterFilters; @Input() searchPosition: SearchPosition = SearchPositions.afterFilters;
@Output() readonly closeAction = new EventEmitter(); @Output() readonly closeAction = new EventEmitter();
readonly filters$ = this.filterService?.filterGroups$.pipe(filterEach(f => !!f.icon)); readonly filters;
readonly showResetFilters$ = this.#showResetFilters$; readonly searchValue;
readonly showResetFilters;
constructor(@Optional() readonly filterService: FilterService, @Optional() readonly searchService: SearchService<T>) {} constructor(@Optional() readonly filterService: FilterService, @Optional() readonly searchService: SearchService<T>) {
this.filters = computed(() => this.filterService.filterGroups().filter(fG => fG.icon));
this.searchValue = toSignal(this.searchService.valueChanges$);
this.showResetFilters = computed(() => {
return this.filterService.showResetFilters() || this.searchValue();
});
}
get filterHelpModeKey() { get filterHelpModeKey() {
return this.helpModeKey ? `filter_${this.helpModeKey}_list` : ''; return this.helpModeKey ? `filter_${this.helpModeKey}_list` : '';
} }
get #showResetFilters$(): Observable<boolean> {
if (!this.filterService) {
return of(false);
}
return combineLatest([this.filterService.showResetFilters$, this.searchService.valueChanges$]).pipe(
map(([showResetFilters, searchValue]) => showResetFilters || !!searchValue),
distinctUntilChanged(),
);
}
resetFilters(): void { resetFilters(): void {
this.filterService.reset(); this.filterService.reset();
this.searchService.reset(); this.searchService.reset();

View File

@ -7,6 +7,7 @@ import { Id, IListable } from '../models';
import { EntitiesService } from './entities.service'; import { EntitiesService } from './entities.service';
import { getLength, shareDistinctLast, shareLast, some } from '../../utils'; import { getLength, shareDistinctLast, shareLast, some } from '../../utils';
import { SortingService } from '../../sorting'; import { SortingService } from '../../sorting';
import { toObservable } from '@angular/core/rxjs-interop';
@Injectable() @Injectable()
export class ListingService<Class extends IListable<PrimaryKey>, PrimaryKey extends Id = Class['id']> { export class ListingService<Class extends IListable<PrimaryKey>, PrimaryKey extends Id = Class['id']> {
@ -60,10 +61,10 @@ export class ListingService<Class extends IListable<PrimaryKey>, PrimaryKey exte
} }
private get _getDisplayed$(): Observable<Class[]> { private get _getDisplayed$(): Observable<Class[]> {
const { filterGroups$ } = this._filterService; const { filterGroups } = this._filterService;
const { valueChanges$ } = this._searchService; const { valueChanges$ } = this._searchService;
return combineLatest([this._entitiesService.all$, filterGroups$, valueChanges$]).pipe( return combineLatest([this._entitiesService.all$, toObservable(filterGroups), valueChanges$]).pipe(
map(([entities, filterGroups]) => getFilteredEntities(entities, filterGroups)), map(([entities, filterGroups]) => getFilteredEntities(entities, filterGroups)),
map(entities => this._searchService.searchIn(entities)), map(entities => this._searchService.searchIn(entities)),
tap(displayed => { tap(displayed => {

View File

@ -15,7 +15,7 @@
<ng-container [ngTemplateOutlet]="bulkActions"></ng-container> <ng-container [ngTemplateOutlet]="bulkActions"></ng-container>
<iqser-quick-filters *ngIf="quickFilters$ | async"></iqser-quick-filters> <iqser-quick-filters *ngIf="quickFilters()"></iqser-quick-filters>
<!-- Custom content--> <!-- Custom content-->
<ng-content></ng-content> <ng-content></ng-content>
@ -31,7 +31,7 @@
<div *ngIf="selectionEnabled" class="select-oval-placeholder"></div> <div *ngIf="selectionEnabled" class="select-oval-placeholder"></div>
<iqser-table-column-name <iqser-table-column-name
*ngFor="let config of tableColumnConfigs" *ngFor="let config of tableColumnConfigs ?? []"
[class]="config.class" [class]="config.class"
[label]="config.notTranslatable ? config.label : (config.label | translate)" [label]="config.notTranslatable ? config.label : (config.label | translate)"
[leftIcon]="config.leftIcon" [leftIcon]="config.leftIcon"

View File

@ -1,4 +1,4 @@
import { ChangeDetectionStrategy, Component, Input, TemplateRef } from '@angular/core'; import { ChangeDetectionStrategy, Component, computed, inject, Input, TemplateRef } from '@angular/core';
import { FilterService } from '../../filtering'; import { FilterService } from '../../filtering';
import { EntitiesService, ListingService } from '../services'; import { EntitiesService, ListingService } from '../services';
import { Id, IListable, ListingMode, ListingModes, TableColumnConfig } from '../models'; import { Id, IListable, ListingMode, ListingModes, TableColumnConfig } from '../models';
@ -21,11 +21,9 @@ export class TableHeaderComponent<T extends IListable<PrimaryKey>, PrimaryKey ex
@Input() bulkActions?: TemplateRef<unknown>; @Input() bulkActions?: TemplateRef<unknown>;
@Input() helpModeKey?: string; @Input() helpModeKey?: string;
readonly quickFilters$ = this.filterService.getFilterModels$('quickFilters'); readonly entitiesService = inject(EntitiesService<T, T>);
readonly listingService = inject(ListingService<T>);
readonly #filterService = inject(FilterService);
constructor( readonly quickFilters = computed(() => this.#filterService.getFilterModels('quickFilters'));
readonly entitiesService: EntitiesService<T, T>,
readonly listingService: ListingService<T>,
readonly filterService: FilterService,
) {}
} }

View File

@ -1,4 +1,4 @@
<div (click)="selectValue()" [class.active]="filterChecked$ | async" [class.pointer]="filterKey && !!filterService"> <div (click)="selectValue()" [class.active]="filterChecked()" [class.pointer]="filterKey && !!filterService">
<div class="details mb-6"> <div class="details mb-6">
<span>{{ config.count }} {{ config.label | translate }}</span> <span>{{ config.count }} {{ config.label | translate }}</span>
<mat-icon [svgIcon]="config.icon"></mat-icon> <mat-icon [svgIcon]="config.icon"></mat-icon>

View File

@ -1,9 +1,6 @@
import { ChangeDetectionStrategy, Component, Input, Optional } from '@angular/core'; import { ChangeDetectionStrategy, Component, computed, Input, OnInit, Optional, Signal } from '@angular/core';
import { ProgressBarConfigModel } from './progress-bar-config.model'; import { ProgressBarConfigModel } from './progress-bar-config.model';
import { FilterService, INestedFilter } from '../../filtering'; import { FilterService, INestedFilter } from '../../filtering';
import { Observable, of } from 'rxjs';
import { get, shareLast } from '../../utils';
import { map } from 'rxjs/operators';
@Component({ @Component({
selector: 'iqser-progress-bar [config]', selector: 'iqser-progress-bar [config]',
@ -11,26 +8,18 @@ import { map } from 'rxjs/operators';
styleUrls: ['./progress-bar.component.scss'], styleUrls: ['./progress-bar.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class ProgressBarComponent { export class ProgressBarComponent implements OnInit {
@Input() config!: ProgressBarConfigModel; @Input() config!: ProgressBarConfigModel;
@Input() filterKey?: string; @Input() filterKey?: string;
filterChecked$!: Observable<boolean>; filterChecked!: Signal<boolean>;
filters$!: Observable<INestedFilter[]>; filters!: Signal<INestedFilter[]>;
constructor(@Optional() readonly filterService: FilterService) {} constructor(@Optional() readonly filterService: FilterService) {}
ngOnInit() { ngOnInit() {
this.filters$ = this.filters = computed(() => this.filterService?.getFilterModels(this.filterKey || '-') ?? []);
this.filterService?.getFilterModels$(this.filterKey || '-').pipe( this.filterChecked = computed(() => !!this.filters().find(f => f.id === this.config.id)?.checked);
map(filters => filters ?? []),
shareLast(),
) ?? of([]);
this.filterChecked$ = this.filters$.pipe(
get(filter => filter.id === this.config.id),
map(filter => !!filter?.checked),
);
} }
selectValue(): void { selectValue(): void {