From 3e400e6dd2f0b6cdace9a6e38847f78655302b07 Mon Sep 17 00:00:00 2001 From: Dan Percic Date: Thu, 8 Jul 2021 01:59:09 +0300 Subject: [PATCH] split listing component into services --- .../dossier-listing-screen.component.html | 15 +-- .../dossier-listing-screen.component.ts | 114 +++++++++++------- .../dossier-overview-screen.component.html | 4 - .../dossier-overview-screen.component.ts | 4 +- .../shared/base/base-listing.component.ts | 13 +- .../shared/base/new-base-listing.component.ts | 69 +++++++++++ .../model/filter-wrapper.model.ts | 8 ++ .../popup-filter/utils/filter-utils.ts | 14 +-- .../page-header/page-header.component.html | 5 +- .../page-header/page-header.component.ts | 45 +++---- .../modules/shared/services/filter.service.ts | 47 ++++++++ .../shared/services/screen-state.service.ts | 103 ++++++++++++++++ .../modules/shared/services/search.service.ts | 60 +++++++++ 13 files changed, 398 insertions(+), 103 deletions(-) create mode 100644 apps/red-ui/src/app/modules/shared/base/new-base-listing.component.ts create mode 100644 apps/red-ui/src/app/modules/shared/components/filters/popup-filter/model/filter-wrapper.model.ts create mode 100644 apps/red-ui/src/app/modules/shared/services/filter.service.ts create mode 100644 apps/red-ui/src/app/modules/shared/services/screen-state.service.ts create mode 100644 apps/red-ui/src/app/modules/shared/services/search.service.ts diff --git a/apps/red-ui/src/app/modules/dossier/screens/dossier-listing-screen/dossier-listing-screen.component.html b/apps/red-ui/src/app/modules/dossier/screens/dossier-listing-screen/dossier-listing-screen.component.html index a45e9638e..89ce05585 100644 --- a/apps/red-ui/src/app/modules/dossier/screens/dossier-listing-screen/dossier-listing-screen.component.html +++ b/apps/red-ui/src/app/modules/dossier/screens/dossier-listing-screen/dossier-listing-screen.component.html @@ -1,11 +1,7 @@
@@ -17,7 +13,7 @@ {{ 'dossier-listing.table-header.title' - | translate: { length: displayedEntities.length || 0 } + | translate: { length: (displayed$ | async)?.length || 0 } }} @@ -54,14 +50,14 @@ @@ -69,7 +65,8 @@
+ extends NewBaseListingComponent implements OnInit, OnDestroy, OnAttach, OnDetach { dossiersChartData: DoughnutChartConfig[] = []; @@ -61,7 +65,6 @@ export class DossierListingScreenComponent readonly itemSize = 85; - protected readonly _searchKey = 'name'; protected readonly _sortKey = 'dossier-listing'; private _dossierAutoUpdateTimer: Subscription; @@ -85,23 +88,26 @@ export class DossierListingScreenComponent ) { super(_injector); this._appStateService.reset(); + this._searchService.searchKey = 'name'; this._loadEntitiesFromState(); } - get noData() { - return this.allEntities.length === 0; + get activeDossiersCount(): number { + return this._screenStateService.entities.filter( + p => p.dossier.status === Dossier.StatusEnum.ACTIVE + ).length; } - get user() { - return this._userService.user; + get inactiveDossiersCount(): number { + return this._screenStateService.entities.length - this.activeDossiersCount; } - get activeDossiersCount() { - return this.allEntities.filter(p => p.dossier.status === Dossier.StatusEnum.ACTIVE).length; + get displayed$(): Observable { + return this._screenStateService.displayedEntities$; } - get inactiveDossiersCount() { - return this.allEntities.length - this.activeDossiersCount; + get entities$(): Observable { + return this._screenStateService.entities$; } protected get _filters(): { @@ -129,32 +135,37 @@ export class DossierListingScreenComponent } ngOnInit(): void { - this._calculateData(); - - this._dossierAutoUpdateTimer = timer(0, 10000) - .pipe( - tap(async () => { - await this._appStateService.loadAllDossiers(); - this._loadEntitiesFromState(); - }) - ) - .subscribe(); - - this._fileChangedSub = this._appStateService.fileChanged.subscribe(() => { + try { this._calculateData(); - }); - this._routerEventsScrollPositionSub = this._router.events - .pipe( - filter( - events => events instanceof NavigationStart || events instanceof NavigationEnd + this._dossierAutoUpdateTimer = timer(0, 10000) + .pipe( + tap(async () => { + await this._appStateService.loadAllDossiers(); + this._loadEntitiesFromState(); + }) ) - ) - .subscribe(event => { - if (event instanceof NavigationStart && event.url !== '/main/dossiers') { - this._lastScrollPosition = this.scrollViewport.measureScrollOffset('top'); - } + .subscribe(); + + this._fileChangedSub = this._appStateService.fileChanged.subscribe(() => { + this._calculateData(); }); + + this._routerEventsScrollPositionSub = this._router.events + .pipe( + filter( + events => + events instanceof NavigationStart || events instanceof NavigationEnd + ) + ) + .subscribe(event => { + if (event instanceof NavigationStart && event.url !== '/main/dossiers') { + this._lastScrollPosition = this.scrollViewport.measureScrollOffset('top'); + } + }); + } catch (e) { + console.log(e); + } } ngOnAttach() { @@ -207,6 +218,10 @@ export class DossierListingScreenComponent this._calculateData(); } + filtersChanged(event) { + this._filterService.filtersChanged(event); + } + protected _preFilter() { this.detailsContainerFilters = { statusFilters: this.statusFilters.map(f => ({ ...f })) @@ -214,12 +229,18 @@ export class DossierListingScreenComponent } private _loadEntitiesFromState() { - this.allEntities = this._appStateService.allDossiers; + this._screenStateService.setEntities(this._appStateService.allDossiers); + } + + private get _user() { + return this._userService.user; } private _calculateData() { this._computeAllFilters(); - this._filterEntities(); + this._filterService.filters = this._filters; + this._filterService.preFilter = () => this._preFilter(); + this._filterService.filterEntities(); this.dossiersChartData = [ { value: this.activeDossiersCount, color: 'ACTIVE', label: 'active' }, { value: this.inactiveDossiersCount, color: 'DELETED', label: 'archived' } @@ -245,7 +266,8 @@ export class DossierListingScreenComponent const allDistinctPeople = new Set(); const allDistinctNeedsWork = new Set(); const allDistinctDossierTemplates = new Set(); - this.allEntities.forEach(entry => { + this._screenStateService.logCurrentState(); + this._screenStateService?.entities?.forEach(entry => { // all people entry.dossier.memberIds.forEach(f => allDistinctPeople.add(f)); // file statuses @@ -342,24 +364,24 @@ export class DossierListingScreenComponent private _createQuickFilters() { const filters: FilterModel[] = [ { - key: this.user.id, + key: this._user.id, label: this._translateService.instant('dossier-listing.quick-filters.my-dossiers'), - checker: (dw: DossierWrapper) => dw.ownerId === this.user.id + checker: (dw: DossierWrapper) => dw.ownerId === this._user.id }, { - key: this.user.id, + key: this._user.id, label: this._translateService.instant('dossier-listing.quick-filters.to-approve'), - checker: (dw: DossierWrapper) => dw.approverIds.includes(this.user.id) + checker: (dw: DossierWrapper) => dw.approverIds.includes(this._user.id) }, { - key: this.user.id, + key: this._user.id, label: this._translateService.instant('dossier-listing.quick-filters.to-review'), - checker: (dw: DossierWrapper) => dw.memberIds.includes(this.user.id) + checker: (dw: DossierWrapper) => dw.memberIds.includes(this._user.id) }, { - key: this.user.id, + key: this._user.id, label: this._translateService.instant('dossier-listing.quick-filters.other'), - checker: (dw: DossierWrapper) => !dw.memberIds.includes(this.user.id) + checker: (dw: DossierWrapper) => !dw.memberIds.includes(this._user.id) } ]; diff --git a/apps/red-ui/src/app/modules/dossier/screens/dossier-overview-screen/dossier-overview-screen.component.html b/apps/red-ui/src/app/modules/dossier/screens/dossier-overview-screen/dossier-overview-screen.component.html index a0cd15405..726486399 100644 --- a/apps/red-ui/src/app/modules/dossier/screens/dossier-overview-screen/dossier-overview-screen.component.html +++ b/apps/red-ui/src/app/modules/dossier/screens/dossier-overview-screen/dossier-overview-screen.component.html @@ -1,11 +1,7 @@
diff --git a/apps/red-ui/src/app/modules/dossier/screens/dossier-overview-screen/dossier-overview-screen.component.ts b/apps/red-ui/src/app/modules/dossier/screens/dossier-overview-screen/dossier-overview-screen.component.ts index 8eba4f95a..9ad1a7e6f 100644 --- a/apps/red-ui/src/app/modules/dossier/screens/dossier-overview-screen/dossier-overview-screen.component.ts +++ b/apps/red-ui/src/app/modules/dossier/screens/dossier-overview-screen/dossier-overview-screen.component.ts @@ -40,11 +40,13 @@ import { FilterModel } from '@shared/components/filters/popup-filter/model/filte import { AppConfigKey, AppConfigService } from '../../../app-config/app-config.service'; import { FilterConfig } from '@shared/components/page-header/models/filter-config.model'; import { ActionConfig } from '@shared/components/page-header/models/action-config.model'; +import { FilterService } from '../../../shared/services/filter.service'; @Component({ selector: 'redaction-dossier-overview-screen', templateUrl: './dossier-overview-screen.component.html', - styleUrls: ['./dossier-overview-screen.component.scss'] + styleUrls: ['./dossier-overview-screen.component.scss'], + providers: [FilterService] }) export class DossierOverviewScreenComponent extends BaseListingComponent diff --git a/apps/red-ui/src/app/modules/shared/base/base-listing.component.ts b/apps/red-ui/src/app/modules/shared/base/base-listing.component.ts index d2a1c65a3..9aec7b702 100644 --- a/apps/red-ui/src/app/modules/shared/base/base-listing.component.ts +++ b/apps/red-ui/src/app/modules/shared/base/base-listing.component.ts @@ -1,11 +1,14 @@ import { ChangeDetectorRef, Component, Injector, ViewChild } from '@angular/core'; import { FormBuilder, FormGroup } from '@angular/forms'; -import { debounce } from '@utils/debounce'; import { ScreenName, SortingOption, SortingService } from '@services/sorting.service'; import { FilterModel } from '../components/filters/popup-filter/model/filter.model'; -import { getFilteredEntities } from '../components/filters/popup-filter/utils/filter-utils'; import { QuickFiltersComponent } from '../components/filters/quick-filters/quick-filters.component'; import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling'; +import { FilterService } from '../services/filter.service'; +import { SearchService } from '../services/search.service'; +import { ScreenStateService } from '../services/screen-state.service'; +import { getFilteredEntities } from '../components/filters/popup-filter/utils/filter-utils'; +import { debounce } from '../../../utils/debounce'; // Functionalities: Filter, search, select, sort @@ -24,6 +27,9 @@ export abstract class BaseListingComponent { protected readonly _formBuilder: FormBuilder; protected readonly _changeDetectorRef: ChangeDetectorRef; protected readonly _sortingService: SortingService; + protected readonly _filterService: FilterService; + protected readonly _searchService: SearchService; + protected readonly _screenStateService: ScreenStateService; // ---- // Overwrite in child class: @@ -40,6 +46,9 @@ export abstract class BaseListingComponent { this._formBuilder = this._injector.get(FormBuilder); this._changeDetectorRef = this._injector.get(ChangeDetectorRef); this._sortingService = this._injector.get(SortingService); + this._filterService = this._injector.get>(FilterService); + this._searchService = this._injector.get>(SearchService); + this._screenStateService = this._injector.get>(ScreenStateService); } get areAllEntitiesSelected() { diff --git a/apps/red-ui/src/app/modules/shared/base/new-base-listing.component.ts b/apps/red-ui/src/app/modules/shared/base/new-base-listing.component.ts new file mode 100644 index 000000000..a3e0f4e24 --- /dev/null +++ b/apps/red-ui/src/app/modules/shared/base/new-base-listing.component.ts @@ -0,0 +1,69 @@ +import { ChangeDetectorRef, Component, Injector, ViewChild } from '@angular/core'; +import { ScreenName, SortingOption, SortingService } from '@services/sorting.service'; +import { FilterModel } from '../components/filters/popup-filter/model/filter.model'; +import { QuickFiltersComponent } from '../components/filters/quick-filters/quick-filters.component'; +import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling'; +import { FilterService } from '../services/filter.service'; +import { SearchService } from '../services/search.service'; +import { ScreenStateService } from '../services/screen-state.service'; + +@Component({ template: '' }) +export abstract class NewBaseListingComponent { + @ViewChild(CdkVirtualScrollViewport) scrollViewport: CdkVirtualScrollViewport; + + protected readonly _changeDetectorRef: ChangeDetectorRef; + protected readonly _sortingService: SortingService; + protected readonly _filterService: FilterService; + protected readonly _searchService: SearchService; + protected readonly _screenStateService: ScreenStateService; + + // ---- + // Overwrite in child class: + protected readonly _sortKey: ScreenName; + // Overwrite this in ngOnInit + @ViewChild(QuickFiltersComponent) + protected _quickFilters: QuickFiltersComponent; + + protected constructor(protected readonly _injector: Injector) { + this._changeDetectorRef = this._injector.get(ChangeDetectorRef); + this._sortingService = this._injector.get(SortingService); + this._filterService = this._injector.get>(FilterService); + this._searchService = this._injector.get>(SearchService); + this._screenStateService = this._injector.get>(ScreenStateService); + } + + get sortingOption(): SortingOption { + return this._sortingService.getSortingOption(this._getSortKey); + } + + protected get _filters(): { + values: FilterModel[]; + checker: Function; + matchAll?: boolean; + checkerArgs?: any; + }[] { + return []; + } + + private get _getSortKey(): ScreenName { + if (!this._sortKey) throw new Error('Not implemented'); + + return this._sortKey; + } + + resetFilters() { + this._quickFilters.deactivateFilters(); + this._filterService.reset(); + } + + toggleSort($event) { + this._sortingService.toggleSort(this._getSortKey, $event); + } + + // protected _filterEntities() { + // this._preFilter(); + // this.filteredEntities = getFilteredEntities(this.allEntities, this._filters); + // this.executeSearch(this._searchValue); + // this._changeDetectorRef.detectChanges(); + // } +} diff --git a/apps/red-ui/src/app/modules/shared/components/filters/popup-filter/model/filter-wrapper.model.ts b/apps/red-ui/src/app/modules/shared/components/filters/popup-filter/model/filter-wrapper.model.ts new file mode 100644 index 000000000..9e1a890c5 --- /dev/null +++ b/apps/red-ui/src/app/modules/shared/components/filters/popup-filter/model/filter-wrapper.model.ts @@ -0,0 +1,8 @@ +import { FilterModel } from './filter.model'; + +export interface FilterWrapper { + values: FilterModel[]; + checker: Function; + matchAll?: boolean; + checkerArgs?: any; +} diff --git a/apps/red-ui/src/app/modules/shared/components/filters/popup-filter/utils/filter-utils.ts b/apps/red-ui/src/app/modules/shared/components/filters/popup-filter/utils/filter-utils.ts index 1eb819636..b25d99598 100644 --- a/apps/red-ui/src/app/modules/shared/components/filters/popup-filter/utils/filter-utils.ts +++ b/apps/red-ui/src/app/modules/shared/components/filters/popup-filter/utils/filter-utils.ts @@ -1,7 +1,8 @@ import { FilterModel } from '../model/filter.model'; -import { FileStatusWrapper } from '../../../../../../models/file/file-status.wrapper'; -import { DossierWrapper } from '../../../../../../state/model/dossier.wrapper'; -import { PermissionsService } from '../../../../../../services/permissions.service'; +import { FileStatusWrapper } from '@models/file/file-status.wrapper'; +import { DossierWrapper } from '@state/model/dossier.wrapper'; +import { PermissionsService } from '@services/permissions.service'; +import { FilterWrapper } from '@shared/components/filters/popup-filter/model/filter-wrapper.model'; export function processFilters(oldFilters: FilterModel[], newFilters: FilterModel[]) { copySettings(oldFilters, newFilters); @@ -175,11 +176,8 @@ export const addedDateChecker = (dw: DossierWrapper, filter: FilterModel) => export const dossierApproverChecker = (dw: DossierWrapper, filter: FilterModel) => dw.approverIds.includes(filter.key); -export function getFilteredEntities( - entities: any[], - filters: { values: FilterModel[]; checker: Function; matchAll?: boolean; checkerArgs?: any }[] -) { - const filteredEntities = []; +export function getFilteredEntities(entities: T[], filters: FilterWrapper[]) { + const filteredEntities: T[] = []; for (const entity of entities) { let add = true; for (const filter of filters) { diff --git a/apps/red-ui/src/app/modules/shared/components/page-header/page-header.component.html b/apps/red-ui/src/app/modules/shared/components/page-header/page-header.component.html index c953293a9..3e960cc89 100644 --- a/apps/red-ui/src/app/modules/shared/components/page-header/page-header.component.html +++ b/apps/red-ui/src/app/modules/shared/components/page-header/page-header.component.html @@ -6,7 +6,7 @@ @@ -50,6 +50,7 @@ > + { @Input() pageLabel: string; @Input() showCloseButton: boolean; - @Input() showResetFilters: boolean; @Input() filterConfigs: FilterConfig[]; @Input() actionConfigs: ActionConfig[]; @Input() buttonConfigs: ButtonConfig[]; @Input() searchPlaceholder: string; - @Output() filtersChanged = new EventEmitter<{ - primary: FilterModel[]; - secondary?: FilterModel[]; - }>(); - @Output() filtersReset = new EventEmitter(); - @Output() searchChanged = new EventEmitter(); - - readonly searchForm = this._formBuilder.group({ - query: [''] - }); @ViewChildren(PopupFilterComponent) private readonly _filterComponents: QueryList; constructor( readonly permissionsService: PermissionsService, - private readonly _formBuilder: FormBuilder - ) { - this.searchForm.valueChanges.subscribe(value => this.searchChanged.emit(value.query)); - } + readonly filterService: FilterService, + readonly searchService: SearchService + ) {} get hasActiveFilters() { const hasActiveFilters = this._filterComponents?.reduce( (acc, component) => acc || component?.hasActiveFilters, false ); - return hasActiveFilters || this.searchForm.get('query').value || this.showResetFilters; + return ( + hasActiveFilters || + this.searchService.searchValue || + this.filterService.showResetFilters + ); } resetFilters() { + this.filterService.reset(); this._filterComponents.forEach(component => component?.deactivateFilters()); - - this.filtersReset.emit(); - this.searchForm.reset({ query: '' }); + this.searchService.reset(); } trackByLabel(index: number, item: BaseHeaderConfig) { diff --git a/apps/red-ui/src/app/modules/shared/services/filter.service.ts b/apps/red-ui/src/app/modules/shared/services/filter.service.ts new file mode 100644 index 000000000..ef99f938a --- /dev/null +++ b/apps/red-ui/src/app/modules/shared/services/filter.service.ts @@ -0,0 +1,47 @@ +import { ChangeDetectorRef, Injectable } from '@angular/core'; +import { FilterModel } from '@shared/components/filters/popup-filter/model/filter.model'; +import { getFilteredEntities } from '@shared/components/filters/popup-filter/utils/filter-utils'; +import { FilterWrapper } from '@shared/components/filters/popup-filter/model/filter-wrapper.model'; +import { ScreenStateService } from '@shared/services/screen-state.service'; +import { SearchService } from '@shared/services/search.service'; + +@Injectable() +export class FilterService { + showResetFilters = false; + preFilter: () => void; + filters: FilterWrapper[]; + + constructor( + private readonly _screenStateService: ScreenStateService, + private readonly _searchService: SearchService, + private readonly _changeDetector: ChangeDetectorRef + ) {} + + filtersChanged(filters?: { [key: string]: FilterModel[] } | FilterModel[]): void { + console.log(filters); + if (filters instanceof Array) this.showResetFilters = !!filters.find(f => f.checked); + else + for (const key of Object.keys(filters ?? {})) { + for (let idx = 0; idx < this[key]?.length; ++idx) { + this[key][idx] = filters[key][idx]; + } + } + + this.filterEntities(); + } + + filterEntities(): void { + if (this.preFilter) this.preFilter(); + this._screenStateService.setFilteredEntities( + getFilteredEntities(this._screenStateService.entities, this.filters) + ); + this._searchService.executeSearchImmediately(); + this._changeDetector.detectChanges(); + } + + reset(): void { + // this._quickFilters.deactivateFilters(); + this.showResetFilters = false; + this.filtersChanged(); + } +} diff --git a/apps/red-ui/src/app/modules/shared/services/screen-state.service.ts b/apps/red-ui/src/app/modules/shared/services/screen-state.service.ts new file mode 100644 index 000000000..fa0e5d14f --- /dev/null +++ b/apps/red-ui/src/app/modules/shared/services/screen-state.service.ts @@ -0,0 +1,103 @@ +import { Injectable } from '@angular/core'; +import { BehaviorSubject } from 'rxjs'; + +@Injectable() +export class ScreenStateService { + entities$ = new BehaviorSubject>([]); + filteredEntities$ = new BehaviorSubject([]); + displayedEntities$ = new BehaviorSubject([]); + selectedEntitiesIds: string[] = []; + + private _selectionKey: string; + get entities(): T[] { + return Object.values(this.entities$.getValue()); + } + + get filteredEntities(): T[] { + return Object.values(this.filteredEntities$.getValue()); + } + + get displayedEntities(): T[] { + return Object.values(this.displayedEntities$.getValue()); + } + // + // select(mapFn: (state: T[]) => K): Observable { + // return this.entities$.asObservable().pipe( + // map((state: T[]) => mapFn(state)), + // distinctUntilChanged() + // ); + // } + + setEntities(newEntities: Partial) { + this.entities$.next(newEntities); + } + + setFilteredEntities(newEntities: Partial) { + this.filteredEntities$.next(newEntities); + } + + setDisplayedEntities(newEntities: Partial) { + console.log(this.displayedEntities); + this.displayedEntities$.next(newEntities); + console.log(this.displayedEntities); + } + + set selectionKey(value: string) { + this._selectionKey = value; + } + + get areAllEntitiesSelected() { + return ( + this.displayedEntities.length !== 0 && + this.selectedEntitiesIds.length === this.displayedEntities.length + ); + } + + get areSomeEntitiesSelected() { + return this.selectedEntitiesIds.length > 0; + } + + isSelected(entity: T) { + return this.selectedEntitiesIds.indexOf(entity[this._getSelectionKey]) !== -1; + } + + toggleEntitySelected(entity: T) { + const idx = this.selectedEntitiesIds.indexOf(entity[this._getSelectionKey]); + if (idx === -1) { + this.selectedEntitiesIds.push(entity[this._getSelectionKey]); + } else { + this.selectedEntitiesIds.splice(idx, 1); + } + } + + toggleSelectAll() { + if (this.areSomeEntitiesSelected) { + this.selectedEntitiesIds = []; + } else { + this.selectedEntitiesIds = this.displayedEntities.map( + entity => entity[this._getSelectionKey] + ); + } + } + + updateSelection() { + if (this._selectionKey) { + this.selectedEntitiesIds = this.displayedEntities + .map(entity => entity[this._getSelectionKey]) + .filter(id => this.selectedEntitiesIds.includes(id)); + } + } + + logCurrentState() { + console.log('Entities', this.entities); + console.log('Displayed', this.displayedEntities$.getValue()); + console.log('Filtered', this.filteredEntities$.getValue()); + console.log('Selected', this.selectedEntitiesIds); + } + + private get _getSelectionKey(): string { + if (!this._selectionKey) throw new Error('Not implemented'); + + return this._selectionKey; + } +} diff --git a/apps/red-ui/src/app/modules/shared/services/search.service.ts b/apps/red-ui/src/app/modules/shared/services/search.service.ts new file mode 100644 index 000000000..f2e4e94c2 --- /dev/null +++ b/apps/red-ui/src/app/modules/shared/services/search.service.ts @@ -0,0 +1,60 @@ +import { Injectable } from '@angular/core'; +import { debounce } from '@utils/debounce'; +import { ScreenStateService } from '@shared/services/screen-state.service'; +import { FormBuilder } from '@angular/forms'; + +@Injectable() +export class SearchService { + private _searchValue = ''; + private _searchKey: string; + + readonly searchForm = this._formBuilder.group({ + query: [''] + }); + + constructor( + private readonly _screenStateService: ScreenStateService, + private readonly _formBuilder: FormBuilder + ) { + this.searchForm.valueChanges.subscribe(() => this.executeSearch()); + } + + @debounce(200) + executeSearch(): void { + this._searchValue = this.searchValue; + return this.executeSearchImmediately(); + } + + executeSearchImmediately(): void { + const displayed = ( + this._screenStateService.filteredEntities ?? this._screenStateService.entities + ).filter(entity => + this._searchField(entity).toLowerCase().includes(this._searchValue.toLowerCase()) + ); + + this._screenStateService.setDisplayedEntities(displayed); + this._screenStateService.updateSelection(); + } + + set searchKey(value: string) { + this._searchKey = value; + } + + get searchValue(): string { + return this.searchForm.get('query').value; + } + + reset() { + this.searchForm.reset({ query: '' }); + } + + private get _getSearchKey(): string { + if (!this._searchKey) throw new Error('Not implemented'); + + return this._searchKey; + } + + protected _searchField(entity: T): string { + return entity[this._getSearchKey]; + } +}