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"
[actionsTemplate]="annotationFilterActionTemplate"
[filters]="annotationFilters"
[enableFilterOptions]="true"
></redaction-filter>
</div>
</div>

View File

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

View File

@ -218,7 +218,7 @@ export class FilePreviewScreenComponent implements OnInit, OnDestroy, OnAttach,
);
const annotationFilters = this._annotationProcessingService.getAnnotationFilter(this.annotations);
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] Annotation Redraw and filter rebuild time: ' +
@ -527,7 +527,7 @@ export class FilePreviewScreenComponent implements OnInit, OnDestroy, OnAttach,
const oldPageSpecificFilters = this._annotationProcessingService.getAnnotationFilter(currentPageAnnotations);
const newPageSpecificFilters = this._annotationProcessingService.getAnnotationFilter(newPageAnnotations);
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 { AppStateService } from '../../../state/app-state.service';
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 { SuperTypeSorter } from '../../../utils/sorters/super-type-sorter';
@ -11,7 +11,7 @@ export class AnnotationProcessingService {
getAnnotationFilter(annotations: AnnotationWrapper[]): FilterModel[] {
const filterMap = new Map<string, FilterModel>();
const filters: FilterModel[] = [];
let filters: FilterModel[] = [];
annotations?.forEach((a) => {
const topLevelFilter = a.superType !== 'hint' && a.superType !== 'redaction' && a.superType !== 'recommendation';
@ -28,7 +28,13 @@ export class AnnotationProcessingService {
if (!parentFilter) {
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);
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 = {
key: key,
type: type || FilterTypes.primary,
topLevelFilter: true,
matches: 1,
label: 'annotation-type.' + key,
@ -62,38 +72,40 @@ export class AnnotationProcessingService {
return filter;
}
filterAndGroupAnnotations(
annotations: AnnotationWrapper[],
filters: FilterModel[],
extraFilterBy?: (annotation: AnnotationWrapper) => boolean
): { [key: number]: { annotations: AnnotationWrapper[] } } {
filterAndGroupAnnotations(annotations: AnnotationWrapper[], filters: FilterModel[]): { [key: number]: { annotations: AnnotationWrapper[] } } {
const obj = {};
const hasActiveFilters = this._hasActiveFilters(filters);
const flatFilters = [];
const flatFilters: FilterModel[] = [];
filters.forEach((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 type = annotation.superType;
if (hasActiveFilters) {
let found = false;
for (const filter of flatFilters) {
for (const filter of primaryFilters) {
if (
filter.checked &&
((filter.key === annotation.dictionary &&
(filter.key === annotation.dictionary &&
(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;
}
}
@ -139,4 +151,20 @@ export class AnnotationProcessingService {
private _hasActiveFilters(filters: FilterModel[]): boolean {
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>
</div>
<div *ngFor="let filter of filters">
<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="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 *ngFor="let filter of primaryFilters">
<ng-template
*ngTemplateOutlet="defaultFilterTemplate; context: { filter: filter, atLeastOneIsExpandable: atLeastOneFilterIsExpandable }"
></ng-template>
</div>
<div class="filter-options" *ngIf="enableFilterOptions">
<div class="filter-options" *ngIf="secondaryFilters?.length">
<div class="filter-menu-options">
<div class="all-caps-label" translate="filter-menu.filter-options"></div>
</div>
<div class="mat-menu-item flex">
<mat-checkbox class="filter-menu-checkbox" (click)="filterOptionsCheckboxClicked($event)" [checked]="filterOnlyWithComments">
<mat-icon svgIcon="red:comment"></mat-icon>
{{ 'filter-menu.with-comments' | translate }}
</mat-checkbox>
<!-- <div class="mat-menu-item flex">-->
<!-- <mat-checkbox class="filter-menu-checkbox" (click)="filterOptionsCheckboxClicked($event)" [checked]="filterOnlyWithComments">-->
<!-- <mat-icon svgIcon="red:comment"></mat-icon>-->
<!-- {{ 'filter-menu.with-comments' | translate }}-->
<!-- </mat-checkbox>-->
<!-- </div>-->
<div *ngFor="let filter of secondaryFilters">
<ng-template
*ngTemplateOutlet="defaultFilterTemplate; context: { filter: filter, atLeastOneIsExpandable: atLeastOneSecondaryFilterIsExpandable }"
></ng-template>
</div>
</div>
</div>
</mat-menu>
<ng-template #defaultFilterTemplate let-filter="filter">
<ng-template #defaultFilterLabelTemplate let-filter="filter">
{{ filter?.label }}
</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 { FilterModel } from './model/filter.model';
import { FilterModel, FilterTypes } from './model/filter.model';
import { handleCheckedValue } from './utils/filter-utils';
import { MatMenuTrigger } from '@angular/material/menu';
import { MAT_CHECKBOX_DEFAULT_OPTIONS } from '@angular/material/checkbox';
import { AnnotationWrapper } from '../../../../models/file/annotation.wrapper';
@Component({
selector: 'redaction-filter',
@ -21,31 +19,30 @@ import { AnnotationWrapper } from '../../../../models/file/annotation.wrapper';
]
})
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() actionsTemplate: TemplateRef<any>;
@Input() filters: FilterModel[] = [];
@Input() filterLabel = 'filter-menu.label';
@Input() enableFilterOptions = false;
@Input() icon: string;
@Input() chevron = false;
@ViewChild(MatMenuTrigger) trigger: MatMenuTrigger;
mouseOver = true;
mouseOverTimeout: number;
atLeastOnFilterIsExpandable = false;
filterOnlyWithComments = false;
atLeastOneFilterIsExpandable = false;
atLeastOneSecondaryFilterIsExpandable = false;
constructor(public readonly appStateService: AppStateService, private readonly _changeDetectorRef: ChangeDetectorRef) {}
ngOnChanges(changes: SimpleChanges): void {
this.atLeastOnFilterIsExpandable = false;
if (this.filters) {
this.filters.forEach((f) => {
this.atLeastOnFilterIsExpandable = this.atLeastOnFilterIsExpandable || this.isExpandable(f);
});
}
this.atLeastOneFilterIsExpandable = false;
this.atLeastOneSecondaryFilterIsExpandable = false;
this.filters?.forEach((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) {
@ -65,12 +62,6 @@ export class FilterComponent implements OnChanges {
this.applyFilters();
}
filterOptionsCheckboxClicked($event: Event) {
$event.stopPropagation();
this.filterOnlyWithComments = !this.filterOnlyWithComments;
this.applyFilters();
}
activateAllFilters() {
this._setAllFilters(true);
}
@ -88,12 +79,16 @@ export class FilterComponent implements OnChanges {
return false;
}
applyFilters() {
this.filtersChanged.emit({ filters: this.filters, extraFilterBy: this._extraFilterBy });
get primaryFilters() {
return this.filters?.filter((f) => f.type === FilterTypes.primary || f.type === undefined);
}
private get _extraFilterBy(): (a: AnnotationWrapper) => boolean {
return this.enableFilterOptions && this.filterOnlyWithComments ? (a) => a.comments.length !== 0 : (a) => true;
get secondaryFilters() {
return this.filters?.filter((f) => f.type === FilterTypes.secondary);
}
applyFilters() {
this.filtersChanged.emit(this.filters);
}
toggleFilterExpanded($event: MouseEvent, filter: FilterModel) {

View File

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