use reactive annotations

This commit is contained in:
Dan Percic 2022-03-16 16:05:32 +02:00
parent ee6f8dab02
commit c8a2e0b580
12 changed files with 159 additions and 121 deletions

View File

@ -1,8 +1,9 @@
import { Injectable, Injector } from '@angular/core'; import { Injectable, Injector } from '@angular/core';
import { GenericService, RequiredParam, Toaster, Validate } from '@iqser/common-ui'; import { GenericService, RequiredParam, Toaster, Validate } from '@iqser/common-ui';
import { TextHighlightOperation, TextHighlightRequest, TextHighlightResponse } from '@red/domain'; import { ImportedRedaction, TextHighlightOperation, TextHighlightRequest, TextHighlightResponse } from '@red/domain';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { tap } from 'rxjs/operators'; import { catchError, map, tap } from 'rxjs/operators';
import { of } from 'rxjs';
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
@ -20,7 +21,10 @@ export class TextHighlightService extends GenericService<TextHighlightResponse>
operation: TextHighlightOperation.INFO, operation: TextHighlightOperation.INFO,
}; };
return this._post(request); return this._post(request).pipe(
map(response => response.redactionPerColor),
catchError(() => of({} as Record<string, ImportedRedaction[]>)),
);
} }
@Validate() @Validate()

View File

@ -9,6 +9,7 @@ import { MultiSelectService } from '../../services/multi-select.service';
import { FilePreviewStateService } from '../../services/file-preview-state.service'; import { FilePreviewStateService } from '../../services/file-preview-state.service';
import { HelpModeService, ScrollableParentView, ScrollableParentViews } from '@iqser/common-ui'; import { HelpModeService, ScrollableParentView, ScrollableParentViews } from '@iqser/common-ui';
import { PdfViewer } from '../../services/pdf-viewer.service'; import { PdfViewer } from '../../services/pdf-viewer.service';
import { FileDataService } from '../../services/file-data.service';
export const AnnotationButtonTypes = { export const AnnotationButtonTypes = {
dark: 'dark', dark: 'dark',
@ -39,6 +40,7 @@ export class AnnotationActionsComponent implements OnChanges {
private readonly _pdf: PdfViewer, private readonly _pdf: PdfViewer,
private readonly _state: FilePreviewStateService, private readonly _state: FilePreviewStateService,
private readonly _permissionsService: PermissionsService, private readonly _permissionsService: PermissionsService,
private readonly _fileDataService: FileDataService,
) {} ) {}
private _annotations: AnnotationWrapper[]; private _annotations: AnnotationWrapper[];
@ -101,14 +103,14 @@ export class AnnotationActionsComponent implements OnChanges {
$event.stopPropagation(); $event.stopPropagation();
this._pdf.annotationManager.hideAnnotations(this.viewerAnnotations); this._pdf.annotationManager.hideAnnotations(this.viewerAnnotations);
this._pdf.annotationManager.deselectAllAnnotations(); this._pdf.annotationManager.deselectAllAnnotations();
this.annotationActionsService.updateHiddenAnnotation(this.annotations, this.viewerAnnotations, true); this._fileDataService.updateHiddenAnnotations(this.viewerAnnotations, true);
} }
showAnnotation($event: MouseEvent) { showAnnotation($event: MouseEvent) {
$event.stopPropagation(); $event.stopPropagation();
this._pdf.annotationManager.showAnnotations(this.viewerAnnotations); this._pdf.annotationManager.showAnnotations(this.viewerAnnotations);
this._pdf.annotationManager.deselectAllAnnotations(); this._pdf.annotationManager.deselectAllAnnotations();
this.annotationActionsService.updateHiddenAnnotation(this.annotations, this.viewerAnnotations, false); this._fileDataService.updateHiddenAnnotations(this.viewerAnnotations, false);
} }
resize($event: MouseEvent) { resize($event: MouseEvent) {

View File

@ -1,9 +1,10 @@
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
import { AnnotationWrapper } from '@models/file/annotation.wrapper'; import { AnnotationWrapper } from '@models/file/annotation.wrapper';
import { AnnotationReferencesService } from '../../services/annotation-references.service'; import { AnnotationReferencesService } from '../../services/annotation-references.service';
import { Observable } from 'rxjs'; import { Observable, switchMap } from 'rxjs';
import { filter, map } from 'rxjs/operators'; import { filter } from 'rxjs/operators';
import { FileDataService } from '../../services/file-data.service'; import { FileDataService } from '../../services/file-data.service';
import { filterEach } from '../../../../../../../../libs/common-ui/src';
@Component({ @Component({
selector: 'redaction-annotation-references-list', selector: 'redaction-annotation-references-list',
@ -21,7 +22,7 @@ export class AnnotationReferencesListComponent {
private get _annotationReferences(): Observable<AnnotationWrapper[]> { private get _annotationReferences(): Observable<AnnotationWrapper[]> {
return this.annotationReferencesService.annotation$.pipe( return this.annotationReferencesService.annotation$.pipe(
filter(annotation => !!annotation), filter(annotation => !!annotation),
map(({ reference }) => this._fileDataService.allAnnotations.filter(a => reference.includes(a.annotationId))), switchMap(({ reference }) => this._fileDataService.annotations$.pipe(filterEach(a => reference.includes(a.annotationId)))),
); );
} }

View File

@ -24,7 +24,7 @@ import {
shareDistinctLast, shareDistinctLast,
shareLast, shareLast,
} from '@iqser/common-ui'; } from '@iqser/common-ui';
import { BehaviorSubject, combineLatest, Observable } from 'rxjs'; import { combineLatest, Observable } from 'rxjs';
import { map, tap } from 'rxjs/operators'; import { map, tap } from 'rxjs/operators';
import { File } from '@red/domain'; import { File } from '@red/domain';
import { ExcludedPagesService } from '../../services/excluded-pages.service'; import { ExcludedPagesService } from '../../services/excluded-pages.service';
@ -66,7 +66,6 @@ export class FileWorkloadComponent {
readonly showExcludedPages$: Observable<boolean>; readonly showExcludedPages$: Observable<boolean>;
readonly title$: Observable<string>; readonly title$: Observable<string>;
readonly isHighlights$: Observable<boolean>; readonly isHighlights$: Observable<boolean>;
private _annotations$ = new BehaviorSubject<AnnotationWrapper[]>([]);
@ViewChild('annotationsElement') private readonly _annotationsElement: ElementRef; @ViewChild('annotationsElement') private readonly _annotationsElement: ElementRef;
@ViewChild('quickNavigation') private readonly _quickNavigationElement: ElementRef; @ViewChild('quickNavigation') private readonly _quickNavigationElement: ElementRef;
@ -89,11 +88,6 @@ export class FileWorkloadComponent {
this.title$ = this._title$; this.title$ = this._title$;
} }
@Input()
set annotations(value: AnnotationWrapper[]) {
this._annotations$.next(value);
}
get activeAnnotations(): AnnotationWrapper[] | undefined { get activeAnnotations(): AnnotationWrapper[] | undefined {
return this.displayedAnnotations.get(this.activeViewerPage); return this.displayedAnnotations.get(this.activeViewerPage);
} }
@ -142,7 +136,7 @@ export class FileWorkloadComponent {
const primary$ = this.filterService.getFilterModels$('primaryFilters'); const primary$ = this.filterService.getFilterModels$('primaryFilters');
const secondary$ = this.filterService.getFilterModels$('secondaryFilters'); const secondary$ = this.filterService.getFilterModels$('secondaryFilters');
return combineLatest([this._annotations$.asObservable(), primary$, secondary$]).pipe( return combineLatest([this.fileDataService.visibleAnnotations$, primary$, secondary$]).pipe(
map(([annotations, primary, secondary]) => this._filterAnnotations(annotations, primary, secondary)), map(([annotations, primary, secondary]) => this._filterAnnotations(annotations, primary, secondary)),
); );
} }

View File

@ -41,6 +41,7 @@ import { PageRotationService } from '../../services/page-rotation.service';
import { ALLOWED_KEYBOARD_SHORTCUTS, HeaderElements, TextPopups } from '../../shared/constants'; import { ALLOWED_KEYBOARD_SHORTCUTS, HeaderElements, TextPopups } from '../../shared/constants';
import { FilePreviewDialogService } from '../../services/file-preview-dialog.service'; import { FilePreviewDialogService } from '../../services/file-preview-dialog.service';
import { loadCompareDocumentWrapper } from '../../../dossier/utils/compare-mode.utils'; import { loadCompareDocumentWrapper } from '../../../dossier/utils/compare-mode.utils';
import { FileDataService } from '../../services/file-data.service';
import Tools = Core.Tools; import Tools = Core.Tools;
import TextTool = Tools.TextTool; import TextTool = Tools.TextTool;
import Annotation = Core.Annotations.Annotation; import Annotation = Core.Annotations.Annotation;
@ -60,7 +61,6 @@ function getDivider(hiddenOn?: readonly ('desktop' | 'mobile' | 'tablet')[]) {
export class PdfViewerComponent extends AutoUnsubscribe implements OnInit, OnChanges { export class PdfViewerComponent extends AutoUnsubscribe implements OnInit, OnChanges {
@Input() dossier: Dossier; @Input() dossier: Dossier;
@Input() canPerformActions = false; @Input() canPerformActions = false;
@Input() annotations: AnnotationWrapper[];
@Output() readonly fileReady = new EventEmitter(); @Output() readonly fileReady = new EventEmitter();
@Output() readonly annotationSelected = new EventEmitter<string[]>(); @Output() readonly annotationSelected = new EventEmitter<string[]>();
@Output() readonly manualAnnotationRequested = new EventEmitter<ManualRedactionEntryWrapper>(); @Output() readonly manualAnnotationRequested = new EventEmitter<ManualRedactionEntryWrapper>();
@ -89,6 +89,7 @@ export class PdfViewerComponent extends AutoUnsubscribe implements OnInit, OnCha
private readonly _loadingService: LoadingService, private readonly _loadingService: LoadingService,
private readonly _fileManagementService: FileManagementService, private readonly _fileManagementService: FileManagementService,
private readonly _pageRotationService: PageRotationService, private readonly _pageRotationService: PageRotationService,
private readonly _fileDataService: FileDataService,
readonly stateService: FilePreviewStateService, readonly stateService: FilePreviewStateService,
readonly viewModeService: ViewModeService, readonly viewModeService: ViewModeService,
readonly multiSelectService: MultiSelectService, readonly multiSelectService: MultiSelectService,
@ -226,18 +227,19 @@ export class PdfViewerComponent extends AutoUnsubscribe implements OnInit, OnCha
this.pdf.disableHotkeys(); this.pdf.disableHotkeys();
await this._configureTextPopup(); await this._configureTextPopup();
this.annotationManager.addEventListener('annotationSelected', (annotations: Annotation[], action) => { this.annotationManager.addEventListener('annotationSelected', async (annotations: Annotation[], action) => {
const nextAnnotations = this.multiSelectService.isEnabled ? this.annotationManager.getSelectedAnnotations() : annotations; const nextAnnotations = this.multiSelectService.isEnabled ? this.annotationManager.getSelectedAnnotations() : annotations;
this.annotationSelected.emit(nextAnnotations.map(ann => ann.Id)); this.annotationSelected.emit(nextAnnotations.map(ann => ann.Id));
if (action === 'deselected') { if (action === 'deselected') {
this._toggleRectangleAnnotationAction(true); return this._toggleRectangleAnnotationAction(true);
} else { }
if (!this.multiSelectService.isEnabled) { if (!this.multiSelectService.isEnabled) {
this.pdf.deselectAnnotations(this.annotations.filter(wrapper => !nextAnnotations.find(ann => ann.Id === wrapper.id))); const visibleAnnotations = await this._fileDataService.visibleAnnotations;
this.pdf.deselectAnnotations(visibleAnnotations.filter(wrapper => !nextAnnotations.find(ann => ann.Id === wrapper.id)));
} }
this._configureAnnotationSpecificActions(annotations); this.#configureAnnotationSpecificActions(annotations);
this._toggleRectangleAnnotationAction(annotations.length === 1 && annotations[0].ReadOnly); this._toggleRectangleAnnotationAction(annotations.length === 1 && annotations[0].ReadOnly);
}
}); });
this.annotationManager.addEventListener('annotationChanged', (annotations: Annotation[]) => { this.annotationManager.addEventListener('annotationChanged', (annotations: Annotation[]) => {
@ -480,12 +482,13 @@ export class PdfViewerComponent extends AutoUnsubscribe implements OnInit, OnCha
}); });
} }
private _configureAnnotationSpecificActions(viewerAnnotations: Annotation[]) { async #configureAnnotationSpecificActions(viewerAnnotations: Annotation[]) {
if (!this.canPerformActions) { if (!this.canPerformActions) {
return; return;
} }
const annotationWrappers = viewerAnnotations.map(va => this.annotations.find(a => a.id === va.Id)).filter(va => !!va); const visibleAnnotations = await this._fileDataService.visibleAnnotations;
const annotationWrappers = viewerAnnotations.map(va => visibleAnnotations.find(a => a.id === va.Id)).filter(va => !!va);
this.instance.UI.annotationPopup.update([]); this.instance.UI.annotationPopup.update([]);
if (annotationWrappers.length === 0) { if (annotationWrappers.length === 0) {
@ -513,7 +516,7 @@ export class PdfViewerComponent extends AutoUnsubscribe implements OnInit, OnCha
this.annotationManager.showAnnotations(viewerAnnotations); this.annotationManager.showAnnotations(viewerAnnotations);
} }
this.annotationManager.deselectAllAnnotations(); this.annotationManager.deselectAllAnnotations();
this._annotationActionsService.updateHiddenAnnotation(this.annotations, viewerAnnotations, allAreVisible); this._fileDataService.updateHiddenAnnotations(viewerAnnotations, allAreVisible);
}); });
}, },
}, },

View File

@ -69,7 +69,6 @@
(pageChanged)="viewerPageChanged($event)" (pageChanged)="viewerPageChanged($event)"
(viewerReady)="viewerReady()" (viewerReady)="viewerReady()"
*ngIf="displayPdfViewer" *ngIf="displayPdfViewer"
[annotations]="visibleAnnotations"
[canPerformActions]="canPerformAnnotationActions$ | async" [canPerformActions]="canPerformAnnotationActions$ | async"
[class.hidden]="!ready" [class.hidden]="!ready"
[dossier]="dossier" [dossier]="dossier"
@ -95,7 +94,6 @@
*ngIf="!file.excluded" *ngIf="!file.excluded"
[activeViewerPage]="activeViewerPage" [activeViewerPage]="activeViewerPage"
[annotationActionsTemplate]="annotationActionsTemplate" [annotationActionsTemplate]="annotationActionsTemplate"
[annotations]="visibleAnnotations"
[dialogRef]="dialogRef" [dialogRef]="dialogRef"
[file]="file" [file]="file"
[selectedAnnotations]="selectedAnnotations" [selectedAnnotations]="selectedAnnotations"

View File

@ -125,14 +125,6 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
return this._pageRotationService.hasRotations(); return this._pageRotationService.hasRotations();
} }
get visibleAnnotations(): AnnotationWrapper[] {
return this._fileDataService.getVisibleAnnotations(this._viewModeService.viewMode);
}
get allAnnotations(): AnnotationWrapper[] {
return this._fileDataService.allAnnotations;
}
private get _canPerformAnnotationActions$() { private get _canPerformAnnotationActions$() {
const viewMode$ = this._viewModeService.viewMode$.pipe(tap(() => this.#deactivateMultiSelect())); const viewMode$ = this._viewModeService.viewMode$.pipe(tap(() => this.#deactivateMultiSelect()));
@ -154,15 +146,17 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
return; return;
} }
this._pdf.deleteAnnotations(this._fileDataService.textHighlightAnnotations.map(a => a.id)); this._pdf.deleteAnnotations(this._fileDataService.textHighlights.map(a => a.id));
const ocrAnnotationIds = this.allAnnotations.filter(a => a.isOCR).map(a => a.id);
const annotations = this._pdf.getAnnotations(a => a.getCustomData('redact-manager')); const annotations = this._pdf.getAnnotations(a => a.getCustomData('redact-manager'));
const redactions = annotations.filter(a => a.getCustomData('redaction')); const redactions = annotations.filter(a => a.getCustomData('redaction'));
switch (this._viewModeService.viewMode) { switch (this._viewModeService.viewMode) {
case 'STANDARD': { case 'STANDARD': {
this._setAnnotationsColor(redactions, 'annotationColor'); this._setAnnotationsColor(redactions, 'annotationColor');
const wrappers = await this._fileDataService.annotations;
const ocrAnnotationIds = wrappers.filter(a => a.isOCR).map(a => a.id);
const standardEntries = annotations const standardEntries = annotations
.filter(a => a.getCustomData('changeLogRemoved') === 'false') .filter(a => a.getCustomData('changeLogRemoved') === 'false')
.filter(a => !ocrAnnotationIds.includes(a.Id)); .filter(a => !ocrAnnotationIds.includes(a.Id));
@ -192,14 +186,14 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
case 'TEXT_HIGHLIGHTS': { case 'TEXT_HIGHLIGHTS': {
this._loadingService.start(); this._loadingService.start();
this._pdf.hideAnnotations(annotations); this._pdf.hideAnnotations(annotations);
await this._fileDataService.loadTextHighlights(); const highlights = await this._fileDataService.loadTextHighlights();
await this._annotationDrawService.drawAnnotations(this._fileDataService.textHighlightAnnotations); await this._annotationDrawService.drawAnnotations(highlights);
this._loadingService.stop(); this._loadingService.stop();
} }
} }
await this._stampPDF(); await this._stampPDF();
this.rebuildFilters(); await this.rebuildFilters();
} }
ngOnDetach(): void { ngOnDetach(): void {
@ -237,7 +231,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
this.displayPdfViewer = true; this.displayPdfViewer = true;
} }
rebuildFilters(deletePreviousAnnotations = false): void { async rebuildFilters(deletePreviousAnnotations = false) {
const startTime = new Date().getTime(); const startTime = new Date().getTime();
if (deletePreviousAnnotations) { if (deletePreviousAnnotations) {
this._pdf.deleteAnnotations(); this._pdf.deleteAnnotations();
@ -246,7 +240,8 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
} }
const processStartTime = new Date().getTime(); const processStartTime = new Date().getTime();
const annotationFilters = this._annotationProcessingService.getAnnotationFilter(this.visibleAnnotations); const visibleAnnotations = await this._fileDataService.visibleAnnotations;
const annotationFilters = this._annotationProcessingService.getAnnotationFilter(visibleAnnotations);
const primaryFilters = this._filterService.getGroup('primaryFilters')?.filters; const primaryFilters = this._filterService.getGroup('primaryFilters')?.filters;
this._filterService.addFilterGroup({ this._filterService.addFilterGroup({
slug: 'primaryFilters', slug: 'primaryFilters',
@ -266,9 +261,10 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
console.log(`[REDACTION] Filter rebuild time: ${new Date().getTime() - startTime}`); console.log(`[REDACTION] Filter rebuild time: ${new Date().getTime() - startTime}`);
} }
handleAnnotationSelected(annotationIds: string[]) { async handleAnnotationSelected(annotationIds: string[]) {
const visibleAnnotations = await this._fileDataService.visibleAnnotations;
this.selectedAnnotations = annotationIds this.selectedAnnotations = annotationIds
.map(id => this.visibleAnnotations.find(annotationWrapper => annotationWrapper.id === id)) .map(id => visibleAnnotations.find(annotation => annotation.id === id))
.filter(ann => ann !== undefined); .filter(ann => ann !== undefined);
if (this.selectedAnnotations.length > 1) { if (this.selectedAnnotations.length > 1) {
this.multiSelectService.activate(); this.multiSelectService.activate();
@ -447,10 +443,10 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
download(await firstValueFrom(originalFile), file.filename); download(await firstValueFrom(originalFile), file.filename);
} }
#deactivateMultiSelect(): void { async #deactivateMultiSelect() {
this.multiSelectService.deactivate(); this.multiSelectService.deactivate();
this._pdf.deselectAllAnnotations(); this._pdf.deselectAllAnnotations();
this.handleAnnotationSelected([]); await this.handleAnnotationSelected([]);
} }
private _setExcludedPageStyles() { private _setExcludedPageStyles() {
@ -498,7 +494,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
} }
private async _stampPreview(document: PDFNet.PDFDoc, dossierTemplateId: string) { private async _stampPreview(document: PDFNet.PDFDoc, dossierTemplateId: string) {
const watermark = await this._watermarkService.getWatermark(dossierTemplateId).toPromise(); const watermark = await firstValueFrom(this._watermarkService.getWatermark(dossierTemplateId));
await stampPDFPage( await stampPDFPage(
document, document,
this._pdf.PDFNet, this._pdf.PDFNet,
@ -605,7 +601,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
return; return;
} }
const currentPageAnnotations = this.visibleAnnotations.filter(a => a.pageNumber === page); const currentPageAnnotations = (await this._fileDataService.visibleAnnotations).filter(a => a.pageNumber === page);
await this._fileDataService.loadRedactionLog(); await this._fileDataService.loadRedactionLog();
this._pdf.deleteAnnotations(currentPageAnnotations.map(a => a.id)); this._pdf.deleteAnnotations(currentPageAnnotations.map(a => a.id));
@ -618,14 +614,15 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
} }
const currentFilters = this._filterService.getGroup('primaryFilters')?.filters || []; const currentFilters = this._filterService.getGroup('primaryFilters')?.filters || [];
this.rebuildFilters(); await this.rebuildFilters();
const startTime = new Date().getTime(); const startTime = new Date().getTime();
const annotations = this.allAnnotations; const annotations = await this._fileDataService.annotations;
const newAnnotations = newAnnotationsFilter ? annotations.filter(newAnnotationsFilter) : annotations; const newAnnotations = newAnnotationsFilter ? annotations.filter(newAnnotationsFilter) : annotations;
if (currentFilters) { if (currentFilters) {
this._handleDeltaAnnotationFilters(currentFilters, this.visibleAnnotations); const visibleAnnotations = await this._fileDataService.visibleAnnotations;
this._handleDeltaAnnotationFilters(currentFilters, visibleAnnotations);
} }
await this._annotationDrawService.drawAnnotations(newAnnotations); await this._annotationDrawService.drawAnnotations(newAnnotations);

View File

@ -24,7 +24,6 @@ import { MatDialog } from '@angular/material/dialog';
import { FilePreviewStateService } from './file-preview-state.service'; import { FilePreviewStateService } from './file-preview-state.service';
import { PdfViewer } from './pdf-viewer.service'; import { PdfViewer } from './pdf-viewer.service';
import { FilePreviewDialogService } from './file-preview-dialog.service'; import { FilePreviewDialogService } from './file-preview-dialog.service';
import Annotation = Core.Annotations.Annotation;
import Quad = Core.Math.Quad; import Quad = Core.Math.Quad;
@Injectable() @Injectable()
@ -414,12 +413,6 @@ export class AnnotationActionsService {
return availableActions; return availableActions;
} }
updateHiddenAnnotation(annotations: AnnotationWrapper[], viewerAnnotations: Annotation[], hidden: boolean) {
const annotationId = viewerAnnotations[0].Id;
const annotationToBeUpdated = annotations.find((a: AnnotationWrapper) => a.annotationId === annotationId);
annotationToBeUpdated.hidden = hidden;
}
resize($event: MouseEvent, annotationWrapper: AnnotationWrapper) { resize($event: MouseEvent, annotationWrapper: AnnotationWrapper) {
$event?.stopPropagation(); $event?.stopPropagation();

View File

@ -14,6 +14,7 @@ import { DossiersService } from '@services/dossiers/dossiers.service';
import { PdfViewer } from './pdf-viewer.service'; import { PdfViewer } from './pdf-viewer.service';
import { FilePreviewStateService } from './file-preview-state.service'; import { FilePreviewStateService } from './file-preview-state.service';
import { ViewModeService } from './view-mode.service'; import { ViewModeService } from './view-mode.service';
import { FileDataService } from './file-data.service';
import Annotation = Core.Annotations.Annotation; import Annotation = Core.Annotations.Annotation;
@Injectable() @Injectable()
@ -31,6 +32,7 @@ export class AnnotationDrawService {
private readonly _pdf: PdfViewer, private readonly _pdf: PdfViewer,
private readonly _state: FilePreviewStateService, private readonly _state: FilePreviewStateService,
private readonly _viewModeService: ViewModeService, private readonly _viewModeService: ViewModeService,
private readonly _fileDataService: FileDataService,
) {} ) {}
drawAnnotations(annotationWrappers: AnnotationWrapper[]) { drawAnnotations(annotationWrappers: AnnotationWrapper[]) {
@ -200,7 +202,7 @@ export class AnnotationDrawService {
annotationWrapper.isChangeLogRemoved || annotationWrapper.isChangeLogRemoved ||
(this._skippedService.hideSkipped && annotationWrapper.isSkipped) || (this._skippedService.hideSkipped && annotationWrapper.isSkipped) ||
annotationWrapper.isOCR || annotationWrapper.isOCR ||
annotationWrapper.hidden; this._fileDataService.isHidden(annotationWrapper.annotationId);
annotation.setCustomData('redact-manager', 'true'); annotation.setCustomData('redact-manager', 'true');
annotation.setCustomData('redaction', String(annotationWrapper.previewAnnotation)); annotation.setCustomData('redaction', String(annotationWrapper.previewAnnotation));
annotation.setCustomData('skipped', String(annotationWrapper.isSkipped)); annotation.setCustomData('skipped', String(annotationWrapper.isSkipped));

View File

@ -1,66 +1,100 @@
import { import {
ChangeType, ChangeType,
File, File,
ImportedRedaction,
IRedactionLog, IRedactionLog,
IRedactionLogEntry, IRedactionLogEntry,
IViewedPage, IViewedPage,
LogEntryStatus, LogEntryStatus,
ManualRedactionType, ManualRedactionType,
TextHighlightResponse,
ViewMode, ViewMode,
ViewModes,
} from '@red/domain'; } from '@red/domain';
import { AnnotationWrapper } from '../../../models/file/annotation.wrapper'; import { AnnotationWrapper } from '../../../models/file/annotation.wrapper';
import * as moment from 'moment'; import * as moment from 'moment';
import { BehaviorSubject, firstValueFrom, of } from 'rxjs'; import { BehaviorSubject, firstValueFrom, iif, Observable, of } from 'rxjs';
import { RedactionLogEntry } from '../../../models/file/redaction-log.entry'; import { RedactionLogEntry } from '../../../models/file/redaction-log.entry';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { FilePreviewStateService } from './file-preview-state.service'; import { FilePreviewStateService } from './file-preview-state.service';
import { ViewedPagesService } from '../../../services/entity-services/viewed-pages.service'; import { ViewedPagesService } from '../../../services/entity-services/viewed-pages.service';
import { UserPreferenceService } from '../../../services/user-preference.service'; import { UserPreferenceService } from '../../../services/user-preference.service';
import { DictionariesMapService } from '../../../services/entity-services/dictionaries-map.service'; import { DictionariesMapService } from '../../../services/entity-services/dictionaries-map.service';
import { catchError, tap } from 'rxjs/operators'; import { catchError, map, switchMap, tap, withLatestFrom } from 'rxjs/operators';
import { PermissionsService } from '../../../services/permissions.service'; import { PermissionsService } from '../../../services/permissions.service';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { Toaster } from '../../../../../../../libs/common-ui/src'; import { log, shareLast, Toaster } from '../../../../../../../libs/common-ui/src';
import { RedactionLogService } from '../../dossier/services/redaction-log.service'; import { RedactionLogService } from '../../dossier/services/redaction-log.service';
import { TextHighlightService } from '../../dossier/services/text-highlight.service'; import { TextHighlightService } from '../../dossier/services/text-highlight.service';
import { ViewModeService } from './view-mode.service';
import { Core } from '@pdftron/webviewer';
import Annotation = Core.Annotations.Annotation;
const DELTA_VIEW_TIME = 10 * 60 * 1000; // 10 minutes;
@Injectable() @Injectable()
export class FileDataService { export class FileDataService {
static readonly DELTA_VIEW_TIME = 10 * 60 * 1000; // 10 minutes;
viewedPages: IViewedPage[] = []; viewedPages: IViewedPage[] = [];
allAnnotations: AnnotationWrapper[] = [];
readonly hasChangeLog$ = new BehaviorSubject<boolean>(false); readonly hasChangeLog$ = new BehaviorSubject<boolean>(false);
missingTypes = new Set<string>(); missingTypes = new Set<string>();
textHighlightAnnotations: AnnotationWrapper[] = [];
shouldUpdateAnnotations = false; shouldUpdateAnnotations = false;
readonly annotations$: Observable<AnnotationWrapper[]>;
readonly visibleAnnotations$: Observable<AnnotationWrapper[]>;
readonly hiddenAnnotations = new Set<string>();
#redactionLog: IRedactionLog; #redactionLog: IRedactionLog;
#redactionLogHash = ''; #redactionLogHash = '';
readonly #redactionLog$ = new BehaviorSubject<IRedactionLog>({});
readonly #textHighlights$ = new BehaviorSubject<AnnotationWrapper[]>([]);
constructor( constructor(
private readonly _state: FilePreviewStateService, private readonly _state: FilePreviewStateService,
private readonly _viewedPagesService: ViewedPagesService, private readonly _viewedPagesService: ViewedPagesService,
private readonly _viewModeService: ViewModeService,
private readonly _userPreferenceService: UserPreferenceService, private readonly _userPreferenceService: UserPreferenceService,
private readonly _dictionariesMapService: DictionariesMapService, private readonly _dictionariesMapService: DictionariesMapService,
private readonly _permissionsService: PermissionsService, private readonly _permissionsService: PermissionsService,
private readonly _redactionLogService: RedactionLogService, private readonly _redactionLogService: RedactionLogService,
private readonly _textHighlightsService: TextHighlightService, private readonly _textHighlightsService: TextHighlightService,
private readonly _toaster: Toaster, private readonly _toaster: Toaster,
) {} ) {
this.annotations$ = this.#annotations$;
set textHighlights(textHighlightResponse: TextHighlightResponse) { this.visibleAnnotations$ = this._viewModeService.viewMode$.pipe(
const highlights = []; switchMap(viewMode =>
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument iif(
for (const color of Object.keys(textHighlightResponse.redactionPerColor)) { () => viewMode === ViewModes.TEXT_HIGHLIGHTS,
for (const entry of textHighlightResponse.redactionPerColor[color]) { this.#textHighlights$,
const annotation = AnnotationWrapper.fromHighlight(color, entry); this.annotations$.pipe(map(annotations => this.getVisibleAnnotations(annotations, viewMode))),
highlights.push(annotation); ),
} ),
} log('visible annotations: '),
this.textHighlightAnnotations = highlights; shareLast(),
);
} }
async setRedactionLog(redactionLog: IRedactionLog) { get visibleAnnotations() {
return firstValueFrom(this.visibleAnnotations$);
}
get annotations() {
return firstValueFrom(this.annotations$);
}
get textHighlights() {
return this.#textHighlights$.value;
}
get #annotations$() {
return this.#redactionLog$.pipe(
withLatestFrom(this._state.file$),
map(([redactionLog, file]) => this.#buildAnnotations(redactionLog, file)),
map(annotations =>
this._userPreferenceService.areDevFeaturesEnabled ? annotations : annotations.filter(a => !a.isFalsePositive),
),
shareLast(),
);
}
setRedactionLog(redactionLog: IRedactionLog) {
this.#redactionLog = redactionLog; this.#redactionLog = redactionLog;
const hash = require('object-hash'); const hash = require('object-hash');
const newRedactionLogHash = hash(redactionLog.redactionLogEntry ?? []); const newRedactionLogHash = hash(redactionLog.redactionLogEntry ?? []);
@ -73,7 +107,6 @@ export class FileDataService {
this.shouldUpdateAnnotations = true; this.shouldUpdateAnnotations = true;
this.#redactionLogHash = newRedactionLogHash; this.#redactionLogHash = newRedactionLogHash;
this.missingTypes.clear(); this.missingTypes.clear();
await this.#buildAllAnnotations();
} }
async load(file: File) { async load(file: File) {
@ -90,8 +123,11 @@ export class FileDataService {
async loadTextHighlights() { async loadTextHighlights() {
const { dossierId, fileId } = this._state; const { dossierId, fileId } = this._state;
const highlights = this._textHighlightsService.getTextHighlights(dossierId, fileId).pipe(catchError(() => of({}))); const redactionPerColor = await firstValueFrom(this._textHighlightsService.getTextHighlights(dossierId, fileId));
this.textHighlights = await firstValueFrom(highlights); const textHighlights = this.#buildTextHighlights(redactionPerColor);
this.#textHighlights$.next(textHighlights);
return textHighlights;
} }
getViewedPagesFor(file: File) { getViewedPagesFor(file: File) {
@ -105,60 +141,61 @@ export class FileDataService {
const redactionLog$ = this._redactionLogService.getRedactionLog(this._state.dossierId, this._state.fileId).pipe( const redactionLog$ = this._redactionLogService.getRedactionLog(this._state.dossierId, this._state.fileId).pipe(
tap(redactionLog => redactionLog.redactionLogEntry.sort((a, b) => a.positions[0].page - b.positions[0].page)), tap(redactionLog => redactionLog.redactionLogEntry.sort((a, b) => a.positions[0].page - b.positions[0].page)),
catchError(() => of({})), catchError(() => of({})),
tap(redactionLog => this.#redactionLog$.next(redactionLog)),
); );
return this.setRedactionLog(await firstValueFrom(redactionLog$)); this.setRedactionLog(await firstValueFrom(redactionLog$));
} }
getVisibleAnnotations(viewMode: ViewMode) { getVisibleAnnotations(annotations: AnnotationWrapper[], viewMode: ViewMode) {
if (viewMode === 'TEXT_HIGHLIGHTS') { return annotations.filter(annotation => {
return this.textHighlightAnnotations;
}
return this.allAnnotations.filter(annotation => {
if (viewMode === 'STANDARD') { if (viewMode === 'STANDARD') {
return !annotation.isChangeLogRemoved; return !annotation.isChangeLogRemoved;
} else if (viewMode === 'DELTA') { }
if (viewMode === 'DELTA') {
return annotation.isChangeLogEntry; return annotation.isChangeLogEntry;
} else { }
return annotation.previewAnnotation; return annotation.previewAnnotation;
}
}); });
} }
async #buildAllAnnotations() { updateHiddenAnnotations(viewerAnnotations: Annotation[], hidden: boolean) {
const file = await this._state.file; const annotationId = viewerAnnotations[0].Id;
const entries: RedactionLogEntry[] = this.#convertData(file); if (hidden) {
this.hiddenAnnotations.add(annotationId);
const previousAnnotations = [...this.allAnnotations]; } else {
this.allAnnotations = entries this.hiddenAnnotations.delete(annotationId);
.map(entry => AnnotationWrapper.fromData(entry)) }
.filter(ann => ann.manual || !file.excludedPages.includes(ann.pageNumber));
if (!this._userPreferenceService.areDevFeaturesEnabled) {
this.allAnnotations = this.allAnnotations.filter(annotation => !annotation.isFalsePositive);
} }
this._setHiddenPropertyToNewAnnotations(this.allAnnotations, previousAnnotations); isHidden(annotationId: string) {
return this.hiddenAnnotations.has(annotationId);
} }
private _setHiddenPropertyToNewAnnotations(newAnnotations: AnnotationWrapper[], oldAnnotations: AnnotationWrapper[]) { #buildTextHighlights(redactionPerColor: Record<string, ImportedRedaction[]>): AnnotationWrapper[] {
newAnnotations.forEach(newAnnotation => { if (!redactionPerColor) {
const oldAnnotation = oldAnnotations.find(a => a.annotationId === newAnnotation.annotationId); return [];
if (oldAnnotation) {
newAnnotation.hidden = oldAnnotation.hidden;
}
});
} }
#convertData(file: File): RedactionLogEntry[] { const highlights = Object.entries(redactionPerColor);
return highlights.flatMap(([color, entries]) => entries.map(entry => AnnotationWrapper.fromHighlight(color, entry)));
}
#buildAnnotations(redactionLog: IRedactionLog, file: File) {
const entries: RedactionLogEntry[] = this.#convertData(redactionLog, file);
const annotations = entries.map(entry => AnnotationWrapper.fromData(entry));
return annotations.filter(ann => ann.manual || !file.excludedPages.includes(ann.pageNumber));
}
#convertData(redactionLog: IRedactionLog, file: File): RedactionLogEntry[] {
let result: RedactionLogEntry[] = []; let result: RedactionLogEntry[] = [];
const reasonAnnotationIds: { [key: string]: RedactionLogEntry[] } = {}; const reasonAnnotationIds: { [key: string]: RedactionLogEntry[] } = {};
const _dictionaryData = this._dictionariesMapService.get(this._state.dossierTemplateId); const _dictionaryData = this._dictionariesMapService.get(this._state.dossierTemplateId);
this.#redactionLog.redactionLogEntry?.forEach(redactionLogEntry => {
// copy the redactionLog Entry
redactionLog.redactionLogEntry?.forEach(redactionLogEntry => {
const changeLogValues = this.#getChangeLogValues(redactionLogEntry, file); const changeLogValues = this.#getChangeLogValues(redactionLogEntry, file);
const dictionaryData = _dictionaryData.find(dict => dict.type === redactionLogEntry.type); const dictionaryData = _dictionaryData.find(dict => dict.type === redactionLogEntry.type);
if (!dictionaryData) { if (!dictionaryData) {
@ -171,7 +208,7 @@ export class FileDataService {
changeLogValues.changeLogType, changeLogValues.changeLogType,
changeLogValues.isChangeLogEntry, changeLogValues.isChangeLogEntry,
changeLogValues.hidden, changeLogValues.hidden,
this.#redactionLog.legalBasis, redactionLog.legalBasis,
!!dictionaryData?.hint, !!dictionaryData?.hint,
); );
@ -229,7 +266,7 @@ export class FileDataService {
// page has been seen -> let's see if it's a change // page has been seen -> let's see if it's a change
if (viewedPage) { if (viewedPage) {
const viewTime = moment(viewedPage.viewedTime).valueOf() - FileDataService.DELTA_VIEW_TIME; const viewTime = moment(viewedPage.viewedTime).valueOf() - DELTA_VIEW_TIME;
// these are all unseen changes // these are all unseen changes
const relevantChanges = viableChanges.filter(change => moment(change.dateTime).valueOf() > viewTime); const relevantChanges = viableChanges.filter(change => moment(change.dateTime).valueOf() > viewTime);
// at least one unseen change // at least one unseen change

View File

@ -1 +1,8 @@
export type ViewMode = 'STANDARD' | 'DELTA' | 'REDACTED' | 'TEXT_HIGHLIGHTS'; export const ViewModes = {
STANDARD: 'STANDARD',
DELTA: 'DELTA',
REDACTED: 'REDACTED',
TEXT_HIGHLIGHTS: 'TEXT_HIGHLIGHTS',
} as const;
export type ViewMode = keyof typeof ViewModes;

View File

@ -5,5 +5,5 @@ export interface TextHighlightResponse {
dossierId?: string; dossierId?: string;
fileId?: string; fileId?: string;
operation?: TextHighlightOperation; operation?: TextHighlightOperation;
redactionPerColor?: { [key: string]: ImportedRedaction[] }; redactionPerColor?: Record<string, ImportedRedaction[]>;
} }