Add filter option "only with comments" to annotation filters

This commit is contained in:
Dan Percic 2021-04-20 22:42:23 +03:00
parent f47795cbec
commit a4308c654d
9 changed files with 125 additions and 64 deletions

View File

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

View File

@ -45,6 +45,7 @@ export class FileWorkloadComponent {
public quickScrollLastEnabled = false;
public displayedPages: number[] = [];
public pagesPanelActive = true;
public filterOnlyWithComments = false;
@ViewChild('annotationsElement') private _annotationsElement: ElementRef;
@ViewChild('quickNavigation') private _quickNavigationElement: ElementRef;
@ -109,8 +110,8 @@ export class FileWorkloadComponent {
}
@debounce(0)
public filtersChanged(filters: FilterModel[]) {
this.displayedAnnotations = this._annotationProcessingService.filterAndGroupAnnotations(this._annotations, filters);
public filtersChanged($event: { filters: FilterModel[]; extraFilterBy?: (annotation: AnnotationWrapper) => boolean }) {
this.displayedAnnotations = this._annotationProcessingService.filterAndGroupAnnotations(this._annotations, $event.filters, $event.extraFilterBy);
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(this.annotationFilters);
this._workloadComponent.filtersChanged({ filters: 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(this.annotationFilters);
this._workloadComponent.filtersChanged({ filters: this.annotationFilters });
}
}

View File

@ -62,7 +62,11 @@ export class AnnotationProcessingService {
return filter;
}
filterAndGroupAnnotations(annotations: AnnotationWrapper[], filters: FilterModel[]): { [key: number]: { annotations: AnnotationWrapper[] } } {
filterAndGroupAnnotations(
annotations: AnnotationWrapper[],
filters: FilterModel[],
extraFilterBy?: (annotation: AnnotationWrapper) => boolean
): { [key: number]: { annotations: AnnotationWrapper[] } } {
const obj = {};
const hasActiveFilters = this._hasActiveFilters(filters);
@ -73,6 +77,10 @@ export class AnnotationProcessingService {
flatFilters.push(...filter.filters);
});
for (const annotation of annotations) {
if (typeof extraFilterBy === 'function' && !extraFilterBy(annotation)) {
continue;
}
const pageNumber = annotation.pageNumber;
const type = annotation.superType;
@ -103,6 +111,7 @@ export class AnnotationProcessingService {
skipped: 0
};
}
obj[pageNumber].annotations.push(annotation);
obj[pageNumber][type]++;
}

View File

@ -1,58 +1,63 @@
<div class="filter-root">
<redaction-icon-button
*ngIf="!chevron"
[text]="filterLabel"
[icon]="icon"
[matMenuTriggerFor]="filterMenu"
[showDot]="hasActiveFilters"
></redaction-icon-button>
<redaction-chevron-button *ngIf="chevron" [text]="filterLabel" [matMenuTriggerFor]="filterMenu" [showDot]="hasActiveFilters"></redaction-chevron-button>
<mat-menu #filterMenu="matMenu" xPosition="before" (closed)="applyFilters()">
<div (mouseleave)="filterMouseLeave()" (mouseenter)="filterMouseEnter()">
<div class="filter-menu-header">
<div class="all-caps-label" translate="filter-menu.filter-types"></div>
<div class="actions">
<div class="all-caps-label primary pointer" translate="filter-menu.all" (click)="activateAllFilters(); $event.stopPropagation()"></div>
<div class="all-caps-label primary pointer" translate="filter-menu.none" (click)="deactivateAllFilters(); $event.stopPropagation()"></div>
</div>
<redaction-icon-button
*ngIf="!chevron"
[text]="filterLabel"
[icon]="icon"
[matMenuTriggerFor]="filterMenu"
[showDot]="hasActiveFilters"
></redaction-icon-button>
<redaction-chevron-button *ngIf="chevron" [text]="filterLabel" [matMenuTriggerFor]="filterMenu" [showDot]="hasActiveFilters"></redaction-chevron-button>
<mat-menu #filterMenu="matMenu" xPosition="before" (closed)="applyFilters()">
<div (mouseleave)="filterMouseLeave()" (mouseenter)="filterMouseEnter()">
<div class="filter-menu-header">
<div class="all-caps-label" translate="filter-menu.filter-types"></div>
<div class="actions">
<div class="all-caps-label primary pointer" translate="filter-menu.all" (click)="activateAllFilters(); $event.stopPropagation()"></div>
<div class="all-caps-label primary pointer" translate="filter-menu.none" (click)="deactivateAllFilters(); $event.stopPropagation()"></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); $event.stopPropagation()"
class="filter-menu-checkbox"
color="primary"
>
<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 *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 *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); $event.stopPropagation()"
color="primary"
>
<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 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>
</mat-menu>
</div>
<div class="filter-options" *ngIf="enableFilterOptions">
<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>
</div>
</div>
</mat-menu>
<ng-template #defaultFilterTemplate let-filter="filter">
{{ filter?.label }}

View File

@ -1,5 +1,6 @@
@import '../../../../../assets/styles/red-variables';
.filter-menu-options,
.filter-menu-header {
display: flex;
justify-content: space-between;
@ -15,6 +16,26 @@
}
}
.filter-menu-options {
margin-top: 8px;
padding: 16px 16px 3px;
}
.filter-options {
background-color: $grey-2;
padding-bottom: 8px;
mat-icon {
width: 16px;
height: 16px;
margin-right: 8px;
}
}
::ng-deep .mat-menu-panel .mat-menu-content:not(:empty) {
padding-bottom: 0;
}
::ng-deep .filter-menu-checkbox {
width: 100%;

View File

@ -4,6 +4,7 @@ import { FilterModel } 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',
@ -20,11 +21,12 @@ import { MAT_CHECKBOX_DEFAULT_OPTIONS } from '@angular/material/checkbox';
]
})
export class FilterComponent implements OnChanges {
@Output() filtersChanged = new EventEmitter<FilterModel[]>();
@Output() filtersChanged = new EventEmitter<{ filters: FilterModel[]; extraFilterBy?: (annotation: AnnotationWrapper) => boolean }>();
@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;
@ -34,6 +36,7 @@ export class FilterComponent implements OnChanges {
mouseOverTimeout: number;
atLeastOnFilterIsExpandable = false;
filterOnlyWithComments = false;
constructor(public readonly appStateService: AppStateService, private readonly _changeDetectorRef: ChangeDetectorRef) {}
@ -47,6 +50,8 @@ export class FilterComponent implements OnChanges {
}
filterCheckboxClicked($event: any, filter: FilterModel, parent?: FilterModel) {
$event.stopPropagation();
filter.checked = !filter.checked;
if (parent) {
handleCheckedValue(parent);
@ -61,7 +66,13 @@ export class FilterComponent implements OnChanges {
this.applyFilters();
}
public activateAllFilters() {
filterOptionsCheckboxClicked($event: Event) {
$event.stopPropagation();
this.filterOnlyWithComments = !this.filterOnlyWithComments;
this.applyFilters();
}
activateAllFilters() {
this._setAllFilters(true);
}
@ -78,8 +89,12 @@ export class FilterComponent implements OnChanges {
return false;
}
public applyFilters() {
this.filtersChanged.emit(this.filters);
applyFilters() {
this.filtersChanged.emit({ filters: this.filters, extraFilterBy: this._extraFilterBy });
}
private get _extraFilterBy(): (a: AnnotationWrapper) => boolean {
return this.enableFilterOptions && this.filterOnlyWithComments ? (a) => a.comments.length !== 0 : (a) => true;
}
toggleFilterExpanded($event: MouseEvent, filter: FilterModel) {

View File

@ -514,7 +514,9 @@
"label": "Filter",
"all": "All",
"none": "None",
"filter-types": "Filter types"
"filter-types": "Filter types",
"filter-options": "Filter options",
"with-comments": "Show only annotations with comments"
},
"sorting": {
"recent": "Recent",

View File

@ -1,6 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="100px" height="100px" viewBox="0 0 100 100" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="comments" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<path d="M50,0 C25,0 5,20 5,45 C5,55.5 8.5,65 15,73 L15,100 L44,89.5 C46.5,90 48,90 50,90 C75,90 95,70 95,45 C95,20 75,0 50,0 Z M50,80 C48.5,80 46.5,80 44.5,79.5 L43,79.5 L25,86 L25,69.5 L23.5,68 C18,61.5 15,53.5 15,45 C15,25.5 30.5,10 50,10 C69.5,10 85,25.5 85,45 C85,64.5 69.5,80 50,80 Z" id="Shape" fill="currentColor" fill-rule="nonzero"></path>
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>status</title>
<g id="Styleguide" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Styleguide-Actions" transform="translate(-607.000000, -818.000000)" fill="#283241" fill-rule="nonzero">
<g id="Group-6" transform="translate(598.000000, 809.000000)">
<g id="status" transform="translate(9.000000, 9.000000)">
<path d="M8,0 C4,0 0.8,3.2 0.8,7.2 C0.8,8.88 1.36,10.4 2.4,11.68 L2.4,16 L7.04,14.32 C7.44,14.4 7.68,14.4 8,14.4 C12,14.4 15.2,11.2 15.2,7.2 C15.2,3.2 12,0 8,0 Z M8,12.8 C7.76,12.8 7.44,12.8 7.12,12.72 L6.88,12.72 L4,13.76 L4,11.12 L3.76,10.88 C2.88,9.84 2.4,8.56 2.4,7.2 C2.4,4.08 4.88,1.6 8,1.6 C11.12,1.6 13.6,4.08 13.6,7.2 C13.6,10.32 11.12,12.8 8,12.8 Z" id="Shape"></path>
</g>
</g>
</g>
</g>
</svg>
</svg>

Before

Width:  |  Height:  |  Size: 647 B

After

Width:  |  Height:  |  Size: 1.0 KiB