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
*ngIf="primaryGroup.filterceptionPlaceholder"
[(value)]="searchService.searchValue"
@ -9,19 +9,19 @@
<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
*ngFor="let filter of filters"
[ngTemplateOutletContext]="{
filter: filter,
filterGroup: primaryGroup,
atLeastOneIsExpandable: atLeastOneFilterIsExpandable$ | async
atLeastOneIsExpandable: atLeastOneFilterIsExpandable()
}"
[ngTemplateOutlet]="defaultFilterTemplate"
></ng-container>
</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="all-caps-label" translate="filter-menu.filter-options"></div>
</div>
@ -31,7 +31,7 @@
[ngTemplateOutletContext]="{
filter: filter,
filterGroup: secondaryGroup,
atLeastOneIsExpandable: atLeastOneSecondaryFilterIsExpandable$ | async
atLeastOneIsExpandable: atLeastOneSecondaryFilterIsExpandable()
}"
[ngTemplateOutlet]="defaultFilterTemplate"
></ng-container>
@ -44,7 +44,7 @@
<!--TODO: move to separate component-->
<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 class="actions">
<div

View File

@ -1,22 +1,15 @@
import { ChangeDetectionStrategy, Component, ElementRef, Input, OnInit, TemplateRef } from '@angular/core';
import { combineLatest, Observable, pipe } from 'rxjs';
import { IFilter } from '../models/filter.model';
import { ChangeDetectionStrategy, Component, computed, ElementRef, inject, Input, OnInit, TemplateRef } from '@angular/core';
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 { 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<IFilterGroup | undefined, boolean>(group => !!group?.filters.some(areExpandable)),
shareDistinctLast(),
);
const atLeastOneIsExpandable = (group: IFilterGroup | undefined): boolean => !!group?.filters.some(areExpandable);
export interface LocalStorageFilter {
id: string;
@ -29,6 +22,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 +57,23 @@ export class FilterCardComponent implements OnInit {
@Input() primaryFiltersLabel: string = _('filter-menu.filter-types');
@Input() minWidth = 350;
primaryFilterGroup$!: Observable<IFilterGroup | undefined>;
secondaryFilterGroup$!: Observable<IFilterGroup | undefined>;
primaryFilters$!: Observable<IFilter[] | undefined>;
protected readonly searchService = inject(SearchService);
readonly #filterService = inject(FilterService);
readonly #elementRef = inject(ElementRef);
atLeastOneFilterIsExpandable$?: Observable<boolean>;
atLeastOneSecondaryFilterIsExpandable$?: Observable<boolean>;
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<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(),
);
}
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 +97,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 +131,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]);
}
}

View File

@ -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<string, BehaviorSubject<IFilter | undefined>>();
readonly #filterGroups$ = new BehaviorSubject<IFilterGroup[]>([]);
readonly #refresh$ = new Subject();
readonly showResetFilters$: Observable<boolean>;
readonly filterGroups$: Observable<IFilterGroup[]>;
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);
}
readonly #singleFilters = signal({} as Record<string, IFilter | undefined>);
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<IFilter | undefined>(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<IFilter | undefined>(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<INestedFilter[] | undefined> {
return this.getGroup$(filterGroupSlug).pipe(map(f => f?.filters));
}
getGroup$(slug: string): Observable<IFilterGroup | undefined> {
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<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">
<iqser-icon-button
[attr.aria-expanded]="expanded$ | async"
[class.disabled]="primaryFiltersDisabled$ | async"
[disabled]="primaryFiltersDisabled$ | async"
[attr.aria-expanded]="expanded()"
[class.disabled]="primaryFiltersDisabled()"
[disabled]="primaryFiltersDisabled()"
[icon]="primaryGroup.icon"
[label]="primaryGroup.label?.capitalize() || ('filter-menu.label' | translate)"
[matMenuTriggerFor]="filterMenu"
[showDot]="hasActiveFilters$ | async"
[showDot]="hasActiveFilters()"
buttonId="{{ primaryGroup.slug }}"
></iqser-icon-button>
</ng-container>
<ng-container *ngIf="!primaryGroup.icon">
<iqser-chevron-button
[attr.aria-expanded]="expanded$ | async"
[class.disabled]="primaryFiltersDisabled$ | async"
[disabled]="primaryFiltersDisabled$ | async"
[attr.aria-expanded]="expanded()"
[class.disabled]="primaryFiltersDisabled()"
[disabled]="primaryFiltersDisabled()"
[label]="primaryGroup.label?.capitalize() || ('filter-menu.label' | translate)"
[matMenuTriggerFor]="filterMenu"
[showDot]="hasActiveFilters$ | async"
[showDot]="hasActiveFilters()"
></iqser-chevron-button>
</ng-container>
<mat-menu
#filterMenu="matMenu"
(close)="expanded.next(false)"
[class]="(secondaryFilterGroup$ | async)?.filters.length > 0 ? 'padding-bottom-0' : ''"
(close)="expanded.set(false)"
[class]="secondaryFilterGroup()?.filters.length > 0 ? 'padding-bottom-0' : ''"
xPosition="before"
>
<div id="workload-filters">

View File

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

View File

@ -1,4 +1,4 @@
<ng-container *ngIf="quickFilters$ | async as filters">
<ng-container *ngIf="quickFilters() as filters">
<div
(click)="filterService.toggleFilter('quickFilters', filter.id)"
*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';
@Component({
@ -8,7 +8,7 @@ import { FilterService } from '../filter.service';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class QuickFiltersComponent {
readonly quickFilters$ = this.filterService.getFilterModels$('quickFilters');
readonly quickFilters = computed(() => this.filterService.getFilterModels('quickFilters'));
constructor(readonly filterService: FilterService) {}
}

View File

@ -1,7 +1,7 @@
<div class="page-header">
<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>
<div
@ -20,15 +20,15 @@
></iqser-popup-filter>
</ng-container>
<ng-container *ngFor="let filter$ of filterService.singleFilters">
<iqser-single-filter *ngIf="filter$ | async as filter" [filter]="filter"></iqser-single-filter>
<ng-container *ngFor="let filter of filterService.singleFilters()">
<iqser-single-filter *ngIf="filter" [filter]="filter"></iqser-single-filter>
</ng-container>
<ng-container *ngIf="searchPosition === searchPositions.afterFilters" [ngTemplateOutlet]="searchBar"></ng-container>
<div
(click)="resetFilters()"
*ngIf="!hideResetButton && (showResetFilters$ | async) === true"
*ngIf="!hideResetButton && showResetFilters()"
[attr.help-mode-key]="'filter_' + helpModeKey + '_list'"
class="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 { distinctUntilChanged, map } from 'rxjs/operators';
import { combineLatest, Observable, of } from 'rxjs';
import { IListable } from '../models';
import { IconButtonTypes } from '../../buttons';
import { SearchService } from '../../search';
import { FilterService } from '../../filtering';
import { filterEach, List } from '../../utils';
import { List } from '../../utils';
import { toSignal } from '@angular/core/rxjs-interop';
@Component({
selector: 'iqser-page-header',
@ -31,26 +30,22 @@ export class PageHeaderComponent<T extends IListable> {
@Input() searchPosition: SearchPosition = SearchPositions.afterFilters;
@Output() readonly closeAction = new EventEmitter();
readonly filters$ = this.filterService?.filterGroups$.pipe(filterEach(f => !!f.icon));
readonly showResetFilters$ = this.#showResetFilters$;
readonly filters;
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() {
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 {
this.filterService.reset();
this.searchService.reset();

View File

@ -7,6 +7,7 @@ import { Id, IListable } from '../models';
import { EntitiesService } from './entities.service';
import { getLength, shareDistinctLast, shareLast, some } from '../../utils';
import { SortingService } from '../../sorting';
import { toObservable } from '@angular/core/rxjs-interop';
@Injectable()
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[]> {
const { filterGroups$ } = this._filterService;
const { filterGroups } = this._filterService;
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 => this._searchService.searchIn(entities)),
tap(displayed => {

View File

@ -15,7 +15,7 @@
<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-->
<ng-content></ng-content>
@ -31,7 +31,7 @@
<div *ngIf="selectionEnabled" class="select-oval-placeholder"></div>
<iqser-table-column-name
*ngFor="let config of tableColumnConfigs"
*ngFor="let config of tableColumnConfigs ?? []"
[class]="config.class"
[label]="config.notTranslatable ? config.label : (config.label | translate)"
[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 { EntitiesService, ListingService } from '../services';
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() 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 entitiesService: EntitiesService<T, T>,
readonly listingService: ListingService<T>,
readonly filterService: FilterService,
) {}
readonly quickFilters = computed(() => this.#filterService.getFilterModels('quickFilters'));
}

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