Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
41d19013ad | ||
|
|
9e0c31992a |
@ -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
|
||||
|
||||
@ -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]);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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) {}
|
||||
}
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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 => {
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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'));
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user