add table header component, remove filteredEntities

This commit is contained in:
Dan Percic 2021-07-16 14:43:44 +03:00
parent 27de88ac57
commit 02b38780f6
29 changed files with 213 additions and 153 deletions

View File

@ -88,7 +88,7 @@
<div <div
(mouseenter)="setHoveredColumn.emit(field.csvColumn)" (mouseenter)="setHoveredColumn.emit(field.csvColumn)"
(mouseleave)="setHoveredColumn.emit()" (mouseleave)="setHoveredColumn.emit()"
*cdkVirtualFor="let field of displayedEntities$ | async; trackBy: trackByPrimaryKey" *cdkVirtualFor="let field of sortedDisplayedEntities$ | async; trackBy: trackByPrimaryKey"
class="table-item" class="table-item"
> >
<div (click)="toggleEntitySelected($event, field)" class="selection-column"> <div (click)="toggleEntitySelected($event, field)" class="selection-column">

View File

@ -101,7 +101,7 @@
(click)="toggleFieldActive(field)" (click)="toggleFieldActive(field)"
(mouseenter)="setHoveredColumn(field.csvColumn)" (mouseenter)="setHoveredColumn(field.csvColumn)"
(mouseleave)="setHoveredColumn()" (mouseleave)="setHoveredColumn()"
*ngFor="let field of displayedEntities$ | async; trackBy: trackByPrimaryKey" *ngFor="let field of sortedDisplayedEntities$ | async; trackBy: trackByPrimaryKey"
class="csv-header-pill-wrapper" class="csv-header-pill-wrapper"
> >
<div [class.selected]="isActive(field)" class="csv-header-pill"> <div [class.selected]="isActive(field)" class="csv-header-pill">

View File

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

View File

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

View File

@ -100,7 +100,7 @@ export class FileAttributesListingScreenComponent extends BaseListingComponent<F
this.screenStateService.setEntities(response?.fileAttributeConfigs || []); this.screenStateService.setEntities(response?.fileAttributeConfigs || []);
} catch (e) { } catch (e) {
} finally { } finally {
this.filterService.filterEntities(); this.filterService.applyFilters();
this._loadingService.stop(); this._loadingService.stop();
} }
} }

View File

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

View File

@ -76,7 +76,7 @@
<cdk-virtual-scroll-viewport [itemSize]="80" redactionHasScrollbar> <cdk-virtual-scroll-viewport [itemSize]="80" redactionHasScrollbar>
<!-- Table lines --> <!-- Table lines -->
<div *cdkVirtualFor="let user of displayedEntities$ | async; trackBy: trackByPrimaryKey" class="table-item"> <div *cdkVirtualFor="let user of sortedDisplayedEntities$ | async; trackBy: trackByPrimaryKey" class="table-item">
<div (click)="toggleEntitySelected($event, user)" class="selection-column"> <div (click)="toggleEntitySelected($event, user)" class="selection-column">
<redaction-round-checkbox [active]="isSelected(user)"></redaction-round-checkbox> <redaction-round-checkbox [active]="isSelected(user)"></redaction-round-checkbox>
</div> </div>

View File

@ -86,7 +86,7 @@ export class UserListingScreenComponent extends BaseListingComponent<User> imple
private async _loadData() { private async _loadData() {
this.screenStateService.setEntities(await this._userControllerService.getAllUsers().toPromise()); this.screenStateService.setEntities(await this._userControllerService.getAllUsers().toPromise());
await this.userService.loadAllUsers(); await this.userService.loadAllUsers();
this.filterService.filterEntities(); this.filterService.applyFilters();
this._computeStats(); this._computeStats();
this._loadingService.stop(); this._loadingService.stop();
} }

View File

@ -49,7 +49,7 @@
<div *ngIf="hasFiles" class="mt-24 legend pb-32"> <div *ngIf="hasFiles" class="mt-24 legend pb-32">
<div <div
(click)="filterService.toggleFilter('needsWorkFilters', filter.key)" (click)="filterService.toggleFilter('needsWorkFilters', filter.key)"
*ngFor="let filter of filterService.getFilter$('needsWorkFilters') | async" *ngFor="let filter of filterService.getFilterModels$('needsWorkFilters') | async"
[class.active]="filterService.filterChecked$('needsWorkFilters', filter.key) | async" [class.active]="filterService.filterChecked$('needsWorkFilters', filter.key) | async"
> >
<redaction-type-filter [filter]="filter"></redaction-type-filter> <redaction-type-filter [filter]="filter"></redaction-type-filter>

View File

@ -8,28 +8,10 @@
<div class="red-content-inner"> <div class="red-content-inner">
<div class="content-container"> <div class="content-container">
<div class="header-item"> <redaction-table-header
<span class="all-caps-label"> [tableHeaderLabel]="'dossier-listing.table-header.title'"
{{ 'dossier-listing.table-header.title' | translate: { length: (screenStateService.displayedLength$ | async) } }} [tableColConfigs]="tableColConfigs"
</span> ></redaction-table-header>
<redaction-quick-filters></redaction-quick-filters>
</div>
<div class="table-header" redactionSyncWidth="table-item">
<redaction-table-col-name
[withSort]="true"
column="dossierName"
label="dossier-listing.table-col-names.name"
></redaction-table-col-name>
<redaction-table-col-name label="dossier-listing.table-col-names.needs-work"></redaction-table-col-name>
<redaction-table-col-name class="user-column" label="dossier-listing.table-col-names.owner"></redaction-table-col-name>
<redaction-table-col-name class="flex-end" label="dossier-listing.table-col-names.status"></redaction-table-col-name>
<div class="scrollbar-placeholder"></div>
</div>
<redaction-empty-state <redaction-empty-state
(action)="openAddDossierDialog()" (action)="openAddDossierDialog()"

View File

@ -29,6 +29,7 @@ import { SearchService } from '@shared/services/search.service';
import { ScreenStateService } from '@shared/services/screen-state.service'; import { ScreenStateService } from '@shared/services/screen-state.service';
import { BaseListingComponent } from '@shared/base/base-listing.component'; import { BaseListingComponent } from '@shared/base/base-listing.component';
import { SortingService } from '@services/sorting.service'; import { SortingService } from '@services/sorting.service';
import { TableColConfig } from '../../../shared/components/table-col-name/table-col-name.component';
const isLeavingScreen = event => event instanceof NavigationStart && event.url !== '/main/dossiers'; const isLeavingScreen = event => event instanceof NavigationStart && event.url !== '/main/dossiers';
@ -40,9 +41,6 @@ const isLeavingScreen = event => event instanceof NavigationStart && event.url !
export class DossierListingScreenComponent extends BaseListingComponent<DossierWrapper> implements OnInit, OnDestroy, OnAttach, OnDetach { export class DossierListingScreenComponent extends BaseListingComponent<DossierWrapper> implements OnInit, OnDestroy, OnAttach, OnDetach {
readonly itemSize = 95; readonly itemSize = 95;
protected readonly _primaryKey = 'dossierName'; protected readonly _primaryKey = 'dossierName';
dossiersChartData: DoughnutChartConfig[] = [];
documentsChartData: DoughnutChartConfig[] = [];
buttonConfigs: ButtonConfig[] = [ buttonConfigs: ButtonConfig[] = [
{ {
label: this._translateService.instant('dossier-listing.add-new'), label: this._translateService.instant('dossier-listing.add-new'),
@ -52,6 +50,27 @@ export class DossierListingScreenComponent extends BaseListingComponent<DossierW
type: 'primary' type: 'primary'
} }
]; ];
tableColConfigs: TableColConfig[] = [
{
label: 'dossier-listing.table-col-names.name',
withSort: true,
column: 'dossierName'
},
{
label: 'dossier-listing.table-col-names.needs-work'
},
{
label: 'dossier-listing.table-col-names.owner',
class: 'user-column'
},
{
label: 'dossier-listing.table-col-names.status',
class: 'flex-end'
}
];
dossiersChartData: DoughnutChartConfig[] = [];
documentsChartData: DoughnutChartConfig[] = [];
private _lastScrollPosition: number; private _lastScrollPosition: number;
@ -188,7 +207,7 @@ export class DossierListingScreenComponent extends BaseListingComponent<DossierW
label: this._translateService.instant(status) label: this._translateService.instant(status)
})); }));
this.filterService.addFilter({ this.filterService.addFilterGroup({
slug: 'statusFilters', slug: 'statusFilters',
label: this._translateService.instant('filters.status'), label: this._translateService.instant('filters.status'),
icon: 'red:status', icon: 'red:status',
@ -201,7 +220,7 @@ export class DossierListingScreenComponent extends BaseListingComponent<DossierW
label: this._userService.getNameForId(userId) label: this._userService.getNameForId(userId)
})); }));
this.filterService.addFilter({ this.filterService.addFilterGroup({
slug: 'peopleFilters', slug: 'peopleFilters',
label: this._translateService.instant('filters.people'), label: this._translateService.instant('filters.people'),
icon: 'red:user', icon: 'red:user',
@ -214,7 +233,7 @@ export class DossierListingScreenComponent extends BaseListingComponent<DossierW
label: `filter.${type}` label: `filter.${type}`
})); }));
this.filterService.addFilter({ this.filterService.addFilterGroup({
slug: 'needsWorkFilters', slug: 'needsWorkFilters',
label: this._translateService.instant('filters.needs-work'), label: this._translateService.instant('filters.needs-work'),
icon: 'red:needs-work', icon: 'red:needs-work',
@ -230,23 +249,23 @@ export class DossierListingScreenComponent extends BaseListingComponent<DossierW
label: this._appStateService.getDossierTemplateById(id).name label: this._appStateService.getDossierTemplateById(id).name
})); }));
this.filterService.addFilter({ this.filterService.addFilterGroup({
slug: 'dossierTemplateFilters', slug: 'dossierTemplateFilters',
label: this._translateService.instant('filters.dossier-templates'), label: this._translateService.instant('filters.dossier-templates'),
icon: 'red:template', icon: 'red:template',
hide: this.filterService.getFilter('dossierTemplateFilters')?.values?.length <= 1, hide: this.filterService.getFilterGroup('dossierTemplateFilters')?.values?.length <= 1,
values: dossierTemplateFilters, values: dossierTemplateFilters,
checker: dossierTemplateChecker checker: dossierTemplateChecker
}); });
const quickFilters = this._createQuickFilters(); const quickFilters = this._createQuickFilters();
this.filterService.addFilter({ this.filterService.addFilterGroup({
slug: 'quickFilters', slug: 'quickFilters',
values: quickFilters, values: quickFilters,
checker: (dw: DossierWrapper) => quickFilters.reduce((acc, f) => acc || (f.checked && f.checker(dw)), false) checker: (dw: DossierWrapper) => quickFilters.reduce((acc, f) => acc || (f.checked && f.checker(dw)), false)
}); });
this.filterService.filterEntities(); this.filterService.applyFilters();
} }
private _createQuickFilters() { private _createQuickFilters() {

View File

@ -86,11 +86,11 @@ export class DossierOverviewScreenComponent
} }
get checkedRequiredFilters() { get checkedRequiredFilters() {
return this.filterService.getFilter('quickFilters')?.values.filter(f => f.required && f.checked); return this.filterService.getFilterGroup('quickFilters')?.values.filter(f => f.required && f.checked);
} }
get checkedNotRequiredFilters() { get checkedNotRequiredFilters() {
return this.filterService.getFilter('quickFilters')?.values.filter(f => !f.required && f.checked); return this.filterService.getFilterGroup('quickFilters')?.values.filter(f => !f.required && f.checked);
} }
isLastOpenedFile({ fileId }: FileStatusWrapper): boolean { isLastOpenedFile({ fileId }: FileStatusWrapper): boolean {
@ -163,7 +163,7 @@ export class DossierOverviewScreenComponent
this._loadEntitiesFromState(); this._loadEntitiesFromState();
this._computeAllFilters(); this._computeAllFilters();
this.filterService.filterEntities(); this.filterService.applyFilters();
this._dossierDetailsComponent?.calculateChartConfig(); this._dossierDetailsComponent?.calculateChartConfig();
this._changeDetectorRef.detectChanges(); this._changeDetectorRef.detectChanges();
@ -263,7 +263,7 @@ export class DossierOverviewScreenComponent
label: this._translateService.instant(item) label: this._translateService.instant(item)
})); }));
this.filterService.addFilter({ this.filterService.addFilterGroup({
slug: 'statusFilters', slug: 'statusFilters',
label: this._translateService.instant('filters.status'), label: this._translateService.instant('filters.status'),
icon: 'red:status', icon: 'red:status',
@ -286,7 +286,7 @@ export class DossierOverviewScreenComponent
label: this._userService.getNameForId(userId) label: this._userService.getNameForId(userId)
}); });
}); });
this.filterService.addFilter({ this.filterService.addFilterGroup({
slug: 'peopleFilters', slug: 'peopleFilters',
label: this._translateService.instant('filters.assigned-people'), label: this._translateService.instant('filters.assigned-people'),
icon: 'red:user', icon: 'red:user',
@ -299,7 +299,7 @@ export class DossierOverviewScreenComponent
label: this._translateService.instant('filter.' + item) label: this._translateService.instant('filter.' + item)
})); }));
this.filterService.addFilter({ this.filterService.addFilterGroup({
slug: 'needsWorkFilters', slug: 'needsWorkFilters',
label: this._translateService.instant('filters.needs-work'), label: this._translateService.instant('filters.needs-work'),
icon: 'red:needs-work', icon: 'red:needs-work',
@ -310,7 +310,7 @@ export class DossierOverviewScreenComponent
checkerArgs: this.permissionsService checkerArgs: this.permissionsService
}); });
this.filterService.addFilter({ this.filterService.addFilterGroup({
slug: 'quickFilters', slug: 'quickFilters',
values: this._createQuickFilters(), values: this._createQuickFilters(),
checker: (file: FileStatusWrapper) => checker: (file: FileStatusWrapper) =>

View File

@ -6,7 +6,7 @@ import { SearchService } from '../services/search.service';
import { ScreenStateService } from '../services/screen-state.service'; import { ScreenStateService } from '../services/screen-state.service';
import { combineLatest, Observable } from 'rxjs'; import { combineLatest, Observable } from 'rxjs';
import { AutoUnsubscribeComponent } from './auto-unsubscribe.component'; import { AutoUnsubscribeComponent } from './auto-unsubscribe.component';
import { map } from 'rxjs/operators'; import { distinctUntilChanged, map } from 'rxjs/operators';
import { PermissionsService } from '../../../services/permissions.service'; import { PermissionsService } from '../../../services/permissions.service';
@Component({ template: '' }) @Component({ template: '' })
@ -50,10 +50,6 @@ export abstract class BaseListingComponent<T> extends AutoUnsubscribeComponent i
super.ngOnDestroy(); super.ngOnDestroy();
} }
get displayedEntities$(): Observable<T[]> {
return this.screenStateService.displayedEntities$;
}
get sortedDisplayedEntities$(): Observable<T[]> { get sortedDisplayedEntities$(): Observable<T[]> {
return this.screenStateService.displayedEntities$.pipe(map(entities => this.sortingService.defaultSort(entities))); return this.screenStateService.displayedEntities$.pipe(map(entities => this.sortingService.defaultSort(entities)));
} }
@ -64,13 +60,15 @@ export abstract class BaseListingComponent<T> extends AutoUnsubscribeComponent i
get noMatch$(): Observable<boolean> { get noMatch$(): Observable<boolean> {
return combineLatest([this.screenStateService.allEntitiesLength$, this.screenStateService.displayedLength$]).pipe( return combineLatest([this.screenStateService.allEntitiesLength$, this.screenStateService.displayedLength$]).pipe(
map(([hasEntities, hasDisplayedEntities]) => hasEntities && !hasDisplayedEntities) map(([hasEntities, hasDisplayedEntities]) => hasEntities && !hasDisplayedEntities),
distinctUntilChanged()
); );
} }
canBulkDelete$(hasPermission = true) { canBulkDelete$(hasPermission = true): Observable<boolean> {
return this.screenStateService.areSomeEntitiesSelected$.pipe( return this.screenStateService.areSomeEntitiesSelected$.pipe(
map(areSomeEntitiesSelected => areSomeEntitiesSelected && hasPermission) map(areSomeEntitiesSelected => areSomeEntitiesSelected && hasPermission),
distinctUntilChanged()
); );
} }

View File

@ -1,7 +1,7 @@
import { FilterModel } from './filter.model'; import { FilterModel } from './filter.model';
import { TemplateRef } from '@angular/core'; import { TemplateRef } from '@angular/core';
export interface FilterWrapper { export interface FilterGroup {
slug: string; slug: string;
label?: string; label?: string;
icon?: string; icon?: string;

View File

@ -2,7 +2,7 @@ import { FilterModel } from '../model/filter.model';
import { FileStatusWrapper } from '@models/file/file-status.wrapper'; import { FileStatusWrapper } from '@models/file/file-status.wrapper';
import { DossierWrapper } from '@state/model/dossier.wrapper'; import { DossierWrapper } from '@state/model/dossier.wrapper';
import { PermissionsService } from '@services/permissions.service'; import { PermissionsService } from '@services/permissions.service';
import { FilterWrapper } from '@shared/components/filters/popup-filter/model/filter-wrapper.model'; import { FilterGroup } from '@shared/components/filters/popup-filter/model/filter-wrapper.model';
export function processFilters(oldFilters: FilterModel[], newFilters: FilterModel[]) { export function processFilters(oldFilters: FilterModel[], newFilters: FilterModel[]) {
copySettings(oldFilters, newFilters); copySettings(oldFilters, newFilters);
@ -159,7 +159,7 @@ export const addedDateChecker = (dw: DossierWrapper, filter: FilterModel) => dw.
export const dossierApproverChecker = (dw: DossierWrapper, filter: FilterModel) => dw.approverIds.includes(filter.key); export const dossierApproverChecker = (dw: DossierWrapper, filter: FilterModel) => dw.approverIds.includes(filter.key);
export function getFilteredEntities<T>(entities: T[], filters: FilterWrapper[]) { export function getFilteredEntities<T>(entities: T[], filters: FilterGroup[]) {
const filteredEntities: T[] = []; const filteredEntities: T[] = [];
for (const entity of entities) { for (const entity of entities) {
let add = true; let add = true;

View File

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

View File

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

View File

@ -5,7 +5,7 @@ import { FilterService } from '@shared/services/filter.service';
import { SearchService } from '@shared/services/search.service'; import { SearchService } from '@shared/services/search.service';
import { distinctUntilChanged, map } from 'rxjs/operators'; import { distinctUntilChanged, map } from 'rxjs/operators';
import { combineLatest, Observable } from 'rxjs'; import { combineLatest, Observable } from 'rxjs';
import { FilterWrapper } from '@shared/components/filters/popup-filter/model/filter-wrapper.model'; import { FilterGroup } from '@shared/components/filters/popup-filter/model/filter-wrapper.model';
@Component({ @Component({
selector: 'redaction-page-header', selector: 'redaction-page-header',
@ -21,8 +21,8 @@ export class PageHeaderComponent<T> {
constructor(@Optional() readonly filterService: FilterService<T>, @Optional() readonly searchService: SearchService<T>) {} constructor(@Optional() readonly filterService: FilterService<T>, @Optional() readonly searchService: SearchService<T>) {}
get filters$(): Observable<FilterWrapper[]> { get filters$(): Observable<FilterGroup[]> {
return this.filterService?.allFilters$.pipe(map(all => all.filter(f => f.icon))); return this.filterService?.filterGroups$.pipe(map(all => all.filter(f => f.icon)));
} }
get showResetFilters$(): Observable<boolean> { get showResetFilters$(): Observable<boolean> {

View File

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

View File

@ -1,6 +1,16 @@
import { Component, Input, Optional } from '@angular/core'; import { Component, Input, Optional } from '@angular/core';
import { SortingService } from '@services/sorting.service'; import { SortingService } from '@services/sorting.service';
export interface TableColConfig {
readonly column?: string;
readonly label: string;
readonly withSort?: boolean;
readonly class?: string;
readonly leftIcon?: string;
readonly rightIcon?: string;
readonly rightIconTooltip?: string;
}
@Component({ @Component({
selector: 'redaction-table-col-name', selector: 'redaction-table-col-name',
templateUrl: './table-col-name.component.html', templateUrl: './table-col-name.component.html',

View File

@ -0,0 +1,22 @@
<div class="header-item">
<span class="all-caps-label">
{{ tableHeaderLabel | translate: { length: (screenStateService.displayedLength$ | async) } }}
</span>
<redaction-quick-filters></redaction-quick-filters>
</div>
<div class="table-header" redactionSyncWidth="table-item">
<redaction-table-col-name
*ngFor="let config of tableColConfigs"
[withSort]="config.withSort"
[column]="config.column"
[label]="config.label"
[class]="config.class"
[leftIcon]="config.leftIcon"
[rightIcon]="config.rightIcon"
[rightIconTooltip]="config.rightIconTooltip"
></redaction-table-col-name>
<div class="scrollbar-placeholder"></div>
</div>

View File

@ -0,0 +1,16 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { TableColConfig } from '@shared/components/table-col-name/table-col-name.component';
import { ScreenStateService } from '@shared/services/screen-state.service';
@Component({
selector: 'redaction-table-header',
templateUrl: './table-header.component.html',
styleUrls: ['./table-header.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class TableHeaderComponent<T> {
@Input() tableHeaderLabel: string;
@Input() tableColConfigs: TableColConfig[];
constructor(readonly screenStateService: ScreenStateService<T>) {}
}

View File

@ -1,11 +1,4 @@
import { import { AfterViewInit, Directive, ElementRef, HostListener, Input, OnDestroy } from '@angular/core';
AfterViewInit,
Directive,
ElementRef,
HostListener,
Input,
OnDestroy
} from '@angular/core';
import { debounce } from '@utils/debounce'; import { debounce } from '@utils/debounce';
@Directive({ @Directive({
@ -32,9 +25,8 @@ export class SyncWidthDirective implements AfterViewInit, OnDestroy {
@debounce(10) @debounce(10)
matchWidth() { matchWidth() {
const headerItems = this._elementRef.nativeElement.children; const headerItems = this._elementRef.nativeElement.children;
const tableRows = this._elementRef.nativeElement.parentElement.getElementsByClassName( // const tableRows = document.getElementsByClassName(this.redactionSyncWidth);
this.redactionSyncWidth const tableRows = this._elementRef.nativeElement.parentElement.getElementsByClassName(this.redactionSyncWidth);
);
if (!tableRows || !tableRows.length) { if (!tableRows || !tableRows.length) {
return; return;
@ -48,12 +40,8 @@ export class SyncWidthDirective implements AfterViewInit, OnDestroy {
for (let idx = 0; idx < length - hasExtraColumns - 1; ++idx) { for (let idx = 0; idx < length - hasExtraColumns - 1; ++idx) {
if (headerItems[idx]) { if (headerItems[idx]) {
headerItems[idx].style.width = `${ headerItems[idx].style.width = `${tableRow.children[idx].getBoundingClientRect().width}px`;
tableRow.children[idx].getBoundingClientRect().width headerItems[idx].style.minWidth = `${tableRow.children[idx].getBoundingClientRect().width}px`;
}px`;
headerItems[idx].style.minWidth = `${
tableRow.children[idx].getBoundingClientRect().width
}px`;
} }
} }

View File

@ -1,5 +1,5 @@
import { Pipe, PipeTransform } from '@angular/core'; import { Pipe, PipeTransform } from '@angular/core';
import { SortingService } from '@services/sorting.service'; import { SortingService } from '../../../services/sorting.service';
@Pipe({ name: 'sortBy' }) @Pipe({ name: 'sortBy' })
export class SortByPipe implements PipeTransform { export class SortByPipe implements PipeTransform {

View File

@ -1,7 +1,7 @@
import { ChangeDetectorRef, Injectable } from '@angular/core'; import { ChangeDetectorRef, Injectable } from '@angular/core';
import { FilterModel } from '@shared/components/filters/popup-filter/model/filter.model'; import { FilterModel } from '@shared/components/filters/popup-filter/model/filter.model';
import { getFilteredEntities, processFilters } from '@shared/components/filters/popup-filter/utils/filter-utils'; import { getFilteredEntities, processFilters } from '@shared/components/filters/popup-filter/utils/filter-utils';
import { FilterWrapper } from '@shared/components/filters/popup-filter/model/filter-wrapper.model'; import { FilterGroup } from '@shared/components/filters/popup-filter/model/filter-wrapper.model';
import { ScreenStateService } from '@shared/services/screen-state.service'; import { ScreenStateService } from '@shared/services/screen-state.service';
import { SearchService } from '@shared/services/search.service'; import { SearchService } from '@shared/services/search.service';
import { BehaviorSubject, Observable } from 'rxjs'; import { BehaviorSubject, Observable } from 'rxjs';
@ -9,7 +9,7 @@ import { distinctUntilChanged, filter, map } from 'rxjs/operators';
@Injectable() @Injectable()
export class FilterService<T> { export class FilterService<T> {
_allFilters$ = new BehaviorSubject<FilterWrapper[]>([]); _filterGroups$ = new BehaviorSubject<FilterGroup[]>([]);
constructor( constructor(
private readonly _screenStateService: ScreenStateService<T>, private readonly _screenStateService: ScreenStateService<T>,
@ -17,70 +17,72 @@ export class FilterService<T> {
private readonly _changeDetector: ChangeDetectorRef private readonly _changeDetector: ChangeDetectorRef
) {} ) {}
get filters() { get filterGroups(): FilterGroup[] {
return Object.values(this._allFilters$.getValue()); return Object.values(this._filterGroups$.getValue());
} }
get showResetFilters$() { get showResetFilters$(): Observable<boolean> {
return this.allFilters$.pipe( return this.filterGroups$.pipe(
map(all => this._toFlatFilters(all)), map(all => this._toFlatFilters(all)),
map(f => !!f.find(el => el.checked)), map(f => !!f.find(el => el.checked)),
distinctUntilChanged() distinctUntilChanged()
); );
} }
filterChecked$(slug: string, key: string) { filterChecked$(slug: string, key: string): Observable<boolean> {
const filters = this.getFilter$(slug); return this.getFilterModels$(slug).pipe(
return filters.pipe(map(all => all.find(f => f.key === key)?.checked)); map(all => all.find(f => f.key === key)?.checked),
distinctUntilChanged()
);
} }
toggleFilter(slug: string, key: string) { toggleFilter(slug: string, key: string) {
const filters = this.filters.find(f => f.slug === slug); const filters = this.filterGroups.find(f => f.slug === slug);
let found = filters.values.find(f => f.key === key); let found = filters.values.find(f => f.key === key);
if (!found) found = filters.values.map(f => f.filters?.find(ff => ff.key === key))[0]; if (!found) found = filters.values.map(f => f.filters?.find(ff => ff.key === key))[0];
found.checked = !found.checked; found.checked = !found.checked;
this._allFilters$.next(this.filters); this._filterGroups$.next(this.filterGroups);
this.filterEntities(); this.applyFilters();
} }
filterEntities(): void { applyFilters(): void {
const filtered = getFilteredEntities(this._screenStateService.allEntities, this.filters); const filtered = getFilteredEntities(this._screenStateService.allEntities, this.filterGroups);
this._screenStateService.setFilteredEntities(filtered); this._screenStateService.setDisplayedEntities(filtered);
this._searchService.executeSearchImmediately(); this._searchService.executeSearchImmediately();
this._changeDetector.detectChanges(); this._changeDetector.detectChanges();
} }
addFilter(value: FilterWrapper): void { addFilterGroup(value: FilterGroup): void {
const oldFilters = this.getFilter(value.slug)?.values; const oldFilters = this.getFilterGroup(value.slug)?.values;
if (!oldFilters) return this._allFilters$.next([...this.filters, value]); if (!oldFilters) return this._filterGroups$.next([...this.filterGroups, value]);
value.values = processFilters(oldFilters, value.values); value.values = processFilters(oldFilters, value.values);
this._allFilters$.next([...this.filters.filter(f => f.slug !== value.slug), value]); this._filterGroups$.next([...this.filterGroups.filter(f => f.slug !== value.slug), value]);
} }
getFilter(slug: string): FilterWrapper { getFilterGroup(slug: string): FilterGroup {
return this.filters.find(f => f?.slug === slug); return this.filterGroups.find(f => f?.slug === slug);
} }
getFilter$(slug: string): Observable<FilterModel[]> { getFilterModels$(filterGroupSlug: string): Observable<FilterModel[]> {
return this.getFilterWrapper$(slug).pipe( return this.getFilterGroup$(filterGroupSlug).pipe(
filter(f => f !== null && f !== undefined), filter(f => f !== null && f !== undefined),
map(f => f?.values) map(f => f.values)
); );
} }
getFilterWrapper$(slug: string): Observable<FilterWrapper> { getFilterGroup$(slug: string): Observable<FilterGroup> {
return this.allFilters$.pipe(map(all => all.find(f => f?.slug === slug))); return this.filterGroups$.pipe(map(all => all.find(f => f?.slug === slug)));
} }
get allFilters$(): Observable<FilterWrapper[]> { get filterGroups$(): Observable<FilterGroup[]> {
return this._allFilters$.asObservable(); return this._filterGroups$.asObservable();
} }
reset(): void { reset(): void {
this.filters.forEach(item => { this.filterGroups.forEach(item => {
item.values.forEach(child => { item.values.forEach(child => {
child.checked = false; child.checked = false;
child.indeterminate = false; child.indeterminate = false;
@ -90,11 +92,11 @@ export class FilterService<T> {
}); });
}); });
}); });
this._allFilters$.next(this.filters); this._filterGroups$.next(this.filterGroups);
this.filterEntities(); this.applyFilters();
} }
private _toFlatFilters(entities: FilterWrapper[]): FilterModel[] { private _toFlatFilters(entities: FilterGroup[]): FilterModel[] {
const flatChildren = (filters: FilterModel[]) => (filters ?? []).reduce((acc, f) => [...acc, ...(f?.filters ?? [])], []); const flatChildren = (filters: FilterModel[]) => (filters ?? []).reduce((acc, f) => [...acc, ...(f?.filters ?? [])], []);
return entities.reduce((acc, f) => [...acc, ...f.values, ...flatChildren(f.values)], []); return entities.reduce((acc, f) => [...acc, ...f.values, ...flatChildren(f.values)], []);

View File

@ -4,78 +4,102 @@ import { distinctUntilChanged, map } from 'rxjs/operators';
@Injectable() @Injectable()
export class ScreenStateService<T> { export class ScreenStateService<T> {
allEntities$ = new BehaviorSubject<T[]>([]); _allEntities$ = new BehaviorSubject<T[]>([]);
filteredEntities$ = new BehaviorSubject<T[]>([]); _displayedEntities$ = new BehaviorSubject<T[]>([]);
displayedEntities$ = new BehaviorSubject<T[]>([]); _selectedEntities$ = new BehaviorSubject<T[]>([]);
selectedEntities$ = new BehaviorSubject<T[]>([]);
// 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);
// }
get allEntities(): T[] { get allEntities(): T[] {
return Object.values(this.allEntities$.getValue()); return Object.values(this._allEntities$.getValue());
}
get filteredEntities(): T[] {
return Object.values(this.filteredEntities$.getValue());
} }
get selectedEntities(): T[] { get selectedEntities(): T[] {
return Object.values(this.selectedEntities$.getValue()); return Object.values(this._selectedEntities$.getValue());
} }
get displayedEntities(): T[] { get displayedEntities(): T[] {
return Object.values(this.displayedEntities$.getValue()); 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> { map<K>(func: (state: T[]) => K): Observable<K> {
return this.allEntities$.asObservable().pipe( return this.allEntities$.pipe(
map((state: T[]) => func(state)), map((state: T[]) => func(state)),
distinctUntilChanged() distinctUntilChanged()
); );
} }
setEntities(newEntities: Partial<T[]>): void { setEntities(newEntities: Partial<T[]>): void {
this.allEntities$.next(newEntities); this._allEntities$.next(newEntities);
}
setFilteredEntities(newEntities: Partial<T[]>): void {
this.filteredEntities$.next(newEntities);
} }
setSelectedEntities(newEntities: Partial<T[]>): void { setSelectedEntities(newEntities: Partial<T[]>): void {
this.selectedEntities$.next(newEntities); this._selectedEntities$.next(newEntities);
} }
setDisplayedEntities(newEntities: Partial<T[]>): void { setDisplayedEntities(newEntities: Partial<T[]>): void {
this.displayedEntities$.next(newEntities); this._displayedEntities$.next(newEntities);
} }
get noData$(): Observable<boolean> { get noData$(): Observable<boolean> {
return this.allEntitiesLength$.pipe(map(length => length === 0)); return this.allEntitiesLength$.pipe(
map(length => length === 0),
distinctUntilChanged()
);
} }
/** /**
* Returns the length of all entities * Returns the length of all entities
*/ */
get allEntitiesLength$(): Observable<number> { get allEntitiesLength$(): Observable<number> {
return this.allEntities$.pipe(map(all => all?.length ?? 0)); return this.allEntities$.pipe(
map(all => all?.length ?? 0),
distinctUntilChanged()
);
} }
/** /**
* Returns the length of the currently displayed entities * Returns the length of the currently displayed entities
*/ */
get displayedLength$(): Observable<number> { get displayedLength$(): Observable<number> {
return this.displayedEntities$.pipe(map(all => all?.length ?? 0)); return this.displayedEntities$.pipe(
map(all => all?.length ?? 0),
distinctUntilChanged()
);
} }
/** /**
* Returns the length of the selected entities * Returns the length of the selected entities
*/ */
get selectedLength$(): Observable<number> { get selectedLength$(): Observable<number> {
return this.selectedEntities$.pipe(map(all => all?.length ?? 0)); return this.selectedEntities$.pipe(
map(all => all?.length ?? 0),
distinctUntilChanged()
);
} }
get areAllEntitiesSelected$(): Observable<boolean> { get areAllEntitiesSelected$(): Observable<boolean> {
return combineLatest([this.displayedLength$, this.selectedLength$]).pipe( return combineLatest([this.displayedLength$, this.selectedLength$]).pipe(
map(([displayedLength, selectedLength]) => displayedLength && displayedLength === selectedLength) map(([displayedLength, selectedLength]) => displayedLength && displayedLength === selectedLength),
distinctUntilChanged()
); );
} }
@ -83,7 +107,10 @@ export class ScreenStateService<T> {
* Indicates that some entities are selected. If all are selected this returns true * Indicates that some entities are selected. If all are selected this returns true
*/ */
get areSomeEntitiesSelected$(): Observable<boolean> { get areSomeEntitiesSelected$(): Observable<boolean> {
return this.selectedLength$.pipe(map(value => value > 0)); return this.selectedLength$.pipe(
map(value => value > 0),
distinctUntilChanged()
);
} }
/** /**
@ -91,7 +118,8 @@ export class ScreenStateService<T> {
*/ */
get notAllEntitiesSelected$(): Observable<boolean> { get notAllEntitiesSelected$(): Observable<boolean> {
return combineLatest([this.areAllEntitiesSelected$, this.areSomeEntitiesSelected$]).pipe( return combineLatest([this.areAllEntitiesSelected$, this.areSomeEntitiesSelected$]).pipe(
map(([allEntitiesAreSelected, someEntitiesAreSelected]) => !allEntitiesAreSelected && someEntitiesAreSelected) map(([allEntitiesAreSelected, someEntitiesAreSelected]) => !allEntitiesAreSelected && someEntitiesAreSelected),
distinctUntilChanged()
); );
} }
@ -114,7 +142,6 @@ export class ScreenStateService<T> {
logCurrentState(): void { logCurrentState(): void {
console.log('Entities', this.allEntities); console.log('Entities', this.allEntities);
console.log('Displayed', this.displayedEntities); console.log('Displayed', this.displayedEntities);
console.log('Filtered', this.filteredEntities);
console.log('Selected', this.selectedEntities); console.log('Selected', this.selectedEntities);
} }

View File

@ -29,14 +29,9 @@ export class SearchService<T> {
} }
executeSearchImmediately(): void { executeSearchImmediately(): void {
const displayed = this._screenStateService.filteredEntities.length if (!this._searchKey) return;
? this._screenStateService.filteredEntities
: this._screenStateService.allEntities;
if (!this._searchKey) {
return this._screenStateService.setDisplayedEntities(displayed);
}
const displayed = this._screenStateService.displayedEntities;
this._screenStateService.setDisplayedEntities( this._screenStateService.setDisplayedEntities(
displayed.filter(entity => this._searchField(entity).toLowerCase().includes(this._searchValue)) displayed.filter(entity => this._searchField(entity).toLowerCase().includes(this._searchValue))
); );

View File

@ -24,7 +24,7 @@ import { DictionaryAnnotationIconComponent } from './components/dictionary-annot
import { HiddenActionComponent } from './components/hidden-action/hidden-action.component'; import { HiddenActionComponent } from './components/hidden-action/hidden-action.component';
import { ConfirmationDialogComponent } from './dialogs/confirmation-dialog/confirmation-dialog.component'; import { ConfirmationDialogComponent } from './dialogs/confirmation-dialog/confirmation-dialog.component';
import { EmptyStateComponent } from './components/empty-state/empty-state.component'; import { EmptyStateComponent } from './components/empty-state/empty-state.component';
import { SortByPipe } from './components/sort-pipe/sort-by.pipe'; import { SortByPipe } from './pipes/sort-by.pipe';
import { RoundCheckboxComponent } from './components/checkbox/round-checkbox.component'; import { RoundCheckboxComponent } from './components/checkbox/round-checkbox.component';
import { DateAdapter, MAT_DATE_FORMATS, MAT_DATE_LOCALE } from '@angular/material/core'; import { DateAdapter, MAT_DATE_FORMATS, MAT_DATE_LOCALE } from '@angular/material/core';
import { MomentDateAdapter } from '@angular/material-moment-adapter'; import { MomentDateAdapter } from '@angular/material-moment-adapter';
@ -39,6 +39,7 @@ import { AssignUserDropdownComponent } from './components/assign-user-dropdown/a
import { InputWithActionComponent } from '@shared/components/input-with-action/input-with-action.component'; import { InputWithActionComponent } from '@shared/components/input-with-action/input-with-action.component';
import { PageHeaderComponent } from './components/page-header/page-header.component'; import { PageHeaderComponent } from './components/page-header/page-header.component';
import { DatePipe } from '@shared/pipes/date.pipe'; import { DatePipe } from '@shared/pipes/date.pipe';
import { TableHeaderComponent } from './components/table-header/table-header.component';
const buttons = [ChevronButtonComponent, CircleButtonComponent, FileDownloadBtnComponent, IconButtonComponent, UserButtonComponent]; const buttons = [ChevronButtonComponent, CircleButtonComponent, FileDownloadBtnComponent, IconButtonComponent, UserButtonComponent];
@ -73,9 +74,9 @@ const utils = [HumanizePipe, DatePipe, SyncWidthDirective, HasScrollbarDirective
const modules = [MatConfigModule, TranslateModule, ScrollingModule, IconsModule, FormsModule, ReactiveFormsModule]; const modules = [MatConfigModule, TranslateModule, ScrollingModule, IconsModule, FormsModule, ReactiveFormsModule];
@NgModule({ @NgModule({
declarations: [...components, ...utils], declarations: [...components, ...utils, TableHeaderComponent],
imports: [CommonModule, ...modules, MonacoEditorModule], imports: [CommonModule, ...modules, MonacoEditorModule],
exports: [...modules, ...components, ...utils], exports: [...modules, ...components, ...utils, TableHeaderComponent],
providers: [ providers: [
{ provide: DateAdapter, useClass: MomentDateAdapter, deps: [MAT_DATE_LOCALE] }, { provide: DateAdapter, useClass: MomentDateAdapter, deps: [MAT_DATE_LOCALE] },
{ {