use properties to hold observables, fix filter & search issues

This commit is contained in:
Dan Percic 2021-07-19 22:55:30 +03:00
parent 02b38780f6
commit 334ef37a5c
26 changed files with 196 additions and 261 deletions

View File

@ -29,7 +29,6 @@ export class ActiveFieldsListingComponent extends BaseListingComponent<Field> im
ngOnChanges(changes: SimpleChanges): void {
if (changes.entities) {
this.screenStateService.setEntities(this.entities);
this.screenStateService.setDisplayedEntities(this.entities);
this.screenStateService.updateSelection();
}
}

View File

@ -97,7 +97,6 @@ export class FileAttributesCsvImportDialogComponent extends BaseListingComponent
}
this.screenStateService.setEntities(this.parseResult.meta.fields.map(field => this._buildAttribute(field)));
this.screenStateService.setDisplayedEntities(this.allEntities);
this.activeFields = [];
for (const entity of this.allEntities) {

View File

@ -66,7 +66,6 @@ export class DefaultColorsScreenComponent
value: data[key]
}));
this.screenStateService.setEntities(entities);
this.screenStateService.setDisplayedEntities(entities);
this._loadingService.stop();
}
}

View File

@ -1,4 +1,4 @@
import { ChangeDetectionStrategy, Component, Injector, OnInit } from '@angular/core';
import { Component, Injector, OnInit } from '@angular/core';
import { DoughnutChartConfig } from '@shared/components/simple-doughnut-chart/simple-doughnut-chart.component';
import { DictionaryControllerService } from '@redaction/red-ui-http';
import { AppStateService } from '@state/app-state.service';
@ -17,7 +17,7 @@ import { BaseListingComponent } from '@shared/base/base-listing.component';
import { AdminDialogService } from '../../services/admin-dialog.service';
const toChartConfig = (dict: TypeValueWrapper): DoughnutChartConfig => ({
value: dict.entries ? dict.entries.length : 0,
value: dict.entries?.length ?? 0,
color: dict.hexColor,
label: dict.label,
key: dict.type
@ -100,8 +100,6 @@ export class DictionaryListingScreenComponent extends BaseListingComponent<TypeV
);
else this.screenStateService.setEntities(entities);
this.screenStateService.setDisplayedEntities(this.allEntities);
if (!loadEntries) return;
const dataObs = this.allEntities.map(dict =>

View File

@ -61,7 +61,6 @@ export class DossierAttributesListingScreenComponent extends BaseListingComponen
this._loadingService.start();
const attributes = await this._dossierAttributesService.getConfig();
this.screenStateService.setEntities(attributes);
this.filterService.applyFilters();
this._loadingService.stop();
}
}

View File

@ -54,7 +54,6 @@ export class DossierTemplatesListingScreenComponent extends BaseListingComponent
this._loadingService.start();
this._appStateService.reset();
this.screenStateService.setEntities(this._appStateService.dossierTemplates);
this.filterService.applyFilters();
this._loadDossierTemplateStats();
this._loadingService.stop();
}

View File

@ -91,17 +91,16 @@ export class FileAttributesListingScreenComponent extends BaseListingComponent<F
}
private async _loadData() {
this._loadingService.start();
try {
this._loadingService.start();
const response = await this._fileAttributesService
.getFileAttributesConfiguration(this._appStateService.activeDossierTemplateId)
.toPromise();
this._existingConfiguration = response;
this.screenStateService.setEntities(response?.fileAttributeConfigs || []);
} catch (e) {
} finally {
this.filterService.applyFilters();
this._loadingService.stop();
}
} catch (e) {}
this._loadingService.stop();
}
}

View File

@ -39,7 +39,6 @@ export class TrashScreenComponent extends BaseListingComponent<Dossier> implemen
this._loadingService.start();
await this.loadDossierTemplatesData();
this.filterService.applyFilters();
this._loadingService.stop();
}
@ -74,6 +73,5 @@ export class TrashScreenComponent extends BaseListingComponent<Dossier> implemen
const entities = this.screenStateService.allEntities.filter(e => !ids.includes(e.dossierId));
this.screenStateService.setEntities(entities);
this.screenStateService.setSelectedEntities([]);
this.filterService.applyFilters();
}
}

View File

@ -1,4 +1,4 @@
import { ChangeDetectionStrategy, Component, Injector, OnInit, QueryList, ViewChildren } from '@angular/core';
import { Component, Injector, OnInit, QueryList, ViewChildren } from '@angular/core';
import { PermissionsService } from '@services/permissions.service';
import { UserService } from '@services/user.service';
import { User, UserControllerService } from '@redaction/red-ui-http';
@ -19,11 +19,11 @@ import { map } from 'rxjs/operators';
@Component({
templateUrl: './user-listing-screen.component.html',
styleUrls: ['./user-listing-screen.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [FilterService, SearchService, ScreenStateService, SortingService]
})
export class UserListingScreenComponent extends BaseListingComponent<User> implements OnInit {
protected readonly _primaryKey = 'userId';
readonly canDeleteSelected$ = this._canDeleteSelected$;
collapsedDetails = false;
chartData: DoughnutChartConfig[] = [];
@ -43,7 +43,7 @@ export class UserListingScreenComponent extends BaseListingComponent<User> imple
super(_injector);
}
get canDeleteSelected$(): Observable<boolean> {
get _canDeleteSelected$(): Observable<boolean> {
const entities$ = this.screenStateService.selectedEntities$;
return entities$.pipe(map(all => all.indexOf(this.userService.user) === -1));
}
@ -86,7 +86,6 @@ export class UserListingScreenComponent extends BaseListingComponent<User> imple
private async _loadData() {
this.screenStateService.setEntities(await this._userControllerService.getAllUsers().toPromise());
await this.userService.loadAllUsers();
this.filterService.applyFilters();
this._computeStats();
this._loadingService.stop();
}

View File

@ -41,7 +41,7 @@
[config]="documentsChartData"
[radius]="63"
[strokeWidth]="15"
[subtitle]="i18nKey + 'charts.documents-in-dossier'"
[subtitle]="'dossier-overview.dossier-details.charts.documents-in-dossier'"
direction="row"
></redaction-simple-doughnut-chart>
</div>
@ -49,8 +49,8 @@
<div *ngIf="hasFiles" class="mt-24 legend pb-32">
<div
(click)="filterService.toggleFilter('needsWorkFilters', filter.key)"
*ngFor="let filter of filterService.getFilterModels$('needsWorkFilters') | async"
[class.active]="filterService.filterChecked$('needsWorkFilters', filter.key) | async"
*ngFor="let filter of needsWorkFilters$ | async"
[class.active]="filter.checked"
>
<redaction-type-filter [filter]="filter"></redaction-type-filter>
</div>

View File

@ -9,7 +9,6 @@ import { UserService } from '@services/user.service';
import { User } from '@redaction/red-ui-http';
import { Toaster } from '../../../../services/toaster.service';
import { FilterService } from '../../../shared/services/filter.service';
import { FileStatusWrapper } from '../../../../models/file/file-status.wrapper';
import { DossierAttributeWithValue } from '@models/dossier-attributes.model';
@Component({
@ -18,7 +17,6 @@ import { DossierAttributeWithValue } from '@models/dossier-attributes.model';
styleUrls: ['./dossier-details.component.scss']
})
export class DossierDetailsComponent implements OnInit {
readonly i18nKey = 'dossier-overview.dossier-details.';
documentsChartData: DoughnutChartConfig[] = [];
owner: User;
editingOwner = false;
@ -27,11 +25,13 @@ export class DossierDetailsComponent implements OnInit {
@Output() openDossierDictionaryDialog = new EventEmitter();
@Output() toggleCollapse = new EventEmitter();
readonly needsWorkFilters$ = this.filterService.getFilterModels$('needsWorkFilters');
constructor(
readonly appStateService: AppStateService,
readonly translateChartService: TranslateChartService,
readonly permissionsService: PermissionsService,
readonly filterService: FilterService<FileStatusWrapper>,
readonly filterService: FilterService,
private readonly _changeDetectorRef: ChangeDetectorRef,
private readonly _userService: UserService,
private readonly _toaster: Toaster

View File

@ -9,9 +9,9 @@ import { FilterService } from '@shared/services/filter.service';
styleUrls: ['./dossier-listing-details.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class DossierListingDetailsComponent<T> {
export class DossierListingDetailsComponent {
@Input() dossiersChartData: DoughnutChartConfig[];
@Input() documentsChartData: DoughnutChartConfig[];
constructor(readonly appStateService: AppStateService, readonly filterService: FilterService<T>) {}
constructor(readonly appStateService: AppStateService, readonly filterService: FilterService) {}
}

View File

@ -1,11 +1,10 @@
import { Component, Injector, OnDestroy, OnInit, TemplateRef, ViewChild } from '@angular/core';
import { AfterViewInit, ChangeDetectorRef, Component, Injector, OnDestroy, OnInit, TemplateRef, ViewChild } from '@angular/core';
import { Dossier, DossierTemplateModel } from '@redaction/red-ui-http';
import { AppStateService } from '@state/app-state.service';
import { UserService } from '@services/user.service';
import { DoughnutChartConfig } from '@shared/components/simple-doughnut-chart/simple-doughnut-chart.component';
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 { timer } from 'rxjs';
import { filter } from 'rxjs/operators';
@ -38,7 +37,10 @@ const isLeavingScreen = event => event instanceof NavigationStart && event.url !
styleUrls: ['./dossier-listing-screen.component.scss'],
providers: [FilterService, SearchService, ScreenStateService, SortingService]
})
export class DossierListingScreenComponent extends BaseListingComponent<DossierWrapper> implements OnInit, OnDestroy, OnAttach, OnDetach {
export class DossierListingScreenComponent
extends BaseListingComponent<DossierWrapper>
implements OnInit, AfterViewInit, OnDestroy, OnAttach, OnDetach
{
readonly itemSize = 95;
protected readonly _primaryKey = 'dossierName';
buttonConfigs: ButtonConfig[] = [
@ -78,7 +80,6 @@ export class DossierListingScreenComponent extends BaseListingComponent<DossierW
private readonly _needsWorkTemplate: TemplateRef<any>;
constructor(
readonly permissionsService: PermissionsService,
private readonly _translateChartService: TranslateChartService,
private readonly _userService: UserService,
private readonly _dialogService: DossiersDialogService,
@ -86,6 +87,7 @@ export class DossierListingScreenComponent extends BaseListingComponent<DossierW
private readonly _router: Router,
private readonly _appStateService: AppStateService,
private readonly _userPreferenceService: UserPreferenceService,
readonly changeDetectorRef: ChangeDetectorRef,
protected readonly _injector: Injector
) {
super(_injector);
@ -111,6 +113,10 @@ export class DossierListingScreenComponent extends BaseListingComponent<DossierW
});
}
ngAfterViewInit() {
this.changeDetectorRef.detectChanges();
}
ngOnAttach() {
this.scrollViewport.scrollTo({ top: this._lastScrollPosition });
this._appStateService.reset();
@ -122,10 +128,6 @@ export class DossierListingScreenComponent extends BaseListingComponent<DossierW
this.ngOnDestroy();
}
ngOnDestroy(): void {
super.ngOnDestroy();
}
getDossierTemplate(dw: DossierWrapper): DossierTemplateModel {
return this._appStateService.getDossierTemplateById(dw.dossier.dossierTemplateId);
}
@ -264,8 +266,6 @@ export class DossierListingScreenComponent extends BaseListingComponent<DossierW
values: quickFilters,
checker: (dw: DossierWrapper) => quickFilters.reduce((acc, f) => acc || (f.checked && f.checker(dw)), false)
});
this.filterService.applyFilters();
}
private _createQuickFilters() {

View File

@ -49,8 +49,8 @@ export class DossierOverviewScreenComponent
dossierAttributes: DossierAttributeWithValue[] = [];
@ViewChild(DossierDetailsComponent, { static: false })
private readonly _dossierDetailsComponent: DossierDetailsComponent;
private readonly _lastOpenedFileKey = 'Dossier-Recent-' + this.activeDossier.dossierId;
private _lastScrollPosition: number;
private _lastOpenedFileKey = 'Dossier-Recent-' + this.activeDossier.dossierId;
@ViewChild('needsWorkTemplate', { read: TemplateRef, static: true })
private readonly _needsWorkTemplate: TemplateRef<any>;
@ -163,8 +163,6 @@ export class DossierOverviewScreenComponent
this._loadEntitiesFromState();
this._computeAllFilters();
this.filterService.applyFilters();
this._dossierDetailsComponent?.calculateChartConfig();
this._changeDetectorRef.detectChanges();
}

View File

@ -15,11 +15,14 @@ export abstract class BaseListingComponent<T> extends AutoUnsubscribeComponent i
readonly scrollViewport: CdkVirtualScrollViewport;
readonly permissionsService: PermissionsService;
readonly filterService: FilterService<T>;
readonly filterService: FilterService;
readonly sortingService: SortingService;
readonly searchService: SearchService<T>;
readonly screenStateService: ScreenStateService<T>;
readonly sortedDisplayedEntities$: Observable<T[]>;
readonly noMatch$: Observable<boolean>;
/**
* Key used in the *trackBy* function with **ngFor* or **cdkVirtualFor*
* and in the default sorting and as the search field
@ -36,6 +39,9 @@ export abstract class BaseListingComponent<T> extends AutoUnsubscribeComponent i
this.searchService = this._injector.get(SearchService);
this.screenStateService = this._injector.get<ScreenStateService<T>>(ScreenStateService);
setTimeout(() => this.setInitialConfig());
this.sortedDisplayedEntities$ = this._sortedDisplayedEntities$;
this.noMatch$ = this._noMatch$;
}
setInitialConfig() {
@ -50,7 +56,7 @@ export abstract class BaseListingComponent<T> extends AutoUnsubscribeComponent i
super.ngOnDestroy();
}
get sortedDisplayedEntities$(): Observable<T[]> {
private get _sortedDisplayedEntities$(): Observable<T[]> {
return this.screenStateService.displayedEntities$.pipe(map(entities => this.sortingService.defaultSort(entities)));
}
@ -58,7 +64,7 @@ export abstract class BaseListingComponent<T> extends AutoUnsubscribeComponent i
return this.screenStateService.allEntities;
}
get noMatch$(): Observable<boolean> {
private get _noMatch$(): Observable<boolean> {
return combineLatest([this.screenStateService.allEntitiesLength$, this.screenStateService.displayedLength$]).pipe(
map(([hasEntities, hasDisplayedEntities]) => hasEntities && !hasDisplayedEntities),
distinctUntilChanged()

View File

@ -1,12 +1,4 @@
import {
ChangeDetectorRef,
Component,
EventEmitter,
Input,
OnChanges,
Output,
TemplateRef
} from '@angular/core';
import { ChangeDetectorRef, Component, EventEmitter, Input, OnChanges, Output, TemplateRef } from '@angular/core';
import { FilterModel } from './model/filter.model';
import { handleCheckedValue } from './utils/filter-utils';
import { MAT_CHECKBOX_DEFAULT_OPTIONS } from '@angular/material/checkbox';
@ -42,10 +34,7 @@ export class PopupFilterComponent implements OnChanges {
atLeastOneFilterIsExpandable = false;
atLeastOneSecondaryFilterIsExpandable = false;
constructor(
private readonly _changeDetectorRef: ChangeDetectorRef,
private readonly _translateService: TranslateService
) {}
constructor(private readonly _changeDetectorRef: ChangeDetectorRef, private readonly _translateService: TranslateService) {}
get hasActiveFilters(): boolean {
return !!this._allFilters.find(f => f.checked || f.indeterminate);
@ -57,9 +46,7 @@ export class PopupFilterComponent implements OnChanges {
ngOnChanges(): void {
this.atLeastOneFilterIsExpandable = !!this.primaryFilters?.find(f => this.isExpandable(f));
this.atLeastOneSecondaryFilterIsExpandable = !!this.secondaryFilters?.find(f =>
this.isExpandable(f)
);
this.atLeastOneSecondaryFilterIsExpandable = !!this.secondaryFilters?.find(f => this.isExpandable(f));
}
filterCheckboxClicked($event: any, filter: FilterModel, parent?: FilterModel) {

View File

@ -1,6 +1,6 @@
<div
(click)="filterService.toggleFilter('quickFilters', filter.key)"
*ngFor="let filter of filterService.getFilterModels$('quickFilters') | async"
*ngFor="let filter of quickFilters$ | async"
[class.active]="filter.checked"
class="quick-filter"
>

View File

@ -1,14 +1,14 @@
import { Component, EventEmitter, Output } from '@angular/core';
import { FilterModel } from '../popup-filter/model/filter.model';
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { FilterService } from '@shared/services/filter.service';
@Component({
selector: 'redaction-quick-filters',
templateUrl: './quick-filters.component.html',
styleUrls: ['./quick-filters.component.scss']
styleUrls: ['./quick-filters.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class QuickFiltersComponent<T> {
@Output() filtersChanged = new EventEmitter<FilterModel[]>();
export class QuickFiltersComponent {
readonly quickFilters$ = this.filterService.getFilterModels$('quickFilters');
constructor(readonly filterService: FilterService<T>) {}
constructor(readonly filterService: FilterService) {}
}

View File

@ -6,7 +6,7 @@
<ng-container *ngFor="let config of filters; trackBy: trackByLabel">
<redaction-popup-filter
(filtersChanged)="filterService.applyFilters()"
(filtersChanged)="filterService.refresh()"
*ngIf="!config.hide"
[filterLabel]="config.label"
[icon]="config.icon"

View File

@ -4,8 +4,7 @@ import { ButtonConfig } from '@shared/components/page-header/models/button-confi
import { FilterService } from '@shared/services/filter.service';
import { SearchService } from '@shared/services/search.service';
import { distinctUntilChanged, map } from 'rxjs/operators';
import { combineLatest, Observable } from 'rxjs';
import { FilterGroup } from '@shared/components/filters/popup-filter/model/filter-wrapper.model';
import { combineLatest, Observable, of } from 'rxjs';
@Component({
selector: 'redaction-page-header',
@ -19,13 +18,14 @@ export class PageHeaderComponent<T> {
@Input() buttonConfigs: ButtonConfig[];
@Input() searchPlaceholder: string;
constructor(@Optional() readonly filterService: FilterService<T>, @Optional() readonly searchService: SearchService<T>) {}
readonly filters$ = this.filterService?.filterGroups$.pipe(map(all => all.filter(f => f.icon)));
readonly showResetFilters$ = this._showResetFilters$;
get filters$(): Observable<FilterGroup[]> {
return this.filterService?.filterGroups$.pipe(map(all => all.filter(f => f.icon)));
}
constructor(@Optional() readonly filterService: FilterService, @Optional() readonly searchService: SearchService<T>) {}
get _showResetFilters$(): Observable<boolean> {
if (!this.filterService) return of(false);
get showResetFilters$(): Observable<boolean> {
const filtersLength$ = this.filters$.pipe(
map(f => f.length),
distinctUntilChanged()

View File

@ -33,8 +33,8 @@
<div
(click)="selectValue(val.key)"
*ngFor="let val of config"
[class.active]="filterService.filterChecked$('statusFilters', val.key) | async"
[class.filter-disabled]="(filterService.getFilterModels$('statusFilters') | async)?.length === 0"
[class.active]="filterChecked$(val.key) | async"
[class.filter-disabled]="(statusFilters$ | async)?.length === 0"
>
<redaction-status-bar
[config]="[

View File

@ -1,6 +1,8 @@
import { Component, Input, OnChanges } from '@angular/core';
import { Color } from '@utils/types';
import { FilterService } from '@shared/services/filter.service';
import { Observable, of } from 'rxjs';
import { map } from 'rxjs/operators';
export interface DoughnutChartConfig {
value: number;
@ -15,7 +17,7 @@ export interface DoughnutChartConfig {
templateUrl: './simple-doughnut-chart.component.html',
styleUrls: ['./simple-doughnut-chart.component.scss']
})
export class SimpleDoughnutChartComponent<T> implements OnChanges {
export class SimpleDoughnutChartComponent implements OnChanges {
@Input() subtitle: string;
@Input() config: DoughnutChartConfig[] = [];
@Input() radius = 85;
@ -30,7 +32,9 @@ export class SimpleDoughnutChartComponent<T> implements OnChanges {
cy = 0;
size = 0;
constructor(readonly filterService: FilterService<T>) {}
readonly statusFilters$ = this.filterService.getFilterModels$('statusFilters') ?? of([]);
constructor(readonly filterService: FilterService) {}
get circumference(): number {
return 2 * Math.PI * this.radius;
@ -51,6 +55,10 @@ export class SimpleDoughnutChartComponent<T> implements OnChanges {
this.size = this.strokeWidth + this.radius * 2;
}
filterChecked$(key: string): Observable<boolean> {
return this.statusFilters$.pipe(map(all => all?.find(e => e.key === key)?.checked));
}
calculateChartData() {
let angleOffset = -90;
this.chartData = this.config.map(dataVal => {

View File

@ -1,57 +1,33 @@
import { ChangeDetectorRef, Injectable } from '@angular/core';
import { Injectable } from '@angular/core';
import { FilterModel } from '@shared/components/filters/popup-filter/model/filter.model';
import { getFilteredEntities, processFilters } from '@shared/components/filters/popup-filter/utils/filter-utils';
import { processFilters } from '@shared/components/filters/popup-filter/utils/filter-utils';
import { FilterGroup } 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';
import { BehaviorSubject, Observable } from 'rxjs';
import { distinctUntilChanged, filter, map } from 'rxjs/operators';
import { distinctUntilChanged, map, switchMap } from 'rxjs/operators';
@Injectable()
export class FilterService<T> {
_filterGroups$ = new BehaviorSubject<FilterGroup[]>([]);
export class FilterService {
private readonly _filterGroups$ = new BehaviorSubject<FilterGroup[]>([]);
private readonly _refresh$ = new BehaviorSubject(null);
constructor(
private readonly _screenStateService: ScreenStateService<T>,
private readonly _searchService: SearchService<T>,
private readonly _changeDetector: ChangeDetectorRef
) {}
readonly filterGroups$ = this._refresh$.pipe(switchMap(() => this._filterGroups$.asObservable()));
readonly showResetFilters$ = this._showResetFilters$;
get filterGroups(): FilterGroup[] {
return Object.values(this._filterGroups$.getValue());
}
get showResetFilters$(): Observable<boolean> {
return this.filterGroups$.pipe(
map(all => this._toFlatFilters(all)),
map(f => !!f.find(el => el.checked)),
distinctUntilChanged()
);
}
filterChecked$(slug: string, key: string): Observable<boolean> {
return this.getFilterModels$(slug).pipe(
map(all => all.find(f => f.key === key)?.checked),
distinctUntilChanged()
);
refresh(): void {
this._refresh$.next(null);
}
toggleFilter(slug: string, key: string) {
const filters = this.filterGroups.find(f => f.slug === slug);
let found = filters.values.find(f => f.key === key);
if (!found) found = filters.values.map(f => f.filters?.find(ff => ff.key === key))[0];
const filters = this.filterGroups.find(f => f.slug === slug).values;
let found = filters.find(f => f.key === key);
if (!found) found = filters.map(f => f.filters?.find(ff => ff.key === key))[0];
found.checked = !found.checked;
this._filterGroups$.next(this.filterGroups);
this.applyFilters();
}
applyFilters(): void {
const filtered = getFilteredEntities(this._screenStateService.allEntities, this.filterGroups);
this._screenStateService.setDisplayedEntities(filtered);
this._searchService.executeSearchImmediately();
this._changeDetector.detectChanges();
this.refresh();
}
addFilterGroup(value: FilterGroup): void {
@ -63,22 +39,15 @@ export class FilterService<T> {
}
getFilterGroup(slug: string): FilterGroup {
return this.filterGroups.find(f => f?.slug === slug);
return this.filterGroups.find(f => f.slug === slug);
}
getFilterModels$(filterGroupSlug: string): Observable<FilterModel[]> {
return this.getFilterGroup$(filterGroupSlug).pipe(
filter(f => f !== null && f !== undefined),
map(f => f.values)
);
return this.getFilterGroup$(filterGroupSlug).pipe(map(f => f?.values));
}
getFilterGroup$(slug: string): Observable<FilterGroup> {
return this.filterGroups$.pipe(map(all => all.find(f => f?.slug === slug)));
}
get filterGroups$(): Observable<FilterGroup[]> {
return this._filterGroups$.asObservable();
return this.filterGroups$.pipe(map(all => all.find(f => f.slug === slug)));
}
reset(): void {
@ -92,8 +61,16 @@ export class FilterService<T> {
});
});
});
this._filterGroups$.next(this.filterGroups);
this.applyFilters();
this.refresh();
}
private get _showResetFilters$(): Observable<boolean> {
return this.filterGroups$.pipe(
map(all => this._toFlatFilters(all)),
map(f => !!f.find(el => el.checked)),
distinctUntilChanged()
);
}
private _toFlatFilters(entities: FilterGroup[]): FilterModel[] {

View File

@ -1,20 +1,38 @@
import { Injectable } from '@angular/core';
import { BehaviorSubject, combineLatest, Observable } from 'rxjs';
import { distinctUntilChanged, map } from 'rxjs/operators';
import { distinctUntilChanged, map, tap } from 'rxjs/operators';
import { FilterService } from '@shared/services/filter.service';
import { SearchService } from '@shared/services/search.service';
import { getFilteredEntities } from '@shared/components/filters/popup-filter/utils/filter-utils';
const toLengthValue = entities => entities?.length ?? 0;
@Injectable()
export class ScreenStateService<T> {
_allEntities$ = new BehaviorSubject<T[]>([]);
_displayedEntities$ = new BehaviorSubject<T[]>([]);
_selectedEntities$ = new BehaviorSubject<T[]>([]);
private readonly _allEntities$ = new BehaviorSubject<T[]>([]);
readonly allEntities$ = this._allEntities$.asObservable();
readonly allEntitiesLength$ = this._allEntitiesLength$;
// constructor() {
// setInterval(() => {
// console.log('All entities subs: ', this._allEntities$.observers);
// console.log('Displayed entities subs: ', this._displayedEntities$.observers);
// console.log('Selected entities subs: ', this._selectedEntities$.observers);
// }, 10000);
// }
private readonly _displayedEntities$ = new BehaviorSubject<T[]>([]);
readonly displayedEntities$ = this._getDisplayedEntities$;
readonly displayedLength$ = this._displayedLength$;
private readonly _selectedEntities$ = new BehaviorSubject<T[]>([]);
readonly selectedEntities$ = this._selectedEntities$.asObservable();
readonly selectedLength$ = this._selectedLength$;
readonly noData$ = this._noData$;
readonly areAllEntitiesSelected$ = this._areAllEntitiesSelected$;
readonly areSomeEntitiesSelected$ = this._areSomeEntitiesSelected$;
readonly notAllEntitiesSelected$ = this._notAllEntitiesSelected$;
constructor(private readonly _filterService: FilterService, private readonly _searchService: SearchService<T>) {
// setInterval(() => {
// console.log('All entities subs: ', this._allEntities$.observers);
// console.log('Displayed entities subs: ', this._displayedEntities$.observers);
// console.log('Selected entities subs: ', this._selectedEntities$.observers);
// }, 10000);
}
get allEntities(): T[] {
return Object.values(this._allEntities$.getValue());
@ -28,25 +46,6 @@ export class ScreenStateService<T> {
return Object.values(this._displayedEntities$.getValue());
}
get allEntities$(): Observable<T[]> {
return this._allEntities$.asObservable();
}
get selectedEntities$(): Observable<T[]> {
return this._selectedEntities$.asObservable();
}
get displayedEntities$(): Observable<T[]> {
return this._displayedEntities$.asObservable();
}
map<K>(func: (state: T[]) => K): Observable<K> {
return this.allEntities$.pipe(
map((state: T[]) => func(state)),
distinctUntilChanged()
);
}
setEntities(newEntities: Partial<T[]>): void {
this._allEntities$.next(newEntities);
}
@ -55,74 +54,6 @@ export class ScreenStateService<T> {
this._selectedEntities$.next(newEntities);
}
setDisplayedEntities(newEntities: Partial<T[]>): void {
this._displayedEntities$.next(newEntities);
}
get noData$(): Observable<boolean> {
return this.allEntitiesLength$.pipe(
map(length => length === 0),
distinctUntilChanged()
);
}
/**
* Returns the length of all entities
*/
get allEntitiesLength$(): Observable<number> {
return this.allEntities$.pipe(
map(all => all?.length ?? 0),
distinctUntilChanged()
);
}
/**
* Returns the length of the currently displayed entities
*/
get displayedLength$(): Observable<number> {
return this.displayedEntities$.pipe(
map(all => all?.length ?? 0),
distinctUntilChanged()
);
}
/**
* Returns the length of the selected entities
*/
get selectedLength$(): Observable<number> {
return this.selectedEntities$.pipe(
map(all => all?.length ?? 0),
distinctUntilChanged()
);
}
get areAllEntitiesSelected$(): Observable<boolean> {
return combineLatest([this.displayedLength$, this.selectedLength$]).pipe(
map(([displayedLength, selectedLength]) => displayedLength && displayedLength === selectedLength),
distinctUntilChanged()
);
}
/**
* Indicates that some entities are selected. If all are selected this returns true
*/
get areSomeEntitiesSelected$(): Observable<boolean> {
return this.selectedLength$.pipe(
map(value => value > 0),
distinctUntilChanged()
);
}
/**
* Indicates that some entities are selected, but not all
*/
get notAllEntitiesSelected$(): Observable<boolean> {
return combineLatest([this.areAllEntitiesSelected$, this.areSomeEntitiesSelected$]).pipe(
map(([allEntitiesAreSelected, someEntitiesAreSelected]) => !allEntitiesAreSelected && someEntitiesAreSelected),
distinctUntilChanged()
);
}
isSelected(entity: T): boolean {
return this.selectedEntities.indexOf(entity) !== -1;
}
@ -145,6 +76,63 @@ export class ScreenStateService<T> {
console.log('Selected', this.selectedEntities);
}
get _getDisplayedEntities$(): Observable<T[]> {
const filterGroups$ = this._filterService.filterGroups$;
const searchValue$ = this._searchService.valueChanges$;
return combineLatest([this.allEntities$, filterGroups$, searchValue$]).pipe(
map(([entities, filterGroups]) => getFilteredEntities(entities, filterGroups)),
map(entities => this._searchService.searchIn(entities)),
tap(entities => this._displayedEntities$.next(entities))
);
}
private get _allEntitiesLength$(): Observable<number> {
return this.allEntities$.pipe(map(toLengthValue), distinctUntilChanged());
}
private get _displayedLength$(): Observable<number> {
return this.displayedEntities$.pipe(map(toLengthValue), distinctUntilChanged());
}
private get _selectedLength$(): Observable<number> {
return this.selectedEntities$.pipe(map(toLengthValue), distinctUntilChanged());
}
private get _areAllEntitiesSelected$(): Observable<boolean> {
return combineLatest([this.displayedLength$, this.selectedLength$]).pipe(
map(([displayedLength, selectedLength]) => displayedLength && displayedLength === selectedLength),
distinctUntilChanged()
);
}
/**
* Indicates that some entities are selected. If all are selected this returns true
*/
private get _areSomeEntitiesSelected$(): Observable<boolean> {
return this.selectedLength$.pipe(
map(value => !!value),
distinctUntilChanged()
);
}
/**
* Indicates that some entities are selected, but not all
*/
private get _notAllEntitiesSelected$(): Observable<boolean> {
return combineLatest([this.areAllEntitiesSelected$, this.areSomeEntitiesSelected$]).pipe(
map(([allEntitiesAreSelected, someEntitiesAreSelected]) => !allEntitiesAreSelected && someEntitiesAreSelected),
distinctUntilChanged()
);
}
private get _noData$(): Observable<boolean> {
return this.allEntitiesLength$.pipe(
map(length => length === 0),
distinctUntilChanged()
);
}
private _selectOne(entity: T): void {
const currentEntityIdx = this.selectedEntities.indexOf(entity);
if (currentEntityIdx === -1) {

View File

@ -1,41 +1,24 @@
import { Injectable } from '@angular/core';
import { debounce } from '@utils/debounce';
import { ScreenStateService } from '@shared/services/screen-state.service';
import { FormBuilder } from '@angular/forms';
import { BehaviorSubject } from 'rxjs';
import { startWith } from 'rxjs/operators';
@Injectable()
export class SearchService<T> {
private _searchValue = '';
private _searchKey: string;
valueChanges$ = new BehaviorSubject(this._searchValue);
readonly searchForm = this._formBuilder.group({
query: ['']
});
constructor(private readonly _screenStateService: ScreenStateService<T>, private readonly _formBuilder: FormBuilder) {
this.searchForm.valueChanges.subscribe(() => {
this.valueChanges$.next(this.searchValue.toLowerCase());
this.executeSearch();
});
}
readonly valueChanges$ = this.searchForm.get('query').valueChanges.pipe(startWith(''));
@debounce(200)
executeSearch(): void {
this._searchValue = this.searchValue.toLowerCase();
this.executeSearchImmediately();
}
constructor(private readonly _formBuilder: FormBuilder) {}
executeSearchImmediately(): void {
if (!this._searchKey) return;
searchIn(entities: T[]) {
if (!this._searchKey) return entities;
const displayed = this._screenStateService.displayedEntities;
this._screenStateService.setDisplayedEntities(
displayed.filter(entity => this._searchField(entity).toLowerCase().includes(this._searchValue))
);
this._screenStateService.updateSelection();
const searchValue = this.searchValue.toLowerCase();
return entities.filter(entity => this._searchField(entity).includes(searchValue));
}
setSearchKey(value: string): void {
@ -50,7 +33,7 @@ export class SearchService<T> {
this.searchForm.reset({ query: '' });
}
protected _searchField(entity: T): string {
return entity[this._searchKey];
private _searchField(entity: T): string {
return entity[this._searchKey].toLowerCase();
}
}

View File

@ -1,5 +1,5 @@
import { EventEmitter, Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
const MIN_LOADING_TIME = 300;
@ -7,16 +7,15 @@ const MIN_LOADING_TIME = 300;
providedIn: 'root'
})
export class LoadingService {
private readonly _loadingEvent = new EventEmitter();
private readonly _loadingEvent = new BehaviorSubject(false);
private _loadingStarted: number;
get isLoading(): Observable<boolean> {
return this._loadingEvent;
return this._loadingEvent.asObservable();
}
start(): void {
// setTimeout is used so that value doesn't change after it was checked for changes
setTimeout(() => this._loadingEvent.next(true));
this._loadingEvent.next(true);
this._loadingStarted = new Date().getTime();
}
@ -37,7 +36,7 @@ export class LoadingService {
}
private _stop() {
this._loadingEvent.next(false);
setTimeout(() => this._loadingEvent.next(false));
}
private _stopAfter(timeout: number) {