From ee433cdee2d5c1e34a43a54da2e40f0f16a6ca07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adina=20=C8=9Aeudan?= Date: Thu, 21 Jul 2022 21:38:17 +0300 Subject: [PATCH 1/4] Filter card --- .../filter-card/filter-card.component.html | 101 ++++++++++++++ .../filter-card/filter-card.component.scss | 46 ++++++ .../filter-card/filter-card.component.ts | 132 ++++++++++++++++++ src/lib/filtering/filter.service.ts | 9 ++ src/lib/filtering/filters.module.ts | 3 +- .../popup-filter/popup-filter.component.html | 110 ++------------- .../popup-filter/popup-filter.component.scss | 45 ------ .../popup-filter/popup-filter.component.ts | 89 +----------- 8 files changed, 302 insertions(+), 233 deletions(-) create mode 100644 src/lib/filtering/filter-card/filter-card.component.html create mode 100644 src/lib/filtering/filter-card/filter-card.component.scss create mode 100644 src/lib/filtering/filter-card/filter-card.component.ts diff --git a/src/lib/filtering/filter-card/filter-card.component.html b/src/lib/filtering/filter-card/filter-card.component.html new file mode 100644 index 0000000..db78647 --- /dev/null +++ b/src/lib/filtering/filter-card/filter-card.component.html @@ -0,0 +1,101 @@ + + + + +
+ +
+ +
+
+
+
+ + +
+
+ + + {{ filter?.label }} + + + +
+
+
+
+
+
+
+
+ + +
+
+ + +
+ +
 
+ + + + + + +
+ +
+
+ + + + + +
+
+
diff --git a/src/lib/filtering/filter-card/filter-card.component.scss b/src/lib/filtering/filter-card/filter-card.component.scss new file mode 100644 index 0000000..b5caea8 --- /dev/null +++ b/src/lib/filtering/filter-card/filter-card.component.scss @@ -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%; + } +} diff --git a/src/lib/filtering/filter-card/filter-card.component.ts b/src/lib/filtering/filter-card/filter-card.component.ts new file mode 100644 index 0000000..4d265b8 --- /dev/null +++ b/src/lib/filtering/filter-card/filter-card.component.ts @@ -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(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; + @Input() secondaryFiltersSlug = ''; + @Input() primaryFiltersLabel: string = _('filter-menu.filter-types'); + @Input() minWidth = 350; + + primaryFilterGroup$!: Observable; + secondaryFilterGroup$!: Observable; + primaryFilters$!: Observable; + + atLeastOneFilterIsExpandable$?: Observable; + atLeastOneSecondaryFilterIsExpandable$?: Observable; + + constructor( + readonly filterService: FilterService, + readonly searchService: SearchService, + private readonly _elementRef: ElementRef, + ) {} + + private get _primaryFilters$(): Observable { + return combineLatest([this.primaryFilterGroup$, this.searchService.valueChanges$]).pipe( + map(([group]) => this.searchService.searchIn(group?.filters ?? [])), + shareLast(), + ); + } + + 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(); + } +} diff --git a/src/lib/filtering/filter.service.ts b/src/lib/filtering/filter.service.ts index 19d7246..5f88b84 100644 --- a/src/lib/filtering/filter.service.ts +++ b/src/lib/filtering/filter.service.ts @@ -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(filter)); diff --git a/src/lib/filtering/filters.module.ts b/src/lib/filtering/filters.module.ts index aa445f8..f3143f8 100644 --- a/src/lib/filtering/filters.module.ts +++ b/src/lib/filtering/filters.module.ts @@ -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], diff --git a/src/lib/filtering/popup-filter/popup-filter.component.html b/src/lib/filtering/popup-filter/popup-filter.component.html index bc55ca3..66b5ee8 100644 --- a/src/lib/filtering/popup-filter/popup-filter.component.html +++ b/src/lib/filtering/popup-filter/popup-filter.component.html @@ -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" > @@ -30,106 +30,12 @@ xPosition="before" > - - - - -
- -
- -
-
-
-
- - -
+
- - - {{ filter?.label }} - - - -
-
-
-
-
-
-
-
- - -
-
- - -
- -
 
- - - - - - -
- -
-
- - - - - -
-
-
diff --git a/src/lib/filtering/popup-filter/popup-filter.component.scss b/src/lib/filtering/popup-filter/popup-filter.component.scss index 2e05059..add3363 100644 --- a/src/lib/filtering/popup-filter/popup-filter.component.scss +++ b/src/lib/filtering/popup-filter/popup-filter.component.scss @@ -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; } diff --git a/src/lib/filtering/popup-filter/popup-filter.component.ts b/src/lib/filtering/popup-filter/popup-filter.component.ts index 79e7431..50573b4 100644 --- a/src/lib/filtering/popup-filter/popup-filter.component.ts +++ b/src/lib/filtering/popup-filter/popup-filter.component.ts @@ -1,47 +1,27 @@ 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(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; @Input() actionsTemplate?: TemplateRef; @Input() secondaryFiltersSlug = ''; - @Input() primaryFiltersLabel: string = _('filter-menu.filter-types'); + @Input() primaryFiltersLabel?: string; - atLeastOneFilterIsExpandable$?: Observable; - atLeastOneSecondaryFilterIsExpandable$?: Observable; hasActiveFilters$?: Observable; + readonly expanded = new BehaviorSubject(false); readonly expanded$ = this.expanded.asObservable().pipe(delay(200)); @@ -82,66 +62,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(); } } From bdf3fc87ed52a13cf55175e0fd1a33e17dd0930c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adina=20=C8=9Aeudan?= Date: Fri, 22 Jul 2022 01:13:43 +0300 Subject: [PATCH 2/4] Form field change detection --- src/lib/inputs/form-field/form-field-component.directive.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/lib/inputs/form-field/form-field-component.directive.ts b/src/lib/inputs/form-field/form-field-component.directive.ts index 08ae8e4..db7b09e 100644 --- a/src/lib/inputs/form-field/form-field-component.directive.ts +++ b/src/lib/inputs/form-field/form-field-component.directive.ts @@ -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 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 implements ControlValueAccessor, Val writeValue(option: I): void { this._value = option; + this._changeRef.markForCheck(); } validate(): ValidationErrors | null { From 047fbcf7f1503e28f7f3fe6aeb8549b41ff7d82d Mon Sep 17 00:00:00 2001 From: Dan Percic Date: Fri, 22 Jul 2022 07:36:03 +0300 Subject: [PATCH 3/4] fix filter label --- src/lib/filtering/popup-filter/popup-filter.component.html | 4 ++-- src/lib/filtering/popup-filter/popup-filter.component.ts | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/lib/filtering/popup-filter/popup-filter.component.html b/src/lib/filtering/popup-filter/popup-filter.component.html index 66b5ee8..ec41649 100644 --- a/src/lib/filtering/popup-filter/popup-filter.component.html +++ b/src/lib/filtering/popup-filter/popup-filter.component.html @@ -5,7 +5,7 @@ [class.disabled]="primaryFiltersDisabled$ | async" [disabled]="primaryFiltersDisabled$ | async" [icon]="primaryGroup.icon" - [label]="primaryGroup.label.capitalize() || ('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.capitalize() || ('filter-menu.label' | translate)" + [label]="primaryGroup.label?.capitalize() || ('filter-menu.label' | translate)" [matMenuTriggerFor]="filterMenu" [showDot]="hasActiveFilters$ | async" > diff --git a/src/lib/filtering/popup-filter/popup-filter.component.ts b/src/lib/filtering/popup-filter/popup-filter.component.ts index 50573b4..0270e9a 100644 --- a/src/lib/filtering/popup-filter/popup-filter.component.ts +++ b/src/lib/filtering/popup-filter/popup-filter.component.ts @@ -6,6 +6,7 @@ import { FilterService } from '../filter.service'; import { IFilterGroup } from '../models/filter-group.model'; import { SearchService } from '../../search'; import { Filter, IFilter } from '..'; +import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; @Component({ selector: 'iqser-popup-filter [primaryFiltersSlug]', @@ -18,7 +19,7 @@ export class PopupFilterComponent implements OnInit { @Input() primaryFiltersSlug!: string; @Input() actionsTemplate?: TemplateRef; @Input() secondaryFiltersSlug = ''; - @Input() primaryFiltersLabel?: string; + @Input() primaryFiltersLabel: string = _('filter-menu.filter-types'); hasActiveFilters$?: Observable; From 7875f99a074704eaa3252258d7efd83c39a355b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adina=20=C8=9Aeudan?= Date: Fri, 22 Jul 2022 10:52:26 +0300 Subject: [PATCH 4/4] Entities can have numbers as ids --- src/lib/listing/models/entity.model.ts | 3 ++- src/lib/listing/models/trackable.ts | 4 ++- src/lib/listing/services/entities.service.ts | 11 ++++---- src/lib/services/entities-map.service.ts | 27 ++++++++++---------- src/lib/utils/functions.ts | 4 +-- 5 files changed, 27 insertions(+), 22 deletions(-) diff --git a/src/lib/listing/models/entity.model.ts b/src/lib/listing/models/entity.model.ts index b8fc65f..fb4d63a 100644 --- a/src/lib/listing/models/entity.model.ts +++ b/src/lib/listing/models/entity.model.ts @@ -1,7 +1,8 @@ import { IListable } from './listable'; +import { Id } from './trackable'; export abstract class Entity implements IListable { - abstract readonly id: string; + abstract readonly id: Id; abstract readonly routerLink?: string; abstract readonly searchKey: string; diff --git a/src/lib/listing/models/trackable.ts b/src/lib/listing/models/trackable.ts index b752b41..66c3fb4 100644 --- a/src/lib/listing/models/trackable.ts +++ b/src/lib/listing/models/trackable.ts @@ -1,3 +1,5 @@ +export type Id = string | number; + export interface ITrackable { - readonly id: string; + readonly id: Id; } diff --git a/src/lib/listing/services/entities.service.ts b/src/lib/listing/services/entities.service.ts index 218d49d..ab843a6 100644 --- a/src/lib/listing/services/entities.service.ts +++ b/src/lib/listing/services/entities.service.ts @@ -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 extends ); } - getEntityChanged$(entityId: string): Observable { + getEntityChanged$(entityId: Id): Observable { return this._entityChanged$.pipe( filter(entity => entity.id === entityId), startWith(this.find(entityId)), @@ -55,7 +56,7 @@ export class EntitiesService extends ); } - getEntityDeleted$(entityId: string): Observable { + getEntityDeleted$(entityId: Id): Observable { return this._entityDeleted$.pipe(filter(entity => entity.id === entityId)); } @@ -88,7 +89,7 @@ export class EntitiesService 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 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); } diff --git a/src/lib/services/entities-map.service.ts b/src/lib/services/entities-map.service.ts index 220d47f..4642c15 100644 --- a/src/lib/services/entities-map.service.ts +++ b/src/lib/services/entities-map.service.ts @@ -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, I> { protected abstract readonly _primaryKey: string; - protected readonly _map = new Map>(); + protected readonly _map = new Map>(); readonly #entityChanged$ = new Subject(); readonly #entityDeleted$ = new Subject(); @@ -16,11 +17,11 @@ export abstract class EntitiesMapService, 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([])); } @@ -28,13 +29,13 @@ export abstract class EntitiesMapService, 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, 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(entities)); return entities.forEach(entity => this.#entityChanged$.next(entity)); @@ -96,7 +97,7 @@ export abstract class EntitiesMapService, I> { } @Validate() - watch$(@RequiredParam() key: string, @RequiredParam() entityId: string): Observable { + watch$(@RequiredParam() key: Id, @RequiredParam() entityId: Id): Observable { return this.#entityChanged$.pipe( filter(entity => entity.id === entityId), startWith(this.get(key, entityId) as E), @@ -104,17 +105,17 @@ export abstract class EntitiesMapService, I> { ); } - watchDeleted$(entityId: string): Observable { + watchDeleted$(entityId: Id): Observable { 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 { + private _getBehaviourSubject(key: Id): BehaviorSubject { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion return this._map.get(key)!; } diff --git a/src/lib/utils/functions.ts b/src/lib/utils/functions.ts index f7c5408..510f3ad 100644 --- a/src/lib/utils/functions.ts +++ b/src/lib/utils/functions.ts @@ -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() { - return (_index: number, item: T): string => item.id; + return (_index: number, item: T): Id => item.id; } export function hasFormChanged(form: UntypedFormGroup, initialFormValue: Record): boolean {