refactored filters to separate component

This commit is contained in:
Timo Bejan 2020-11-01 13:57:33 +02:00
parent d3db348fe4
commit 6ae16ae5e9
14 changed files with 269 additions and 242 deletions

View File

@ -63,7 +63,7 @@ import { HumanizePipe } from './utils/humanize.pipe';
import { ManualAnnotationDialogComponent } from './dialogs/manual-redaction-dialog/manual-annotation-dialog.component';
import { FileNotAvailableOverlayComponent } from './screens/file/file-not-available-overlay/file-not-available-overlay.component';
import { ToastComponent } from './components/toast/toast.component';
import { AnnotationFilterComponent } from './screens/file/annotation-filter/annotation-filter.component';
import { FilterComponent } from './common/filter/filter.component';
export function HttpLoaderFactory(httpClient: HttpClient) {
return new TranslateHttpLoader(httpClient, '/assets/i18n/', '.json');
@ -92,7 +92,7 @@ export function HttpLoaderFactory(httpClient: HttpClient) {
HumanizePipe,
ToastComponent,
FileNotAvailableOverlayComponent,
AnnotationFilterComponent
FilterComponent
],
imports: [
BrowserModule,

View File

@ -44,11 +44,13 @@
(change)="filterCheckboxClicked($event, filter)"
color="primary"
>
<redaction-annotation-icon
[typeValue]="appStateService.getDictionaryTypeValue(filter.key)"
></redaction-annotation-icon>
{{ 'file-preview.filter-menu.' + filter.key + '.label' | translate }}
<ng-template
*ngTemplateOutlet="
filterTemplate ? filterTemplate : defaultFilterTemplate;
context: { filter: filter }
"
>
</ng-template>
</mat-checkbox>
</div>
<div *ngIf="filter.filters && filter.expanded">
@ -63,13 +65,20 @@
(change)="filterCheckboxClicked($event, subFilter, filter)"
color="primary"
>
<redaction-annotation-icon
[typeValue]="appStateService.getDictionaryTypeValue(subFilter.key)"
></redaction-annotation-icon>
{{ appStateService.getDictionaryLabel(subFilter.key) }}
<ng-template
*ngTemplateOutlet="
filterTemplate ? filterTemplate : defaultFilterTemplate;
context: { filter: subFilter }
"
>
</ng-template>
</mat-checkbox>
</div>
</div>
</div>
</mat-menu>
</div>
<ng-template #defaultFilterTemplate let-filter="filter">
{{ filter?.key }}
</ng-template>

View File

@ -1,4 +1,4 @@
@import '../../../../assets/styles/red-variables';
@import '../../../assets/styles/red-variables';
.filter-root {
position: relative;

View File

@ -0,0 +1,77 @@
import {
Component,
EventEmitter,
Input,
OnChanges,
Output,
SimpleChanges,
TemplateRef
} from '@angular/core';
import { ManualRedactions } from '@redaction/red-ui-http';
import { AppStateService } from '../../state/app-state.service';
import { FilterModel } from './model/filter.model';
import { handleCheckedValue } from './utils/filter-utils';
@Component({
selector: 'redaction-filter',
templateUrl: './filter.component.html',
styleUrls: ['./filter.component.scss']
})
export class FilterComponent implements OnChanges {
@Input() filterTemplate: TemplateRef<any>;
@Input() manualRedactions: ManualRedactions;
@Output() filtersChanged = new EventEmitter<FilterModel[]>();
@Input() filters: FilterModel[] = [];
constructor(public readonly appStateService: AppStateService) {}
ngOnChanges(changes: SimpleChanges): void {
// this.filtersChanged.emit(this.filters);
}
filterCheckboxClicked($event: any, filter: FilterModel, parent?: FilterModel) {
filter.checked = !filter.checked;
if (parent) {
handleCheckedValue(parent);
} else {
filter.indeterminate = false;
filter.filters?.forEach((f) => (f.checked = filter.checked));
}
}
activateAllFilters() {
this._setAlLFilters(true);
}
deactivateAllFilters() {
this._setAlLFilters(false);
}
get hasActiveFilters(): boolean {
for (const filter of this.filters ? this.filters : []) {
if (filter.checked || filter.indeterminate) {
return true;
}
}
return false;
}
applyFilters() {
this.filtersChanged.emit(this.filters);
}
toggleFilterExpanded($event: MouseEvent, filter: FilterModel) {
$event.stopPropagation();
filter.expanded = !filter.expanded;
}
private _setAlLFilters(value: boolean) {
this.filters?.forEach((f) => {
f.checked = value;
f.indeterminate = value;
f.filters.forEach((ff) => {
ff.checked = value;
});
});
}
}

View File

@ -0,0 +1,8 @@
export interface FilterModel {
key: string;
label?: string;
checked?: boolean;
indeterminate?: boolean;
expanded?: boolean;
filters?: FilterModel[];
}

View File

@ -0,0 +1,10 @@
import { FilterModel } from '../model/filter.model';
export function handleCheckedValue(filter: FilterModel) {
filter.checked = filter.filters.reduce((acc, next) => acc && next.checked, true);
if (filter.checked) {
filter.indeterminate = false;
} else {
filter.indeterminate = filter.filters.reduce((acc, next) => acc || next.checked, false);
}
}

View File

@ -1,118 +0,0 @@
import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core';
import { AnnotationWrapper } from '../model/annotation.wrapper';
import { ManualRedactions } from '@redaction/red-ui-http';
import { AnnotationFilter } from '../../../utils/types';
import { AppStateService } from '../../../state/app-state.service';
@Component({
selector: 'redaction-annotation-filter',
templateUrl: './annotation-filter.component.html',
styleUrls: ['./annotation-filter.component.scss']
})
export class AnnotationFilterComponent implements OnChanges {
@Input() annotations: AnnotationWrapper[];
@Input() manualRedactions: ManualRedactions;
@Output() filtersChanged = new EventEmitter<AnnotationFilter[]>();
filters: AnnotationFilter[] = [];
constructor(public readonly appStateService: AppStateService) {}
ngOnChanges(changes: SimpleChanges): void {
if (this.annotations) {
this.filters = this._getFilters();
this.filtersChanged.emit(this.filters);
}
}
filterCheckboxClicked($event: any, filter: AnnotationFilter, parent?: AnnotationFilter) {
filter.checked = !filter.checked;
if (parent) {
this._handleCheckedValue(parent);
} else {
filter.indeterminate = false;
filter.filters.forEach((f) => (f.checked = filter.checked));
}
}
activateAllFilters() {
this._setAlLFilters(true);
}
deactivateAllFilters() {
this._setAlLFilters(false);
}
get hasActiveFilters(): boolean {
for (const filter of this.filters) {
if (filter.checked || filter.indeterminate) {
return true;
}
}
return false;
}
applyFilters() {
this.filtersChanged.emit(this.filters);
}
toggleFilterExpanded($event: MouseEvent, filter: AnnotationFilter) {
$event.stopPropagation();
filter.expanded = !filter.expanded;
}
private _setAlLFilters(value: boolean) {
this.filters.forEach((f) => {
f.checked = value;
f.indeterminate = value;
f.filters.forEach((ff) => {
ff.checked = value;
});
});
}
private _getFilters(): AnnotationFilter[] {
const filters: AnnotationFilter[] = [];
const availableAnnotationTypes = {};
this.annotations?.forEach((a) => {
if (a.superType === 'hint' || a.superType === 'redaction') {
const entry = availableAnnotationTypes[a.superType];
if (!entry) {
availableAnnotationTypes[a.superType] = new Set<string>([a.dictionary]);
} else {
entry.add(a.dictionary);
}
} else {
availableAnnotationTypes[a.superType] = new Set<string>();
}
});
for (const key of Object.keys(availableAnnotationTypes)) {
const filter: AnnotationFilter = {
key: key,
filters: Array.from(availableAnnotationTypes[key]).map((dc) => {
const defaultFilter = this.appStateService.dictionaryData[dc]?.defaultFilter;
return { key: dc, checked: defaultFilter, filters: [] };
})
};
this._handleCheckedValue(filter);
if (filter.checked || filter.indeterminate) {
filter.expanded = true;
}
filters.push(filter);
}
return filters;
}
private _handleCheckedValue(filter: AnnotationFilter) {
filter.checked = filter.filters.reduce((acc, next) => acc && next.checked, true);
if (filter.checked) {
filter.indeterminate = false;
} else {
filter.indeterminate = filter.filters.reduce((acc, next) => acc || next.checked, false);
}
}
}

View File

@ -87,11 +87,12 @@
<div class="right-fixed-container">
<div class="right-title heading" translate="file-preview.tabs.annotations.label">
<redaction-annotation-filter
[annotations]="annotations"
<redaction-filter
[filterTemplate]="annotationFilterTemplate"
[filters]="filters"
[manualRedactions]="fileData?.manualRedactions"
(filtersChanged)="filtersChanged($event)"
></redaction-annotation-filter>
></redaction-filter>
</div>
<div class="right-content">
@ -193,3 +194,12 @@
<redaction-full-page-loading-indicator
[displayed]="!viewReady"
></redaction-full-page-loading-indicator>
<ng-template #annotationFilterTemplate let-filter="filter">
<ng-container>
<redaction-annotation-icon
[typeValue]="appStateService.getDictionaryTypeValue(filter.key)"
></redaction-annotation-icon>
{{ filter.label ? (filter.label | translate) : (filter.key | humanize) }}
</ng-container>
</ng-template>

View File

@ -8,11 +8,10 @@ import {
ViewChild
} from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { ManualRedactionEntry, ReanalysisControllerService } from '@redaction/red-ui-http';
import { ReanalysisControllerService } from '@redaction/red-ui-http';
import { AppStateService } from '../../../state/app-state.service';
import { WebViewerInstance } from '@pdftron/webviewer';
import { PdfViewerComponent } from '../pdf-viewer/pdf-viewer.component';
import { AnnotationUtils } from '../../../utils/annotation-utils';
import { UserService } from '../../../user/user.service';
import { debounce } from '../../../utils/debounce';
import scrollIntoView from 'scroll-into-view-if-needed';
@ -22,14 +21,14 @@ import { FileType } from '../model/file-type';
import { DialogService } from '../../../dialogs/dialog.service';
import { MatDialogRef, MatDialogState } from '@angular/material/dialog';
import { ManualRedactionEntryWrapper } from '../model/manual-redaction-entry.wrapper';
import { hexToRgb } from '../../../utils/functions';
import { AnnotationWrapper } from '../model/annotation.wrapper';
import { ManualAnnotationService } from '../service/manual-annotation.service';
import { ManualAnnotationResponse } from '../model/manual-annotation-response';
import { FileDataModel } from '../model/file-data.model';
import { AnnotationFilter } from '../../../utils/types';
import { FileActionService } from '../service/file-action.service';
import { AnnotationDrawService } from '../service/annotation-draw.service';
import { AnnotationProcessingService } from '../service/annotation-processing.service';
import { FilterModel } from '../../../common/filter/model/filter.model';
@Component({
selector: 'redaction-file-preview-screen',
@ -53,6 +52,7 @@ export class FilePreviewScreenComponent implements OnInit {
public selectedAnnotation: AnnotationWrapper;
public pagesPanelActive = true;
public viewReady = false;
filters: FilterModel[];
constructor(
public readonly appStateService: AppStateService,
@ -61,6 +61,7 @@ export class FilePreviewScreenComponent implements OnInit {
private readonly _activatedRoute: ActivatedRoute,
private readonly _dialogService: DialogService,
private readonly _router: Router,
private readonly _annotationProcessingService: AnnotationProcessingService,
private readonly _annotationDrawService: AnnotationDrawService,
private readonly _fileActionService: FileActionService,
private readonly _manualAnnotationService: ManualAnnotationService,
@ -111,6 +112,9 @@ export class FilePreviewScreenComponent implements OnInit {
this.annotations.push(...manualRedactionAnnotations);
this.annotations.push(...redactionLogAnnotations);
this.filters = this._annotationProcessingService.getAnnotationFilter(
this.annotations
);
this._changeDetectorRef.detectChanges();
});
}
@ -402,8 +406,8 @@ export class FilePreviewScreenComponent implements OnInit {
);
}
filtersChanged(filters: AnnotationFilter[]) {
this.displayedAnnotations = AnnotationUtils.filterAndGroupAnnotations(
filtersChanged(filters: FilterModel[]) {
this.displayedAnnotations = this._annotationProcessingService.filterAndGroupAnnotations(
this.annotations,
filters
);

View File

@ -18,15 +18,12 @@ import {
ManualRedactions,
Rectangle
} from '@redaction/red-ui-http';
import WebViewer, { Annotations, WebViewerInstance } from '@pdftron/webviewer';
import WebViewer, { WebViewerInstance } from '@pdftron/webviewer';
import { TranslateService } from '@ngx-translate/core';
import { FileDownloadService } from '../service/file-download.service';
import { Subject } from 'rxjs';
import { throttleTime } from 'rxjs/operators';
import { ManualRedactionEntryWrapper } from '../model/manual-redaction-entry.wrapper';
import { AppStateService } from '../../../state/app-state.service';
import { AnnotationWrapper } from '../model/annotation.wrapper';
import { AnnotationUtils } from '../../../utils/annotation-utils';
import { ManualAnnotationService } from '../service/manual-annotation.service';
export interface ViewerState {

View File

@ -0,0 +1,119 @@
import { Injectable } from '@angular/core';
import { AppStateService } from '../../../state/app-state.service';
import { AnnotationWrapper } from '../model/annotation.wrapper';
import { FilterModel } from '../../../common/filter/model/filter.model';
import { handleCheckedValue } from '../../../common/filter/utils/filter-utils';
@Injectable({
providedIn: 'root'
})
export class AnnotationProcessingService {
constructor(private readonly _appStateService: AppStateService) {}
getAnnotationFilter(annotations: AnnotationWrapper[]): FilterModel[] {
const filters: FilterModel[] = [];
const availableAnnotationTypes = {};
annotations?.forEach((a) => {
if (a.superType === 'hint' || a.superType === 'redaction') {
const entry = availableAnnotationTypes[a.superType];
if (!entry) {
availableAnnotationTypes[a.superType] = new Set<string>([a.dictionary]);
} else {
entry.add(a.dictionary);
}
} else {
availableAnnotationTypes[a.superType] = new Set<string>();
}
});
for (const key of Object.keys(availableAnnotationTypes)) {
const filter: FilterModel = {
key: key,
label: 'annotation-filter.super-type.' + key,
filters: Array.from(availableAnnotationTypes[key]).map((dc: string) => {
const defaultFilter = this._appStateService.dictionaryData[dc]?.defaultFilter;
return { key: dc, checked: defaultFilter, filters: [] };
})
};
handleCheckedValue(filter);
if (filter.checked || filter.indeterminate) {
filter.expanded = true;
}
filters.push(filter);
}
return filters;
}
filterAndGroupAnnotations(
annotations: AnnotationWrapper[],
filters: FilterModel[]
): { [key: number]: { annotations: AnnotationWrapper[] } } {
const obj = {};
const hasActiveFilters = this._hasActiveFilters(filters);
const flatFilters = [];
filters.forEach((filter) => {
flatFilters.push(filter);
flatFilters.push(...filter.filters);
});
for (const annotation of annotations) {
const pageNumber = annotation.pageNumber;
const type = annotation.superType;
if (hasActiveFilters) {
let found = false;
for (const filter of flatFilters) {
if (
filter.checked &&
(filter.key === annotation.dictionary ||
filter.key === annotation.superType)
) {
found = true;
break;
}
}
if (!found) {
continue;
}
}
if (!obj[pageNumber]) {
obj[pageNumber] = {
annotations: [],
hint: 0,
redaction: 0,
request: 0,
ignore: 0
};
}
obj[pageNumber].annotations.push(annotation);
obj[pageNumber][type]++;
}
Object.keys(obj).map((page) => {
obj[page].annotations = this._sortAnnotations(obj[page].annotations);
});
return obj;
}
private _sortAnnotations(annotations: AnnotationWrapper[]): AnnotationWrapper[] {
return annotations.sort((ann1, ann2) => {
if (ann1.pageNumber === ann2.pageNumber) {
if (ann1.y === ann2.y) {
return ann1.x < ann2.x ? 1 : -1;
} else {
return ann1.y < ann2.y ? 1 : -1;
}
}
return ann1.pageNumber < ann2.pageNumber ? -1 : 1;
});
}
private _hasActiveFilters(filters: FilterModel[]): boolean {
return filters.reduce((acc, next) => acc || next.checked || next.indeterminate, false);
}
}

View File

@ -1,78 +0,0 @@
import { AnnotationWrapper } from '../screens/file/model/annotation.wrapper';
import { AnnotationFilter } from './types';
import { ManualRedactionEntry } from '@redaction/red-ui-http';
import { WebViewerInstance } from '@pdftron/webviewer';
import { hexToRgb } from './functions';
export class AnnotationUtils {
public static sortAnnotations(annotations: AnnotationWrapper[]): AnnotationWrapper[] {
return annotations.sort((ann1, ann2) => {
if (ann1.pageNumber === ann2.pageNumber) {
if (ann1.y === ann2.y) {
return ann1.x < ann2.x ? 1 : -1;
} else {
return ann1.y < ann2.y ? 1 : -1;
}
}
return ann1.pageNumber < ann2.pageNumber ? -1 : 1;
});
}
public static hasActiveFilters(filters: AnnotationFilter[]): boolean {
return filters.reduce((acc, next) => acc || next.checked || next.indeterminate, false);
}
public static filterAndGroupAnnotations(
annotations: AnnotationWrapper[],
filters: AnnotationFilter[]
): { [key: number]: { annotations: AnnotationWrapper[] } } {
const obj = {};
const hasActiveFilters = AnnotationUtils.hasActiveFilters(filters);
const flatFilters = [];
filters.forEach((filter) => {
flatFilters.push(filter);
flatFilters.push(...filter.filters);
});
for (const annotation of annotations) {
const pageNumber = annotation.pageNumber;
const type = annotation.superType;
if (hasActiveFilters) {
let found = false;
for (const filter of flatFilters) {
if (
filter.checked &&
(filter.key === annotation.dictionary ||
filter.key === annotation.superType)
) {
found = true;
break;
}
}
if (!found) {
continue;
}
}
if (!obj[pageNumber]) {
obj[pageNumber] = {
annotations: [],
hint: 0,
redaction: 0,
request: 0,
ignore: 0
};
}
obj[pageNumber].annotations.push(annotation);
obj[pageNumber][type]++;
}
Object.keys(obj).map((page) => {
obj[page].annotations = this.sortAnnotations(obj[page].annotations);
});
return obj;
}
}

View File

@ -7,11 +7,3 @@ export class SortingOption {
order: string;
column: string;
}
export interface AnnotationFilter {
key: string;
checked?: boolean;
indeterminate?: boolean;
expanded?: boolean;
filters?: AnnotationFilter[];
}

View File

@ -483,18 +483,6 @@
},
"filter-types": {
"label": "Filter types"
},
"redaction": {
"label": "Redaction"
},
"hint": {
"label": "Hint"
},
"request": {
"label": "Redaction Request"
},
"ignore": {
"label": "Ignored redaction"
}
},
"tabs": {
@ -590,5 +578,14 @@
"suggestion": "Suggestion for redaction",
"dictionary": "Dictionary",
"content": "Content",
"page": "Page"
"page": "Page",
"filter": {},
"annotation-filter": {
"super-type": {
"redaction": "Redaction",
"hint": "Hint",
"request": "Redaction Request",
"ignore": "Ignored redaction"
}
}
}