secondary filters wip

This commit is contained in:
Dan Percic 2021-04-23 17:22:35 +03:00
parent f502cda56b
commit 885269c7d1
7 changed files with 126 additions and 86 deletions

View File

@ -12,7 +12,6 @@
[filterTemplate]="annotationFilterTemplate" [filterTemplate]="annotationFilterTemplate"
[actionsTemplate]="annotationFilterActionTemplate" [actionsTemplate]="annotationFilterActionTemplate"
[filters]="annotationFilters" [filters]="annotationFilters"
[enableFilterOptions]="true"
></redaction-filter> ></redaction-filter>
</div> </div>
</div> </div>

View File

@ -106,8 +106,8 @@ export class FileWorkloadComponent {
} }
@debounce(0) @debounce(0)
public filtersChanged($event: { filters: FilterModel[]; extraFilterBy?: (annotation: AnnotationWrapper) => boolean }) { public filtersChanged(filters: FilterModel[]) {
this.displayedAnnotations = this._annotationProcessingService.filterAndGroupAnnotations(this._annotations, $event.filters, $event.extraFilterBy); this.displayedAnnotations = this._annotationProcessingService.filterAndGroupAnnotations(this._annotations, filters);
this.displayedPages = Object.keys(this.displayedAnnotations).map((key) => Number(key)); this.displayedPages = Object.keys(this.displayedAnnotations).map((key) => Number(key));
this.computeQuickNavButtonsState(); this.computeQuickNavButtonsState();
this._changeDetectorRef.markForCheck(); this._changeDetectorRef.markForCheck();

View File

@ -218,7 +218,7 @@ export class FilePreviewScreenComponent implements OnInit, OnDestroy, OnAttach,
); );
const annotationFilters = this._annotationProcessingService.getAnnotationFilter(this.annotations); const annotationFilters = this._annotationProcessingService.getAnnotationFilter(this.annotations);
this.annotationFilters = processFilters(this.annotationFilters, annotationFilters); this.annotationFilters = processFilters(this.annotationFilters, annotationFilters);
this._workloadComponent.filtersChanged({ filters: this.annotationFilters }); this._workloadComponent.filtersChanged(this.annotationFilters);
console.log('[REDACTION] Process time: ' + (new Date().getTime() - processStartTime) + 'ms'); console.log('[REDACTION] Process time: ' + (new Date().getTime() - processStartTime) + 'ms');
console.log( console.log(
'[REDACTION] Annotation Redraw and filter rebuild time: ' + '[REDACTION] Annotation Redraw and filter rebuild time: ' +
@ -527,7 +527,7 @@ export class FilePreviewScreenComponent implements OnInit, OnDestroy, OnAttach,
const oldPageSpecificFilters = this._annotationProcessingService.getAnnotationFilter(currentPageAnnotations); const oldPageSpecificFilters = this._annotationProcessingService.getAnnotationFilter(currentPageAnnotations);
const newPageSpecificFilters = this._annotationProcessingService.getAnnotationFilter(newPageAnnotations); const newPageSpecificFilters = this._annotationProcessingService.getAnnotationFilter(newPageAnnotations);
handleFilterDelta(oldPageSpecificFilters, newPageSpecificFilters, this.annotationFilters); handleFilterDelta(oldPageSpecificFilters, newPageSpecificFilters, this.annotationFilters);
this._workloadComponent.filtersChanged({ filters: this.annotationFilters }); this._workloadComponent.filtersChanged(this.annotationFilters);
} }
} }

View File

@ -1,7 +1,7 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { AppStateService } from '../../../state/app-state.service'; import { AppStateService } from '../../../state/app-state.service';
import { AnnotationWrapper } from '../../../models/file/annotation.wrapper'; import { AnnotationWrapper } from '../../../models/file/annotation.wrapper';
import { FilterModel } from '../../shared/components/filter/model/filter.model'; import { FilterModel, FilterTypes } from '../../shared/components/filter/model/filter.model';
import { handleCheckedValue } from '../../shared/components/filter/utils/filter-utils'; import { handleCheckedValue } from '../../shared/components/filter/utils/filter-utils';
import { SuperTypeSorter } from '../../../utils/sorters/super-type-sorter'; import { SuperTypeSorter } from '../../../utils/sorters/super-type-sorter';
@ -11,7 +11,7 @@ export class AnnotationProcessingService {
getAnnotationFilter(annotations: AnnotationWrapper[]): FilterModel[] { getAnnotationFilter(annotations: AnnotationWrapper[]): FilterModel[] {
const filterMap = new Map<string, FilterModel>(); const filterMap = new Map<string, FilterModel>();
const filters: FilterModel[] = []; let filters: FilterModel[] = [];
annotations?.forEach((a) => { annotations?.forEach((a) => {
const topLevelFilter = a.superType !== 'hint' && a.superType !== 'redaction' && a.superType !== 'recommendation'; const topLevelFilter = a.superType !== 'hint' && a.superType !== 'redaction' && a.superType !== 'recommendation';
@ -28,7 +28,13 @@ export class AnnotationProcessingService {
if (!parentFilter) { if (!parentFilter) {
parentFilter = this._createParentFilter(a.superType, filterMap, filters); parentFilter = this._createParentFilter(a.superType, filterMap, filters);
} }
const childFilter = { key: a.dictionary, checked: false, filters: [], matches: 1 }; const childFilter = {
key: a.dictionary,
type: FilterTypes.primary,
checked: false,
filters: [],
matches: 1
};
filterMap.set(key, childFilter); filterMap.set(key, childFilter);
parentFilter.filters.push(childFilter); parentFilter.filters.push(childFilter);
} }
@ -46,12 +52,16 @@ export class AnnotationProcessingService {
} }
} }
return filters.sort((a, b) => SuperTypeSorter[a.key] - SuperTypeSorter[b.key]); filters = filters.sort((a, b) => SuperTypeSorter[a.key] - SuperTypeSorter[b.key]);
filters.push(...AnnotationProcessingService._secondaryFilters);
return filters;
} }
private _createParentFilter(key: string, filterMap: Map<string, FilterModel>, filters: FilterModel[]) { private _createParentFilter(key: string, filterMap: Map<string, FilterModel>, filters: FilterModel[], type?: FilterTypes) {
const filter: FilterModel = { const filter: FilterModel = {
key: key, key: key,
type: type || FilterTypes.primary,
topLevelFilter: true, topLevelFilter: true,
matches: 1, matches: 1,
label: 'annotation-type.' + key, label: 'annotation-type.' + key,
@ -62,38 +72,40 @@ export class AnnotationProcessingService {
return filter; return filter;
} }
filterAndGroupAnnotations( filterAndGroupAnnotations(annotations: AnnotationWrapper[], filters: FilterModel[]): { [key: number]: { annotations: AnnotationWrapper[] } } {
annotations: AnnotationWrapper[],
filters: FilterModel[],
extraFilterBy?: (annotation: AnnotationWrapper) => boolean
): { [key: number]: { annotations: AnnotationWrapper[] } } {
const obj = {}; const obj = {};
const hasActiveFilters = this._hasActiveFilters(filters); const hasActiveFilters = this._hasActiveFilters(filters);
const flatFilters = []; const flatFilters: FilterModel[] = [];
filters.forEach((filter) => { filters.forEach((filter) => {
flatFilters.push(filter); flatFilters.push(filter);
flatFilters.push(...filter.filters); flatFilters.push(...filter?.filters);
}); });
for (const annotation of annotations) {
if (typeof extraFilterBy === 'function' && !extraFilterBy(annotation)) {
continue;
}
const primaryFilters = flatFilters.filter((f) => f.type === FilterTypes.primary && f.checked);
const secondaryFilters = flatFilters.filter((f) => f.type === FilterTypes.secondary && f.checked && f.action);
console.log(secondaryFilters);
for (const annotation of annotations) {
const pageNumber = annotation.pageNumber; const pageNumber = annotation.pageNumber;
const type = annotation.superType; const type = annotation.superType;
if (hasActiveFilters) { if (hasActiveFilters) {
let found = false; let found = false;
for (const filter of flatFilters) { for (const filter of primaryFilters) {
if ( if (
filter.checked && (filter.key === annotation.dictionary &&
((filter.key === annotation.dictionary &&
(annotation.superType === 'hint' || annotation.superType === 'redaction' || annotation.superType === 'recommendation')) || (annotation.superType === 'hint' || annotation.superType === 'redaction' || annotation.superType === 'recommendation')) ||
filter.key === annotation.superType) filter.key === annotation.superType
) { ) {
found = true; let secondaryFiltersNotMatched = false;
for (const secondaryFilter of secondaryFilters) {
if (!secondaryFilter.action(annotation)) {
secondaryFiltersNotMatched = true;
}
}
found = !secondaryFiltersNotMatched;
break; break;
} }
} }
@ -139,4 +151,20 @@ export class AnnotationProcessingService {
private _hasActiveFilters(filters: FilterModel[]): boolean { private _hasActiveFilters(filters: FilterModel[]): boolean {
return filters.reduce((acc, next) => acc || next.checked || next.indeterminate, false); return filters.reduce((acc, next) => acc || next.checked || next.indeterminate, false);
} }
private static get _secondaryFilters(): FilterModel[] {
return [
{
key: 'with-comments',
label: 'filter-menu.with-comments',
checked: false,
topLevelFilter: true,
type: FilterTypes.secondary,
filters: [],
action: (obj) => {
return obj?.comments?.length > 0;
}
}
];
}
} }

View File

@ -17,48 +17,59 @@
<div class="all-caps-label primary pointer" translate="filter-menu.none" (click)="deactivateAllFilters(); $event.stopPropagation()"></div> <div class="all-caps-label primary pointer" translate="filter-menu.none" (click)="deactivateAllFilters(); $event.stopPropagation()"></div>
</div> </div>
</div> </div>
<div *ngFor="let filter of filters"> <div *ngFor="let filter of primaryFilters">
<div class="mat-menu-item flex" (click)="toggleFilterExpanded($event, filter)"> <ng-template
<div class="arrow-wrapper" *ngIf="isExpandable(filter)"> *ngTemplateOutlet="defaultFilterTemplate; context: { filter: filter, atLeastOneIsExpandable: atLeastOneFilterIsExpandable }"
<mat-icon *ngIf="filter.expanded" svgIcon="red:arrow-down" color="accent"></mat-icon> ></ng-template>
<mat-icon *ngIf="!filter.expanded" color="accent" svgIcon="red:arrow-right"></mat-icon>
</div>
<div class="arrow-wrapper spacer" *ngIf="atLeastOnFilterIsExpandable && !isExpandable(filter)">
&nbsp;
</div>
<mat-checkbox
[checked]="filter.checked"
[indeterminate]="filter.indeterminate"
(click)="filterCheckboxClicked($event, filter)"
class="filter-menu-checkbox"
>
<ng-template *ngTemplateOutlet="filterTemplate ? filterTemplate : defaultFilterTemplate; context: { filter: filter }"></ng-template>
</mat-checkbox>
<ng-template *ngTemplateOutlet="actionsTemplate ? actionsTemplate : null; context: { filter: filter }"></ng-template>
</div>
<div *ngIf="filter.filters?.length && filter.expanded">
<div *ngFor="let subFilter of filter.filters" class="padding-left mat-menu-item" (click)="$event.stopPropagation()">
<mat-checkbox [checked]="subFilter.checked" (click)="filterCheckboxClicked($event, subFilter, filter)">
<ng-template *ngTemplateOutlet="filterTemplate ? filterTemplate : defaultFilterTemplate; context: { filter: subFilter }"> </ng-template>
</mat-checkbox>
<ng-template *ngTemplateOutlet="actionsTemplate ? actionsTemplate : null; context: { filter: subFilter }"></ng-template>
</div>
</div>
</div> </div>
<div class="filter-options" *ngIf="enableFilterOptions"> <div class="filter-options" *ngIf="secondaryFilters?.length">
<div class="filter-menu-options"> <div class="filter-menu-options">
<div class="all-caps-label" translate="filter-menu.filter-options"></div> <div class="all-caps-label" translate="filter-menu.filter-options"></div>
</div> </div>
<div class="mat-menu-item flex"> <!-- <div class="mat-menu-item flex">-->
<mat-checkbox class="filter-menu-checkbox" (click)="filterOptionsCheckboxClicked($event)" [checked]="filterOnlyWithComments"> <!-- <mat-checkbox class="filter-menu-checkbox" (click)="filterOptionsCheckboxClicked($event)" [checked]="filterOnlyWithComments">-->
<mat-icon svgIcon="red:comment"></mat-icon> <!-- <mat-icon svgIcon="red:comment"></mat-icon>-->
{{ 'filter-menu.with-comments' | translate }} <!-- {{ 'filter-menu.with-comments' | translate }}-->
</mat-checkbox> <!-- </mat-checkbox>-->
<!-- </div>-->
<div *ngFor="let filter of secondaryFilters">
<ng-template
*ngTemplateOutlet="defaultFilterTemplate; context: { filter: filter, atLeastOneIsExpandable: atLeastOneSecondaryFilterIsExpandable }"
></ng-template>
</div> </div>
</div> </div>
</div> </div>
</mat-menu> </mat-menu>
<ng-template #defaultFilterTemplate let-filter="filter"> <ng-template #defaultFilterLabelTemplate let-filter="filter">
{{ filter?.label }} {{ filter?.label }}
</ng-template> </ng-template>
<ng-template #defaultFilterTemplate let-filter="filter" let-atLeastOneIsExpandable="atLeastOneIsExpandable">
<div class="mat-menu-item flex" (click)="toggleFilterExpanded($event, filter)">
<div class="arrow-wrapper" *ngIf="isExpandable(filter)">
<mat-icon *ngIf="filter.expanded" svgIcon="red:arrow-down" color="accent"></mat-icon>
<mat-icon *ngIf="!filter.expanded" color="accent" svgIcon="red:arrow-right"></mat-icon>
</div>
<div class="arrow-wrapper spacer" *ngIf="atLeastOneIsExpandable && !isExpandable(filter)">
&nbsp;
</div>
<mat-checkbox
[checked]="filter.checked"
[indeterminate]="filter.indeterminate"
(click)="filterCheckboxClicked($event, filter)"
class="filter-menu-checkbox"
>
<ng-template *ngTemplateOutlet="filterTemplate ? filterTemplate : defaultFilterLabelTemplate; context: { filter: filter }"></ng-template>
</mat-checkbox>
<ng-template *ngTemplateOutlet="actionsTemplate ? actionsTemplate : null; context: { filter: filter }"></ng-template>
</div>
<div *ngIf="filter.filters?.length && filter.expanded">
<div *ngFor="let subFilter of filter.filters" class="padding-left mat-menu-item" (click)="$event.stopPropagation()">
<mat-checkbox [checked]="subFilter.checked" (click)="filterCheckboxClicked($event, subFilter, filter)">
<ng-template *ngTemplateOutlet="filterTemplate ? filterTemplate : defaultFilterLabelTemplate; context: { filter: subFilter }"></ng-template>
</mat-checkbox>
<ng-template *ngTemplateOutlet="actionsTemplate ? actionsTemplate : null; context: { filter: subFilter }"></ng-template>
</div>
</div>
</ng-template>

View File

@ -1,10 +1,8 @@
import { ChangeDetectorRef, Component, EventEmitter, Input, OnChanges, Output, SimpleChanges, TemplateRef, ViewChild } from '@angular/core'; import { ChangeDetectorRef, Component, EventEmitter, Input, OnChanges, Output, SimpleChanges, TemplateRef } from '@angular/core';
import { AppStateService } from '../../../../state/app-state.service'; import { AppStateService } from '../../../../state/app-state.service';
import { FilterModel } from './model/filter.model'; import { FilterModel, FilterTypes } from './model/filter.model';
import { handleCheckedValue } from './utils/filter-utils'; import { handleCheckedValue } from './utils/filter-utils';
import { MatMenuTrigger } from '@angular/material/menu';
import { MAT_CHECKBOX_DEFAULT_OPTIONS } from '@angular/material/checkbox'; import { MAT_CHECKBOX_DEFAULT_OPTIONS } from '@angular/material/checkbox';
import { AnnotationWrapper } from '../../../../models/file/annotation.wrapper';
@Component({ @Component({
selector: 'redaction-filter', selector: 'redaction-filter',
@ -21,31 +19,30 @@ import { AnnotationWrapper } from '../../../../models/file/annotation.wrapper';
] ]
}) })
export class FilterComponent implements OnChanges { export class FilterComponent implements OnChanges {
@Output() filtersChanged = new EventEmitter<{ filters: FilterModel[]; extraFilterBy?: (annotation: AnnotationWrapper) => boolean }>(); @Output() filtersChanged = new EventEmitter<FilterModel[]>();
@Input() filterTemplate: TemplateRef<any>; @Input() filterTemplate: TemplateRef<any>;
@Input() actionsTemplate: TemplateRef<any>; @Input() actionsTemplate: TemplateRef<any>;
@Input() filters: FilterModel[] = []; @Input() filters: FilterModel[] = [];
@Input() filterLabel = 'filter-menu.label'; @Input() filterLabel = 'filter-menu.label';
@Input() enableFilterOptions = false;
@Input() icon: string; @Input() icon: string;
@Input() chevron = false; @Input() chevron = false;
@ViewChild(MatMenuTrigger) trigger: MatMenuTrigger;
mouseOver = true; mouseOver = true;
mouseOverTimeout: number; mouseOverTimeout: number;
atLeastOnFilterIsExpandable = false; atLeastOneFilterIsExpandable = false;
filterOnlyWithComments = false; atLeastOneSecondaryFilterIsExpandable = false;
constructor(public readonly appStateService: AppStateService, private readonly _changeDetectorRef: ChangeDetectorRef) {} constructor(public readonly appStateService: AppStateService, private readonly _changeDetectorRef: ChangeDetectorRef) {}
ngOnChanges(changes: SimpleChanges): void { ngOnChanges(changes: SimpleChanges): void {
this.atLeastOnFilterIsExpandable = false; this.atLeastOneFilterIsExpandable = false;
if (this.filters) { this.atLeastOneSecondaryFilterIsExpandable = false;
this.filters.forEach((f) => { this.filters?.forEach((f) => {
this.atLeastOnFilterIsExpandable = this.atLeastOnFilterIsExpandable || this.isExpandable(f); if (f.type === FilterTypes.primary) this.atLeastOneFilterIsExpandable = this.atLeastOneFilterIsExpandable || this.isExpandable(f);
}); if (f.type === FilterTypes.secondary)
} this.atLeastOneSecondaryFilterIsExpandable = this.atLeastOneSecondaryFilterIsExpandable || this.isExpandable(f);
});
} }
filterCheckboxClicked($event: any, filter: FilterModel, parent?: FilterModel) { filterCheckboxClicked($event: any, filter: FilterModel, parent?: FilterModel) {
@ -65,12 +62,6 @@ export class FilterComponent implements OnChanges {
this.applyFilters(); this.applyFilters();
} }
filterOptionsCheckboxClicked($event: Event) {
$event.stopPropagation();
this.filterOnlyWithComments = !this.filterOnlyWithComments;
this.applyFilters();
}
activateAllFilters() { activateAllFilters() {
this._setAllFilters(true); this._setAllFilters(true);
} }
@ -88,12 +79,16 @@ export class FilterComponent implements OnChanges {
return false; return false;
} }
applyFilters() { get primaryFilters() {
this.filtersChanged.emit({ filters: this.filters, extraFilterBy: this._extraFilterBy }); return this.filters?.filter((f) => f.type === FilterTypes.primary || f.type === undefined);
} }
private get _extraFilterBy(): (a: AnnotationWrapper) => boolean { get secondaryFilters() {
return this.enableFilterOptions && this.filterOnlyWithComments ? (a) => a.comments.length !== 0 : (a) => true; return this.filters?.filter((f) => f.type === FilterTypes.secondary);
}
applyFilters() {
this.filtersChanged.emit(this.filters);
} }
toggleFilterExpanded($event: MouseEvent, filter: FilterModel) { toggleFilterExpanded($event: MouseEvent, filter: FilterModel) {

View File

@ -1,5 +1,11 @@
export enum FilterTypes {
primary = 'primary',
secondary = 'secondary'
}
export interface FilterModel { export interface FilterModel {
key: string; key: string;
type: FilterTypes;
label?: string; label?: string;
checked?: boolean; checked?: boolean;
indeterminate?: boolean; indeterminate?: boolean;
@ -7,4 +13,5 @@ export interface FilterModel {
topLevelFilter?: boolean; topLevelFilter?: boolean;
matches?: number; matches?: number;
filters?: FilterModel[]; filters?: FilterModel[];
action?: (obj?) => boolean;
} }