split listing component into services

This commit is contained in:
Dan Percic 2021-07-08 01:59:09 +03:00
parent 7990636066
commit 3e400e6dd2
13 changed files with 398 additions and 103 deletions

View File

@ -1,11 +1,7 @@
<section>
<redaction-page-header
(filtersChanged)="filtersChanged($event)"
(filtersReset)="resetFilters()"
(searchChanged)="executeSearch($event)"
[filterConfigs]="filterConfigs"
[buttonConfigs]="buttonConfigs"
[showResetFilters]="showResetFilters"
[searchPlaceholder]="'dossier-listing.search' | translate"
></redaction-page-header>
@ -17,7 +13,7 @@
<span class="all-caps-label">
{{
'dossier-listing.table-header.title'
| translate: { length: displayedEntities.length || 0 }
| translate: { length: (displayed$ | async)?.length || 0 }
}}
</span>
@ -54,14 +50,14 @@
<redaction-empty-state
(action)="openAddDossierDialog()"
*ngIf="!allEntities.length"
*ngIf="(entities$ | async)?.length === 0"
[showButton]="permissionsService.isManager()"
icon="red:folder"
screen="dossier-listing"
></redaction-empty-state>
<redaction-empty-state
*ngIf="allEntities.length && !displayedEntities.length"
*ngIf="(entities$ | async)?.length && (displayed$ | async)?.length === 0"
screen="dossier-listing"
type="no-match"
></redaction-empty-state>
@ -69,7 +65,8 @@
<cdk-virtual-scroll-viewport [itemSize]="itemSize" redactionHasScrollbar>
<div
*cdkVirtualFor="
let dw of displayedEntities
let dw of displayed$
| async
| sortBy: sortingOption.order:sortingOption.column
"
[class.pointer]="canOpenDossier(dw)"
@ -145,7 +142,7 @@
<div class="right-container" redactionHasScrollbar>
<redaction-dossier-listing-details
(filtersChanged)="filtersChanged($event)"
*ngIf="allEntities.length"
*ngIf="(entities$ | async)?.length !== 0"
[documentsChartData]="documentsChartData"
[dossiersChartData]="dossiersChartData"
[filters]="detailsContainerFilters"

View File

@ -7,14 +7,13 @@ import { groupBy } from '@utils/functions';
import { TranslateService } from '@ngx-translate/core';
import { PermissionsService } from '@services/permissions.service';
import { DossierWrapper } from '@state/model/dossier.wrapper';
import { Subscription, timer } from 'rxjs';
import { Observable, Subscription, timer } from 'rxjs';
import { filter, tap } from 'rxjs/operators';
import { TranslateChartService } from '@services/translate-chart.service';
import { RedactionFilterSorter } from '@utils/sorters/redaction-filter-sorter';
import { StatusSorter } from '@utils/sorters/status-sorter';
import { NavigationEnd, NavigationStart, Router } from '@angular/router';
import { DossiersDialogService } from '../../services/dossiers-dialog.service';
import { BaseListingComponent } from '@shared/base/base-listing.component';
import { OnAttach, OnDetach } from '@utils/custom-route-reuse.strategy';
import { FilterModel } from '@shared/components/filters/popup-filter/model/filter.model';
import {
@ -27,13 +26,18 @@ import {
import { UserPreferenceService } from '../../../../services/user-preference.service';
import { FilterConfig } from '../../../shared/components/page-header/models/filter-config.model';
import { ButtonConfig } from '../../../shared/components/page-header/models/button-config.model';
import { FilterService } from '../../../shared/services/filter.service';
import { SearchService } from '../../../shared/services/search.service';
import { ScreenStateService } from '../../../shared/services/screen-state.service';
import { NewBaseListingComponent } from '../../../shared/base/new-base-listing.component';
@Component({
templateUrl: './dossier-listing-screen.component.html',
styleUrls: ['./dossier-listing-screen.component.scss']
styleUrls: ['./dossier-listing-screen.component.scss'],
providers: [FilterService, SearchService, ScreenStateService]
})
export class DossierListingScreenComponent
extends BaseListingComponent<DossierWrapper>
extends NewBaseListingComponent<DossierWrapper>
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<DossierWrapper[]> {
return this._screenStateService.displayedEntities$;
}
get inactiveDossiersCount() {
return this.allEntities.length - this.activeDossiersCount;
get entities$(): Observable<DossierWrapper[]> {
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<string>();
const allDistinctNeedsWork = new Set<string>();
const allDistinctDossierTemplates = new Set<string>();
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)
}
];

View File

@ -1,11 +1,7 @@
<section *ngIf="!!activeDossier">
<redaction-page-header
(filtersChanged)="filtersChanged($event)"
(filtersReset)="resetFilters()"
(searchChanged)="executeSearch($event)"
[filterConfigs]="filterConfigs"
[actionConfigs]="actionConfigs"
[showResetFilters]="showResetFilters"
[showCloseButton]="true"
[searchPlaceholder]="'dossier-overview.search' | translate"
>

View File

@ -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<FileStatusWrapper>

View File

@ -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<T = any> {
protected readonly _formBuilder: FormBuilder;
protected readonly _changeDetectorRef: ChangeDetectorRef;
protected readonly _sortingService: SortingService;
protected readonly _filterService: FilterService<T>;
protected readonly _searchService: SearchService<T>;
protected readonly _screenStateService: ScreenStateService<T>;
// ----
// Overwrite in child class:
@ -40,6 +46,9 @@ export abstract class BaseListingComponent<T = any> {
this._formBuilder = this._injector.get<FormBuilder>(FormBuilder);
this._changeDetectorRef = this._injector.get<ChangeDetectorRef>(ChangeDetectorRef);
this._sortingService = this._injector.get<SortingService>(SortingService);
this._filterService = this._injector.get<FilterService<T>>(FilterService);
this._searchService = this._injector.get<SearchService<T>>(SearchService);
this._screenStateService = this._injector.get<ScreenStateService<T>>(ScreenStateService);
}
get areAllEntitiesSelected() {

View File

@ -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<T = any> {
@ViewChild(CdkVirtualScrollViewport) scrollViewport: CdkVirtualScrollViewport;
protected readonly _changeDetectorRef: ChangeDetectorRef;
protected readonly _sortingService: SortingService;
protected readonly _filterService: FilterService<T>;
protected readonly _searchService: SearchService<T>;
protected readonly _screenStateService: ScreenStateService<T>;
// ----
// 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>(ChangeDetectorRef);
this._sortingService = this._injector.get<SortingService>(SortingService);
this._filterService = this._injector.get<FilterService<T>>(FilterService);
this._searchService = this._injector.get<SearchService<T>>(SearchService);
this._screenStateService = this._injector.get<ScreenStateService<T>>(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();
// }
}

View File

@ -0,0 +1,8 @@
import { FilterModel } from './filter.model';
export interface FilterWrapper {
values: FilterModel[];
checker: Function;
matchAll?: boolean;
checkerArgs?: any;
}

View File

@ -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<T>(entities: T[], filters: FilterWrapper[]) {
const filteredEntities: T[] = [];
for (const entity of entities) {
let add = true;
for (const filter of filters) {

View File

@ -6,7 +6,7 @@
<ng-container *ngFor="let config of filterConfigs; trackBy: trackByLabel">
<redaction-popup-filter
(filtersChanged)="filtersChanged.emit($event)"
(filtersChanged)="filterService.filtersChanged($event)"
*ngIf="!config.hide"
[filterLabel]="config.label"
[icon]="config.icon"
@ -16,7 +16,7 @@
</ng-container>
<redaction-input-with-action
[form]="searchForm"
[form]="searchService.searchForm"
[placeholder]="searchPlaceholder"
type="search"
></redaction-input-with-action>
@ -50,6 +50,7 @@
></redaction-circle-button>
</ng-container>
<!-- Extra custom actions here -->
<ng-content></ng-content>
<redaction-circle-button

View File

@ -1,20 +1,12 @@
import {
ChangeDetectionStrategy,
Component,
EventEmitter,
Input,
Output,
QueryList,
ViewChildren
} from '@angular/core';
import { ChangeDetectionStrategy, Component, Input, QueryList, ViewChildren } from '@angular/core';
import { PermissionsService } from '@services/permissions.service';
import { FilterModel } from '@shared/components/filters/popup-filter/model/filter.model';
import { PopupFilterComponent } from '@shared/components/filters/popup-filter/popup-filter.component';
import { FormBuilder } from '@angular/forms';
import { FilterConfig } from '@shared/components/page-header/models/filter-config.model';
import { ActionConfig } from '@shared/components/page-header/models/action-config.model';
import { ButtonConfig } from '@shared/components/page-header/models/button-config.model';
import { BaseHeaderConfig } from '@shared/components/page-header/models/base-config.model';
import { FilterService } from '@shared/services/filter.service';
import { SearchService } from '@shared/services/search.service';
@Component({
selector: 'redaction-page-header',
@ -22,48 +14,39 @@ import { BaseHeaderConfig } from '@shared/components/page-header/models/base-con
styleUrls: ['./page-header.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class PageHeaderComponent {
export class PageHeaderComponent<T> {
@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<never>();
@Output() searchChanged = new EventEmitter<string>();
readonly searchForm = this._formBuilder.group({
query: ['']
});
@ViewChildren(PopupFilterComponent)
private readonly _filterComponents: QueryList<PopupFilterComponent>;
constructor(
readonly permissionsService: PermissionsService,
private readonly _formBuilder: FormBuilder
) {
this.searchForm.valueChanges.subscribe(value => this.searchChanged.emit(value.query));
}
readonly filterService: FilterService<T>,
readonly searchService: SearchService<T>
) {}
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) {

View File

@ -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<T> {
showResetFilters = false;
preFilter: () => void;
filters: FilterWrapper[];
constructor(
private readonly _screenStateService: ScreenStateService<T>,
private readonly _searchService: SearchService<T>,
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();
}
}

View File

@ -0,0 +1,103 @@
import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
@Injectable()
export class ScreenStateService<T> {
entities$ = new BehaviorSubject<Array<T>>([]);
filteredEntities$ = new BehaviorSubject<T[]>([]);
displayedEntities$ = new BehaviorSubject<T[]>([]);
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<K>(mapFn: (state: T[]) => K): Observable<K> {
// return this.entities$.asObservable().pipe(
// map((state: T[]) => mapFn(state)),
// distinctUntilChanged()
// );
// }
setEntities(newEntities: Partial<T[]>) {
this.entities$.next(newEntities);
}
setFilteredEntities(newEntities: Partial<T[]>) {
this.filteredEntities$.next(newEntities);
}
setDisplayedEntities(newEntities: Partial<T[]>) {
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;
}
}

View File

@ -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<T> {
private _searchValue = '';
private _searchKey: string;
readonly searchForm = this._formBuilder.group({
query: ['']
});
constructor(
private readonly _screenStateService: ScreenStateService<T>,
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];
}
}