Merge branch 'master' into VM/RED-3687
This commit is contained in:
commit
49e7806999
101
src/lib/filtering/filter-card/filter-card.component.html
Normal file
101
src/lib/filtering/filter-card/filter-card.component.html
Normal file
@ -0,0 +1,101 @@
|
||||
<ng-container *ngIf="primaryFilterGroup$ | async as primaryGroup">
|
||||
<iqser-input-with-action
|
||||
(click)="$event.stopPropagation()"
|
||||
*ngIf="primaryGroup.filterceptionPlaceholder"
|
||||
[(value)]="searchService.searchValue"
|
||||
[id]="'filterception-' + primaryGroup.slug"
|
||||
[placeholder]="primaryGroup.filterceptionPlaceholder"
|
||||
[width]="'full'"
|
||||
></iqser-input-with-action>
|
||||
<ng-container *ngTemplateOutlet="filterHeader"></ng-container>
|
||||
|
||||
<div *ngIf="primaryFilters$ | async as filters" class="filter-content">
|
||||
<ng-container
|
||||
*ngFor="let filter of filters"
|
||||
[ngTemplateOutletContext]="{
|
||||
filter: filter,
|
||||
filterGroup: primaryGroup,
|
||||
atLeastOneIsExpandable: atLeastOneFilterIsExpandable$ | async
|
||||
}"
|
||||
[ngTemplateOutlet]="defaultFilterTemplate"
|
||||
></ng-container>
|
||||
</div>
|
||||
|
||||
<div *ngIf="secondaryFilterGroup$ | async as secondaryGroup" class="filter-options">
|
||||
<div class="filter-menu-options">
|
||||
<div class="all-caps-label" translate="filter-menu.filter-options"></div>
|
||||
</div>
|
||||
|
||||
<ng-container
|
||||
*ngFor="let filter of secondaryGroup.filters"
|
||||
[ngTemplateOutletContext]="{
|
||||
filter: filter,
|
||||
filterGroup: secondaryGroup,
|
||||
atLeastOneIsExpandable: atLeastOneSecondaryFilterIsExpandable$ | async
|
||||
}"
|
||||
[ngTemplateOutlet]="defaultFilterTemplate"
|
||||
></ng-container>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-template #defaultFilterLabelTemplate let-filter="filter">
|
||||
{{ filter?.label }}
|
||||
</ng-template>
|
||||
|
||||
<ng-template #filterHeader>
|
||||
<div *ngIf="primaryFilterGroup$ | async as primaryGroup" class="filter-menu-header">
|
||||
<div [translateParams]="{ count: primaryGroup.filters.length }" [translate]="primaryFiltersLabel" class="all-caps-label"></div>
|
||||
<div class="actions">
|
||||
<div
|
||||
(click)="activatePrimaryFilters(); $event.stopPropagation()"
|
||||
*ngIf="!primaryGroup.singleSelect"
|
||||
class="all-caps-label primary pointer"
|
||||
translate="actions.all"
|
||||
></div>
|
||||
<div
|
||||
(click)="deactivateFilters(); $event.stopPropagation()"
|
||||
class="all-caps-label primary pointer"
|
||||
translate="actions.none"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #defaultFilterTemplate let-atLeastOneIsExpandable="atLeastOneIsExpandable" let-filter="filter" let-filterGroup="filterGroup">
|
||||
<div (click)="toggleFilterExpanded($event, filter)" class="mat-menu-item flex">
|
||||
<div *ngIf="filter.children?.length > 0" class="arrow-wrapper">
|
||||
<mat-icon *ngIf="filter.expanded" color="accent" svgIcon="iqser:arrow-down"></mat-icon>
|
||||
<mat-icon *ngIf="!filter.expanded" color="accent" svgIcon="iqser:arrow-right"></mat-icon>
|
||||
</div>
|
||||
|
||||
<div *ngIf="atLeastOneIsExpandable && filter.children?.length === 0" class="arrow-wrapper spacer"> </div>
|
||||
|
||||
<mat-checkbox
|
||||
(click)="filterCheckboxClicked($event, filter, filterGroup)"
|
||||
[checked]="filter.checked"
|
||||
[id]="'filter-checkbox-' + (filter.id ? filter.id.replaceAll(' ', '-').replaceAll('.', '-') : 'none')"
|
||||
[indeterminate]="filter.indeterminate"
|
||||
class="filter-menu-checkbox"
|
||||
>
|
||||
<ng-container
|
||||
[ngTemplateOutletContext]="{ filter: filter }"
|
||||
[ngTemplateOutlet]="filterGroup.filterTemplate ?? defaultFilterLabelTemplate"
|
||||
></ng-container>
|
||||
</mat-checkbox>
|
||||
|
||||
<ng-container [ngTemplateOutletContext]="{ filter: filter }" [ngTemplateOutlet]="actionsTemplate"></ng-container>
|
||||
</div>
|
||||
|
||||
<div *ngIf="filter.children?.length && filter.expanded">
|
||||
<div (click)="$event.stopPropagation()" *ngFor="let child of filter.children" class="padding-left mat-menu-item">
|
||||
<mat-checkbox (click)="filterCheckboxClicked($event, child, filterGroup, filter)" [checked]="child.checked">
|
||||
<ng-container
|
||||
[ngTemplateOutletContext]="{ filter: child }"
|
||||
[ngTemplateOutlet]="filterGroup.filterTemplate ?? defaultFilterLabelTemplate"
|
||||
></ng-container>
|
||||
</mat-checkbox>
|
||||
|
||||
<ng-container [ngTemplateOutletContext]="{ filter: child }" [ngTemplateOutlet]="actionsTemplate"></ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
46
src/lib/filtering/filter-card/filter-card.component.scss
Normal file
46
src/lib/filtering/filter-card/filter-card.component.scss
Normal file
@ -0,0 +1,46 @@
|
||||
@use '../../../assets/styles/common-mixins' as mixins;
|
||||
|
||||
.filter-menu-options,
|
||||
.filter-menu-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 8px 16px 16px 16px;
|
||||
min-width: var(--filter-card-min-width);
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
|
||||
> *:not(:last-child) {
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.filter-content {
|
||||
@include mixins.scroll-bar;
|
||||
max-height: 570px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.filter-menu-options {
|
||||
margin-top: 8px;
|
||||
padding: 16px 16px 3px;
|
||||
}
|
||||
|
||||
.filter-options {
|
||||
background-color: var(--iqser-side-nav);
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
iqser-input-with-action {
|
||||
padding: 0 8px 8px 8px;
|
||||
}
|
||||
|
||||
::ng-deep .filter-menu-checkbox {
|
||||
width: 100%;
|
||||
|
||||
label {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
132
src/lib/filtering/filter-card/filter-card.component.ts
Normal file
132
src/lib/filtering/filter-card/filter-card.component.ts
Normal file
@ -0,0 +1,132 @@
|
||||
import { ChangeDetectionStrategy, Component, ElementRef, 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 { IFilterGroup } from '../models/filter-group.model';
|
||||
import { 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';
|
||||
|
||||
const areExpandable = (nestedFilter: INestedFilter) => !!nestedFilter?.children?.length;
|
||||
const atLeastOneIsExpandable = pipe(
|
||||
map<IFilterGroup | undefined, boolean>(group => !!group?.filters.some(areExpandable)),
|
||||
shareDistinctLast(),
|
||||
);
|
||||
|
||||
@Component({
|
||||
selector: 'iqser-filter-card [primaryFiltersSlug]',
|
||||
templateUrl: './filter-card.component.html',
|
||||
styleUrls: ['./filter-card.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
providers: [
|
||||
{
|
||||
provide: MAT_CHECKBOX_DEFAULT_OPTIONS,
|
||||
useValue: {
|
||||
clickAction: 'noop',
|
||||
color: 'primary',
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
export class FilterCardComponent implements OnInit {
|
||||
@Input() primaryFiltersSlug!: string;
|
||||
@Input() actionsTemplate?: TemplateRef<unknown>;
|
||||
@Input() secondaryFiltersSlug = '';
|
||||
@Input() primaryFiltersLabel: string = _('filter-menu.filter-types');
|
||||
@Input() minWidth = 350;
|
||||
|
||||
primaryFilterGroup$!: Observable<IFilterGroup | undefined>;
|
||||
secondaryFilterGroup$!: Observable<IFilterGroup | undefined>;
|
||||
primaryFilters$!: Observable<IFilter[] | undefined>;
|
||||
|
||||
atLeastOneFilterIsExpandable$?: Observable<boolean>;
|
||||
atLeastOneSecondaryFilterIsExpandable$?: Observable<boolean>;
|
||||
|
||||
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(),
|
||||
);
|
||||
}
|
||||
|
||||
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`);
|
||||
}
|
||||
|
||||
filterCheckboxClicked($event: MouseEvent, nestedFilter: INestedFilter, filterGroup: IFilterGroup, parent?: INestedFilter): void {
|
||||
$event.stopPropagation();
|
||||
|
||||
if (filterGroup.singleSelect) {
|
||||
this.deactivateFilters(nestedFilter.id);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
nestedFilter.checked = !nestedFilter.checked;
|
||||
|
||||
if (parent) {
|
||||
handleCheckedValue(parent);
|
||||
} else {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
if (nestedFilter.indeterminate) {
|
||||
nestedFilter.checked = false;
|
||||
}
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
nestedFilter.indeterminate = false;
|
||||
// eslint-disable-next-line no-return-assign,no-param-reassign
|
||||
nestedFilter.children?.forEach(f => (f.checked = !!nestedFilter.checked));
|
||||
}
|
||||
|
||||
this.filterService.refresh();
|
||||
}
|
||||
|
||||
activatePrimaryFilters(): void {
|
||||
this._setFilters(this.primaryFiltersSlug, true);
|
||||
}
|
||||
|
||||
deactivateFilters(exceptedFilterId?: string): void {
|
||||
this._setFilters(this.primaryFiltersSlug, false, exceptedFilterId);
|
||||
if (this.secondaryFiltersSlug) {
|
||||
this._setFilters(this.secondaryFiltersSlug, false, exceptedFilterId);
|
||||
}
|
||||
}
|
||||
|
||||
toggleFilterExpanded($event: MouseEvent, nestedFilter: INestedFilter): void {
|
||||
$event.stopPropagation();
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
@ -102,6 +102,15 @@ export class FilterService {
|
||||
);
|
||||
}
|
||||
|
||||
updateFilterGroups(newFilters: IFilterGroup[]): void {
|
||||
const filters = this.filterGroups.map(oldFilter => {
|
||||
const newFilter = newFilters.find(f => f.slug === oldFilter.slug);
|
||||
return newFilter ? newFilter : oldFilter;
|
||||
});
|
||||
|
||||
this.#filterGroups$.next(filters);
|
||||
}
|
||||
|
||||
addSingleFilter(filter: IFilter) {
|
||||
if (!this.#singleFilters.has(filter.id)) {
|
||||
return this.#singleFilters.set(filter.id, new BehaviorSubject<IFilter | undefined>(filter));
|
||||
|
||||
@ -10,10 +10,11 @@ import { IqserIconsModule } from '../icons';
|
||||
import { IqserInputsModule } from '../inputs';
|
||||
import { IqserHelpModeModule } from '../help-mode';
|
||||
import { SingleFilterComponent } from './single-filter/single-filter.component';
|
||||
import { FilterCardComponent } from './filter-card/filter-card.component';
|
||||
|
||||
const matModules = [MatCheckboxModule, MatMenuModule];
|
||||
const modules = [TranslateModule, IqserButtonsModule, IqserIconsModule, IqserInputsModule, IqserHelpModeModule];
|
||||
const components = [QuickFiltersComponent, PopupFilterComponent, SingleFilterComponent];
|
||||
const components = [QuickFiltersComponent, PopupFilterComponent, SingleFilterComponent, FilterCardComponent];
|
||||
|
||||
@NgModule({
|
||||
declarations: [...components],
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
[class.disabled]="primaryFiltersDisabled$ | async"
|
||||
[disabled]="primaryFiltersDisabled$ | async"
|
||||
[icon]="primaryGroup.icon"
|
||||
[label]="primaryGroup.label || ('filter-menu.label' | translate)"
|
||||
[label]="primaryGroup.label?.capitalize() || ('filter-menu.label' | translate)"
|
||||
[matMenuTriggerFor]="filterMenu"
|
||||
[showDot]="hasActiveFilters$ | async"
|
||||
id="{{ primaryGroup.slug }}"
|
||||
@ -17,7 +17,7 @@
|
||||
[attr.aria-expanded]="expanded$ | async"
|
||||
[class.disabled]="primaryFiltersDisabled$ | async"
|
||||
[disabled]="primaryFiltersDisabled$ | async"
|
||||
[label]="primaryGroup.label || ('filter-menu.label' | translate)"
|
||||
[label]="primaryGroup.label?.capitalize() || ('filter-menu.label' | translate)"
|
||||
[matMenuTriggerFor]="filterMenu"
|
||||
[showDot]="hasActiveFilters$ | async"
|
||||
></iqser-chevron-button>
|
||||
@ -30,106 +30,12 @@
|
||||
xPosition="before"
|
||||
>
|
||||
<ng-template matMenuContent>
|
||||
<iqser-input-with-action
|
||||
(click)="$event.stopPropagation()"
|
||||
*ngIf="primaryGroup.filterceptionPlaceholder"
|
||||
[(value)]="searchService.searchValue"
|
||||
[id]="'filterception-' + primaryGroup.slug"
|
||||
[placeholder]="primaryGroup.filterceptionPlaceholder"
|
||||
[width]="'full'"
|
||||
></iqser-input-with-action>
|
||||
|
||||
<ng-container *ngTemplateOutlet="filterHeader"></ng-container>
|
||||
|
||||
<div *ngIf="primaryFilters$ | async as filters" class="filter-content">
|
||||
<ng-container
|
||||
*ngFor="let filter of filters"
|
||||
[ngTemplateOutletContext]="{
|
||||
filter: filter,
|
||||
filterGroup: primaryGroup,
|
||||
atLeastOneIsExpandable: atLeastOneFilterIsExpandable$ | async
|
||||
}"
|
||||
[ngTemplateOutlet]="defaultFilterTemplate"
|
||||
></ng-container>
|
||||
</div>
|
||||
|
||||
<div *ngIf="secondaryFilterGroup$ | async as secondaryGroup" class="filter-options">
|
||||
<div class="filter-menu-options">
|
||||
<div class="all-caps-label" translate="filter-menu.filter-options"></div>
|
||||
</div>
|
||||
|
||||
<ng-container
|
||||
*ngFor="let filter of secondaryGroup.filters"
|
||||
[ngTemplateOutletContext]="{
|
||||
filter: filter,
|
||||
filterGroup: secondaryGroup,
|
||||
atLeastOneIsExpandable: atLeastOneSecondaryFilterIsExpandable$ | async
|
||||
}"
|
||||
[ngTemplateOutlet]="defaultFilterTemplate"
|
||||
></ng-container>
|
||||
</div>
|
||||
<iqser-filter-card
|
||||
[actionsTemplate]="actionsTemplate"
|
||||
[primaryFiltersLabel]="primaryFiltersLabel"
|
||||
[primaryFiltersSlug]="primaryFiltersSlug"
|
||||
[secondaryFiltersSlug]="secondaryFiltersSlug"
|
||||
></iqser-filter-card>
|
||||
</ng-template>
|
||||
</mat-menu>
|
||||
</ng-container>
|
||||
|
||||
<ng-template #defaultFilterLabelTemplate let-filter="filter">
|
||||
{{ filter?.label }}
|
||||
</ng-template>
|
||||
|
||||
<ng-template #filterHeader>
|
||||
<div *ngIf="primaryFilterGroup$ | async as primaryGroup" class="filter-menu-header">
|
||||
<div [translateParams]="{ count: primaryGroup.filters.length }" [translate]="primaryFiltersLabel" class="all-caps-label"></div>
|
||||
<div class="actions">
|
||||
<div
|
||||
(click)="activatePrimaryFilters(); $event.stopPropagation()"
|
||||
*ngIf="!primaryGroup.singleSelect"
|
||||
class="all-caps-label primary pointer"
|
||||
translate="actions.all"
|
||||
></div>
|
||||
<div
|
||||
(click)="deactivateFilters(); $event.stopPropagation()"
|
||||
class="all-caps-label primary pointer"
|
||||
translate="actions.none"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #defaultFilterTemplate let-atLeastOneIsExpandable="atLeastOneIsExpandable" let-filter="filter" let-filterGroup="filterGroup">
|
||||
<div (click)="toggleFilterExpanded($event, filter)" class="mat-menu-item flex">
|
||||
<div *ngIf="filter.children?.length > 0" class="arrow-wrapper">
|
||||
<mat-icon *ngIf="filter.expanded" color="accent" svgIcon="iqser:arrow-down"></mat-icon>
|
||||
<mat-icon *ngIf="!filter.expanded" color="accent" svgIcon="iqser:arrow-right"></mat-icon>
|
||||
</div>
|
||||
|
||||
<div *ngIf="atLeastOneIsExpandable && filter.children?.length === 0" class="arrow-wrapper spacer"> </div>
|
||||
|
||||
<mat-checkbox
|
||||
(click)="filterCheckboxClicked($event, filter, filterGroup)"
|
||||
[checked]="filter.checked"
|
||||
[id]="'filter-checkbox-' + (filter.id ? filter.id.replaceAll(' ', '-').replaceAll('.', '-') : 'none')"
|
||||
[indeterminate]="filter.indeterminate"
|
||||
class="filter-menu-checkbox"
|
||||
>
|
||||
<ng-container
|
||||
[ngTemplateOutletContext]="{ filter: filter }"
|
||||
[ngTemplateOutlet]="filterGroup.filterTemplate ?? defaultFilterLabelTemplate"
|
||||
></ng-container>
|
||||
</mat-checkbox>
|
||||
|
||||
<ng-container [ngTemplateOutletContext]="{ filter: filter }" [ngTemplateOutlet]="actionsTemplate"></ng-container>
|
||||
</div>
|
||||
|
||||
<div *ngIf="filter.children?.length && filter.expanded">
|
||||
<div (click)="$event.stopPropagation()" *ngFor="let child of filter.children" class="padding-left mat-menu-item">
|
||||
<mat-checkbox (click)="filterCheckboxClicked($event, child, filterGroup, filter)" [checked]="child.checked">
|
||||
<ng-container
|
||||
[ngTemplateOutletContext]="{ filter: child }"
|
||||
[ngTemplateOutlet]="filterGroup.filterTemplate ?? defaultFilterLabelTemplate"
|
||||
></ng-container>
|
||||
</mat-checkbox>
|
||||
|
||||
<ng-container [ngTemplateOutletContext]="{ filter: child }" [ngTemplateOutlet]="actionsTemplate"></ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
@ -1,50 +1,5 @@
|
||||
@use '../../../assets/styles/common-mixins' as mixins;
|
||||
|
||||
.filter-menu-options,
|
||||
.filter-menu-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 8px 16px 16px 16px;
|
||||
min-width: 350px;
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
|
||||
> *:not(:last-child) {
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.filter-content {
|
||||
@include mixins.scroll-bar;
|
||||
max-height: 570px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.filter-menu-options {
|
||||
margin-top: 8px;
|
||||
padding: 16px 16px 3px;
|
||||
}
|
||||
|
||||
.filter-options {
|
||||
background-color: var(--iqser-side-nav);
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
::ng-deep .filter-menu-checkbox {
|
||||
width: 100%;
|
||||
|
||||
label {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
iqser-input-with-action {
|
||||
padding: 0 8px 8px 8px;
|
||||
}
|
||||
|
||||
.disabled {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@ -1,37 +1,19 @@
|
||||
import { ChangeDetectionStrategy, Component, Input, OnInit, TemplateRef } from '@angular/core';
|
||||
import { MAT_CHECKBOX_DEFAULT_OPTIONS } from '@angular/material/checkbox';
|
||||
import { BehaviorSubject, combineLatest, Observable, pipe } from 'rxjs';
|
||||
import { BehaviorSubject, combineLatest, Observable } from 'rxjs';
|
||||
import { delay, map } from 'rxjs/operators';
|
||||
import { any, shareDistinctLast, shareLast } from '../../utils';
|
||||
import { handleCheckedValue } from '../filter-utils';
|
||||
import { FilterService } from '../filter.service';
|
||||
import { IFilterGroup } from '../models/filter-group.model';
|
||||
import { INestedFilter } from '../models/nested-filter.model';
|
||||
import { SearchService } from '../../search';
|
||||
import { Filter, IFilter } from '..';
|
||||
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
|
||||
|
||||
const areExpandable = (nestedFilter: INestedFilter) => !!nestedFilter?.children?.length;
|
||||
const atLeastOneIsExpandable = pipe(
|
||||
map<IFilterGroup | undefined, boolean>(group => !!group?.filters.some(areExpandable)),
|
||||
shareDistinctLast(),
|
||||
);
|
||||
|
||||
@Component({
|
||||
selector: 'iqser-popup-filter [primaryFiltersSlug]',
|
||||
templateUrl: './popup-filter.component.html',
|
||||
styleUrls: ['./popup-filter.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
providers: [
|
||||
{
|
||||
provide: MAT_CHECKBOX_DEFAULT_OPTIONS,
|
||||
useValue: {
|
||||
clickAction: 'noop',
|
||||
color: 'primary',
|
||||
},
|
||||
},
|
||||
SearchService,
|
||||
],
|
||||
providers: [SearchService],
|
||||
})
|
||||
export class PopupFilterComponent implements OnInit {
|
||||
@Input() primaryFiltersSlug!: string;
|
||||
@ -39,9 +21,8 @@ export class PopupFilterComponent implements OnInit {
|
||||
@Input() secondaryFiltersSlug = '';
|
||||
@Input() primaryFiltersLabel: string = _('filter-menu.filter-types');
|
||||
|
||||
atLeastOneFilterIsExpandable$?: Observable<boolean>;
|
||||
atLeastOneSecondaryFilterIsExpandable$?: Observable<boolean>;
|
||||
hasActiveFilters$?: Observable<boolean>;
|
||||
|
||||
readonly expanded = new BehaviorSubject<boolean>(false);
|
||||
readonly expanded$ = this.expanded.asObservable().pipe(delay(200));
|
||||
|
||||
@ -82,66 +63,5 @@ export class PopupFilterComponent implements OnInit {
|
||||
|
||||
this.hasActiveFilters$ = this._hasActiveFilters$;
|
||||
this.primaryFiltersDisabled$ = this._primaryFiltersDisabled$;
|
||||
this.atLeastOneFilterIsExpandable$ = atLeastOneIsExpandable(this.primaryFilterGroup$);
|
||||
this.atLeastOneSecondaryFilterIsExpandable$ = atLeastOneIsExpandable(this.secondaryFilterGroup$);
|
||||
}
|
||||
|
||||
filterCheckboxClicked($event: MouseEvent, nestedFilter: INestedFilter, filterGroup: IFilterGroup, parent?: INestedFilter): void {
|
||||
$event.stopPropagation();
|
||||
|
||||
if (filterGroup.singleSelect) {
|
||||
this.deactivateFilters(nestedFilter.id);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
nestedFilter.checked = !nestedFilter.checked;
|
||||
|
||||
if (parent) {
|
||||
handleCheckedValue(parent);
|
||||
} else {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
if (nestedFilter.indeterminate) {
|
||||
nestedFilter.checked = false;
|
||||
}
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
nestedFilter.indeterminate = false;
|
||||
// eslint-disable-next-line no-return-assign,no-param-reassign
|
||||
nestedFilter.children?.forEach(f => (f.checked = !!nestedFilter.checked));
|
||||
}
|
||||
|
||||
this.filterService.refresh();
|
||||
}
|
||||
|
||||
activatePrimaryFilters(): void {
|
||||
this._setFilters(this.primaryFiltersSlug, true);
|
||||
}
|
||||
|
||||
deactivateFilters(exceptedFilterId?: string): void {
|
||||
this._setFilters(this.primaryFiltersSlug, false, exceptedFilterId);
|
||||
if (this.secondaryFiltersSlug) {
|
||||
this._setFilters(this.secondaryFiltersSlug, false, exceptedFilterId);
|
||||
}
|
||||
}
|
||||
|
||||
toggleFilterExpanded($event: MouseEvent, nestedFilter: INestedFilter): void {
|
||||
$event.stopPropagation();
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { Directive } from '@angular/core';
|
||||
import { ChangeDetectorRef, Directive, inject } from '@angular/core';
|
||||
import { ControlValueAccessor, ValidationErrors, Validator } from '@angular/forms';
|
||||
|
||||
@Directive()
|
||||
@ -6,6 +6,8 @@ export abstract class FormFieldComponent<I> implements ControlValueAccessor, Val
|
||||
touched = false;
|
||||
disabled = false;
|
||||
|
||||
protected readonly _changeRef = inject(ChangeDetectorRef);
|
||||
|
||||
protected _value: I | undefined;
|
||||
|
||||
get value(): I | undefined {
|
||||
@ -38,6 +40,7 @@ export abstract class FormFieldComponent<I> implements ControlValueAccessor, Val
|
||||
|
||||
writeValue(option: I): void {
|
||||
this._value = option;
|
||||
this._changeRef.markForCheck();
|
||||
}
|
||||
|
||||
validate(): ValidationErrors | null {
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
import { IListable } from './listable';
|
||||
import { Id } from './trackable';
|
||||
|
||||
export abstract class Entity<I> implements IListable {
|
||||
abstract readonly id: string;
|
||||
abstract readonly id: Id;
|
||||
abstract readonly routerLink?: string;
|
||||
abstract readonly searchKey: string;
|
||||
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
export type Id = string | number;
|
||||
|
||||
export interface ITrackable {
|
||||
readonly id: string;
|
||||
readonly id: Id;
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@ import { filter, map, startWith, tap } from 'rxjs/operators';
|
||||
import { IListable } from '../models';
|
||||
import { GenericService, QueryParam } from '../../services';
|
||||
import { getLength, List, mapEach, shareDistinctLast, shareLast } from '../../utils';
|
||||
import { Id } from '../models/trackable';
|
||||
|
||||
@Injectable()
|
||||
/**
|
||||
@ -47,7 +48,7 @@ export class EntitiesService<I, E extends I & IListable = I & IListable> extends
|
||||
);
|
||||
}
|
||||
|
||||
getEntityChanged$(entityId: string): Observable<E | undefined> {
|
||||
getEntityChanged$(entityId: Id): Observable<E | undefined> {
|
||||
return this._entityChanged$.pipe(
|
||||
filter(entity => entity.id === entityId),
|
||||
startWith(this.find(entityId)),
|
||||
@ -55,7 +56,7 @@ export class EntitiesService<I, E extends I & IListable = I & IListable> extends
|
||||
);
|
||||
}
|
||||
|
||||
getEntityDeleted$(entityId: string): Observable<E | undefined> {
|
||||
getEntityDeleted$(entityId: Id): Observable<E | undefined> {
|
||||
return this._entityDeleted$.pipe(filter(entity => entity.id === entityId));
|
||||
}
|
||||
|
||||
@ -88,7 +89,7 @@ export class EntitiesService<I, E extends I & IListable = I & IListable> extends
|
||||
}
|
||||
}
|
||||
|
||||
remove(id: string) {
|
||||
remove(id: Id) {
|
||||
const entity = this.all.find(item => item.id === id);
|
||||
if (entity) {
|
||||
this.#all$.next(this.all.filter(item => item.id !== id));
|
||||
@ -96,11 +97,11 @@ export class EntitiesService<I, E extends I & IListable = I & IListable> extends
|
||||
}
|
||||
}
|
||||
|
||||
find(id: string): E | undefined {
|
||||
find(id: Id): E | undefined {
|
||||
return this.all.find(entity => entity.id === id);
|
||||
}
|
||||
|
||||
has(id: string): boolean {
|
||||
has(id: Id): boolean {
|
||||
return this.all.some(entity => entity.id === id);
|
||||
}
|
||||
|
||||
|
||||
@ -3,12 +3,13 @@ import { BehaviorSubject, Observable, Subject } from 'rxjs';
|
||||
import { filter, startWith } from 'rxjs/operators';
|
||||
import { Entity } from '../listing';
|
||||
import { RequiredParam, shareLast, Validate } from '../utils';
|
||||
import { Id } from '../listing/models/trackable';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export abstract class EntitiesMapService<E extends Entity<I>, I> {
|
||||
protected abstract readonly _primaryKey: string;
|
||||
|
||||
protected readonly _map = new Map<string, BehaviorSubject<E[]>>();
|
||||
protected readonly _map = new Map<Id, BehaviorSubject<E[]>>();
|
||||
readonly #entityChanged$ = new Subject<E>();
|
||||
readonly #entityDeleted$ = new Subject<E>();
|
||||
|
||||
@ -16,11 +17,11 @@ export abstract class EntitiesMapService<E extends Entity<I>, I> {
|
||||
return this._map.size === 0;
|
||||
}
|
||||
|
||||
delete(keys: string[]): void {
|
||||
delete(keys: Id[]): void {
|
||||
keys.forEach(key => this._map.delete(key));
|
||||
}
|
||||
|
||||
get$(key: string) {
|
||||
get$(key: Id) {
|
||||
if (!this._map.has(key)) {
|
||||
this._map.set(key, new BehaviorSubject<E[]>([]));
|
||||
}
|
||||
@ -28,13 +29,13 @@ export abstract class EntitiesMapService<E extends Entity<I>, I> {
|
||||
return this._getBehaviourSubject(key).asObservable();
|
||||
}
|
||||
|
||||
has(parentId: string) {
|
||||
has(parentId: Id) {
|
||||
return this._map.has(parentId);
|
||||
}
|
||||
|
||||
get(key: string): E[];
|
||||
get(key: string, id: string): E | undefined;
|
||||
get(key: string, id?: string): E | E[] | undefined {
|
||||
get(key: Id): E[];
|
||||
get(key: Id, id: Id): E | undefined;
|
||||
get(key: Id, id?: Id): E | E[] | undefined {
|
||||
const value = this._getBehaviourSubject(key)?.value;
|
||||
if (!id) {
|
||||
return value ?? [];
|
||||
@ -42,7 +43,7 @@ export abstract class EntitiesMapService<E extends Entity<I>, I> {
|
||||
return value?.find(item => item.id === id);
|
||||
}
|
||||
|
||||
set(key: string, entities: E[]): void {
|
||||
set(key: Id, entities: E[]): void {
|
||||
if (!this._map.has(key)) {
|
||||
this._map.set(key, new BehaviorSubject<E[]>(entities));
|
||||
return entities.forEach(entity => this.#entityChanged$.next(entity));
|
||||
@ -96,7 +97,7 @@ export abstract class EntitiesMapService<E extends Entity<I>, I> {
|
||||
}
|
||||
|
||||
@Validate()
|
||||
watch$(@RequiredParam() key: string, @RequiredParam() entityId: string): Observable<E> {
|
||||
watch$(@RequiredParam() key: Id, @RequiredParam() entityId: Id): Observable<E> {
|
||||
return this.#entityChanged$.pipe(
|
||||
filter(entity => entity.id === entityId),
|
||||
startWith(this.get(key, entityId) as E),
|
||||
@ -104,17 +105,17 @@ export abstract class EntitiesMapService<E extends Entity<I>, I> {
|
||||
);
|
||||
}
|
||||
|
||||
watchDeleted$(entityId: string): Observable<E> {
|
||||
watchDeleted$(entityId: Id): Observable<E> {
|
||||
return this.#entityDeleted$.pipe(filter(entity => entity.id === entityId));
|
||||
}
|
||||
|
||||
private _pluckPrimaryKey(entity: E): string {
|
||||
private _pluckPrimaryKey(entity: E): Id {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
return entity[this._primaryKey] as string;
|
||||
return entity[this._primaryKey] as Id;
|
||||
}
|
||||
|
||||
private _getBehaviourSubject(key: string): BehaviorSubject<E[]> {
|
||||
private _getBehaviourSubject(key: Id): BehaviorSubject<E[]> {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
return this._map.get(key)!;
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { ITrackable } from '../listing/models/trackable';
|
||||
import { Id, ITrackable } from '../listing/models/trackable';
|
||||
import { UntypedFormGroup } from '@angular/forms';
|
||||
import { forOwn, has, isEqual, isPlainObject, transform } from 'lodash-es';
|
||||
import dayjs, { Dayjs } from 'dayjs';
|
||||
@ -34,7 +34,7 @@ export function toNumber(str: string): number {
|
||||
}
|
||||
|
||||
export function trackByFactory<T extends ITrackable>() {
|
||||
return (_index: number, item: T): string => item.id;
|
||||
return (_index: number, item: T): Id => item.id;
|
||||
}
|
||||
|
||||
export function hasFormChanged(form: UntypedFormGroup, initialFormValue: Record<string, string>): boolean {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user