diff --git a/apps/red-ui/src/app/app.module.ts b/apps/red-ui/src/app/app.module.ts index 68d0d847e..0207ecd0b 100644 --- a/apps/red-ui/src/app/app.module.ts +++ b/apps/red-ui/src/app/app.module.ts @@ -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, diff --git a/apps/red-ui/src/app/screens/file/annotation-filter/annotation-filter.component.html b/apps/red-ui/src/app/common/filter/filter.component.html similarity index 79% rename from apps/red-ui/src/app/screens/file/annotation-filter/annotation-filter.component.html rename to apps/red-ui/src/app/common/filter/filter.component.html index 4980531ef..3a98cdd82 100644 --- a/apps/red-ui/src/app/screens/file/annotation-filter/annotation-filter.component.html +++ b/apps/red-ui/src/app/common/filter/filter.component.html @@ -44,11 +44,13 @@ (change)="filterCheckboxClicked($event, filter)" color="primary" > - - - {{ 'file-preview.filter-menu.' + filter.key + '.label' | translate }} + +
@@ -63,13 +65,20 @@ (change)="filterCheckboxClicked($event, subFilter, filter)" color="primary" > - - {{ appStateService.getDictionaryLabel(subFilter.key) }} + +
+ + + {{ filter?.key }} + diff --git a/apps/red-ui/src/app/screens/file/annotation-filter/annotation-filter.component.scss b/apps/red-ui/src/app/common/filter/filter.component.scss similarity index 88% rename from apps/red-ui/src/app/screens/file/annotation-filter/annotation-filter.component.scss rename to apps/red-ui/src/app/common/filter/filter.component.scss index 05d2d7362..cd0f453c6 100644 --- a/apps/red-ui/src/app/screens/file/annotation-filter/annotation-filter.component.scss +++ b/apps/red-ui/src/app/common/filter/filter.component.scss @@ -1,4 +1,4 @@ -@import '../../../../assets/styles/red-variables'; +@import '../../../assets/styles/red-variables'; .filter-root { position: relative; diff --git a/apps/red-ui/src/app/common/filter/filter.component.ts b/apps/red-ui/src/app/common/filter/filter.component.ts new file mode 100644 index 000000000..b6caaf079 --- /dev/null +++ b/apps/red-ui/src/app/common/filter/filter.component.ts @@ -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; + @Input() manualRedactions: ManualRedactions; + @Output() filtersChanged = new EventEmitter(); + @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; + }); + }); + } +} diff --git a/apps/red-ui/src/app/common/filter/model/filter.model.ts b/apps/red-ui/src/app/common/filter/model/filter.model.ts new file mode 100644 index 000000000..a9453e125 --- /dev/null +++ b/apps/red-ui/src/app/common/filter/model/filter.model.ts @@ -0,0 +1,8 @@ +export interface FilterModel { + key: string; + label?: string; + checked?: boolean; + indeterminate?: boolean; + expanded?: boolean; + filters?: FilterModel[]; +} diff --git a/apps/red-ui/src/app/common/filter/utils/filter-utils.ts b/apps/red-ui/src/app/common/filter/utils/filter-utils.ts new file mode 100644 index 000000000..6476d23a8 --- /dev/null +++ b/apps/red-ui/src/app/common/filter/utils/filter-utils.ts @@ -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); + } +} diff --git a/apps/red-ui/src/app/screens/file/annotation-filter/annotation-filter.component.ts b/apps/red-ui/src/app/screens/file/annotation-filter/annotation-filter.component.ts deleted file mode 100644 index d68cc6fac..000000000 --- a/apps/red-ui/src/app/screens/file/annotation-filter/annotation-filter.component.ts +++ /dev/null @@ -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(); - - 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([a.dictionary]); - } else { - entry.add(a.dictionary); - } - } else { - availableAnnotationTypes[a.superType] = new Set(); - } - }); - - 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); - } - } -} diff --git a/apps/red-ui/src/app/screens/file/file-preview-screen/file-preview-screen.component.html b/apps/red-ui/src/app/screens/file/file-preview-screen/file-preview-screen.component.html index 4aa53892e..75f5bd125 100644 --- a/apps/red-ui/src/app/screens/file/file-preview-screen/file-preview-screen.component.html +++ b/apps/red-ui/src/app/screens/file/file-preview-screen/file-preview-screen.component.html @@ -87,11 +87,12 @@
- + >
@@ -193,3 +194,12 @@ + + + + + {{ filter.label ? (filter.label | translate) : (filter.key | humanize) }} + + diff --git a/apps/red-ui/src/app/screens/file/file-preview-screen/file-preview-screen.component.ts b/apps/red-ui/src/app/screens/file/file-preview-screen/file-preview-screen.component.ts index 9c3c33a2b..b5f33ec8c 100644 --- a/apps/red-ui/src/app/screens/file/file-preview-screen/file-preview-screen.component.ts +++ b/apps/red-ui/src/app/screens/file/file-preview-screen/file-preview-screen.component.ts @@ -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 ); diff --git a/apps/red-ui/src/app/screens/file/pdf-viewer/pdf-viewer.component.ts b/apps/red-ui/src/app/screens/file/pdf-viewer/pdf-viewer.component.ts index 7d39b9533..ea2329877 100644 --- a/apps/red-ui/src/app/screens/file/pdf-viewer/pdf-viewer.component.ts +++ b/apps/red-ui/src/app/screens/file/pdf-viewer/pdf-viewer.component.ts @@ -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 { diff --git a/apps/red-ui/src/app/screens/file/service/annotation-processing.service.ts b/apps/red-ui/src/app/screens/file/service/annotation-processing.service.ts new file mode 100644 index 000000000..c01e6d459 --- /dev/null +++ b/apps/red-ui/src/app/screens/file/service/annotation-processing.service.ts @@ -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([a.dictionary]); + } else { + entry.add(a.dictionary); + } + } else { + availableAnnotationTypes[a.superType] = new Set(); + } + }); + + 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); + } +} diff --git a/apps/red-ui/src/app/utils/annotation-utils.ts b/apps/red-ui/src/app/utils/annotation-utils.ts deleted file mode 100644 index 49b7ea6a3..000000000 --- a/apps/red-ui/src/app/utils/annotation-utils.ts +++ /dev/null @@ -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; - } -} diff --git a/apps/red-ui/src/app/utils/types.d.ts b/apps/red-ui/src/app/utils/types.d.ts index 569d5b93d..06777b119 100644 --- a/apps/red-ui/src/app/utils/types.d.ts +++ b/apps/red-ui/src/app/utils/types.d.ts @@ -7,11 +7,3 @@ export class SortingOption { order: string; column: string; } - -export interface AnnotationFilter { - key: string; - checked?: boolean; - indeterminate?: boolean; - expanded?: boolean; - filters?: AnnotationFilter[]; -} diff --git a/apps/red-ui/src/assets/i18n/en.json b/apps/red-ui/src/assets/i18n/en.json index 4d4d40b24..5a309ac5e 100644 --- a/apps/red-ui/src/assets/i18n/en.json +++ b/apps/red-ui/src/assets/i18n/en.json @@ -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" + } + } }