use properties to hold observables, fix filter & search issues
This commit is contained in:
parent
02b38780f6
commit
334ef37a5c
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -66,7 +66,6 @@ export class DefaultColorsScreenComponent
|
||||
value: data[key]
|
||||
}));
|
||||
this.screenStateService.setEntities(entities);
|
||||
this.screenStateService.setDisplayedEntities(entities);
|
||||
this._loadingService.stop();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 =>
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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) {}
|
||||
}
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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"
|
||||
>
|
||||
|
||||
@ -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) {}
|
||||
}
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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]="[
|
||||
|
||||
@ -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 => {
|
||||
|
||||
@ -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[] {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user