From c8a2e0b580e127faca4e5bd17dcb8a658a9e7612 Mon Sep 17 00:00:00 2001 From: Dan Percic Date: Wed, 16 Mar 2022 16:05:32 +0200 Subject: [PATCH] use reactive annotations --- .../services/text-highlight.service.ts | 10 +- .../annotation-actions.component.ts | 6 +- .../annotation-references-list.component.ts | 7 +- .../file-workload/file-workload.component.ts | 10 +- .../pdf-viewer/pdf-viewer.component.ts | 27 ++-- .../file-preview-screen.component.html | 2 - .../file-preview-screen.component.ts | 45 +++--- .../services/annotation-actions.service.ts | 7 - .../services/annotation-draw.service.ts | 4 +- .../services/file-data.service.ts | 151 +++++++++++------- libs/red-domain/src/lib/shared/view-mode.ts | 9 +- .../text-highlight/text-highlight.response.ts | 2 +- 12 files changed, 159 insertions(+), 121 deletions(-) diff --git a/apps/red-ui/src/app/modules/dossier/services/text-highlight.service.ts b/apps/red-ui/src/app/modules/dossier/services/text-highlight.service.ts index a42a5fd38..af3b0c4a9 100644 --- a/apps/red-ui/src/app/modules/dossier/services/text-highlight.service.ts +++ b/apps/red-ui/src/app/modules/dossier/services/text-highlight.service.ts @@ -1,8 +1,9 @@ import { Injectable, Injector } from '@angular/core'; 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 { tap } from 'rxjs/operators'; +import { catchError, map, tap } from 'rxjs/operators'; +import { of } from 'rxjs'; @Injectable({ providedIn: 'root', @@ -20,7 +21,10 @@ export class TextHighlightService extends GenericService operation: TextHighlightOperation.INFO, }; - return this._post(request); + return this._post(request).pipe( + map(response => response.redactionPerColor), + catchError(() => of({} as Record)), + ); } @Validate() diff --git a/apps/red-ui/src/app/modules/file-preview/components/annotation-actions/annotation-actions.component.ts b/apps/red-ui/src/app/modules/file-preview/components/annotation-actions/annotation-actions.component.ts index d16df6947..f6359d91f 100644 --- a/apps/red-ui/src/app/modules/file-preview/components/annotation-actions/annotation-actions.component.ts +++ b/apps/red-ui/src/app/modules/file-preview/components/annotation-actions/annotation-actions.component.ts @@ -9,6 +9,7 @@ import { MultiSelectService } from '../../services/multi-select.service'; import { FilePreviewStateService } from '../../services/file-preview-state.service'; import { HelpModeService, ScrollableParentView, ScrollableParentViews } from '@iqser/common-ui'; import { PdfViewer } from '../../services/pdf-viewer.service'; +import { FileDataService } from '../../services/file-data.service'; export const AnnotationButtonTypes = { dark: 'dark', @@ -39,6 +40,7 @@ export class AnnotationActionsComponent implements OnChanges { private readonly _pdf: PdfViewer, private readonly _state: FilePreviewStateService, private readonly _permissionsService: PermissionsService, + private readonly _fileDataService: FileDataService, ) {} private _annotations: AnnotationWrapper[]; @@ -101,14 +103,14 @@ export class AnnotationActionsComponent implements OnChanges { $event.stopPropagation(); this._pdf.annotationManager.hideAnnotations(this.viewerAnnotations); this._pdf.annotationManager.deselectAllAnnotations(); - this.annotationActionsService.updateHiddenAnnotation(this.annotations, this.viewerAnnotations, true); + this._fileDataService.updateHiddenAnnotations(this.viewerAnnotations, true); } showAnnotation($event: MouseEvent) { $event.stopPropagation(); this._pdf.annotationManager.showAnnotations(this.viewerAnnotations); this._pdf.annotationManager.deselectAllAnnotations(); - this.annotationActionsService.updateHiddenAnnotation(this.annotations, this.viewerAnnotations, false); + this._fileDataService.updateHiddenAnnotations(this.viewerAnnotations, false); } resize($event: MouseEvent) { diff --git a/apps/red-ui/src/app/modules/file-preview/components/annotation-references-list/annotation-references-list.component.ts b/apps/red-ui/src/app/modules/file-preview/components/annotation-references-list/annotation-references-list.component.ts index 5c12ce784..08786b685 100644 --- a/apps/red-ui/src/app/modules/file-preview/components/annotation-references-list/annotation-references-list.component.ts +++ b/apps/red-ui/src/app/modules/file-preview/components/annotation-references-list/annotation-references-list.component.ts @@ -1,9 +1,10 @@ import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; import { AnnotationWrapper } from '@models/file/annotation.wrapper'; import { AnnotationReferencesService } from '../../services/annotation-references.service'; -import { Observable } from 'rxjs'; -import { filter, map } from 'rxjs/operators'; +import { Observable, switchMap } from 'rxjs'; +import { filter } from 'rxjs/operators'; import { FileDataService } from '../../services/file-data.service'; +import { filterEach } from '../../../../../../../../libs/common-ui/src'; @Component({ selector: 'redaction-annotation-references-list', @@ -21,7 +22,7 @@ export class AnnotationReferencesListComponent { private get _annotationReferences(): Observable { return this.annotationReferencesService.annotation$.pipe( 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)))), ); } diff --git a/apps/red-ui/src/app/modules/file-preview/components/file-workload/file-workload.component.ts b/apps/red-ui/src/app/modules/file-preview/components/file-workload/file-workload.component.ts index b5d9d31cd..42e16cd03 100644 --- a/apps/red-ui/src/app/modules/file-preview/components/file-workload/file-workload.component.ts +++ b/apps/red-ui/src/app/modules/file-preview/components/file-workload/file-workload.component.ts @@ -24,7 +24,7 @@ import { shareDistinctLast, shareLast, } from '@iqser/common-ui'; -import { BehaviorSubject, combineLatest, Observable } from 'rxjs'; +import { combineLatest, Observable } from 'rxjs'; import { map, tap } from 'rxjs/operators'; import { File } from '@red/domain'; import { ExcludedPagesService } from '../../services/excluded-pages.service'; @@ -66,7 +66,6 @@ export class FileWorkloadComponent { readonly showExcludedPages$: Observable; readonly title$: Observable; readonly isHighlights$: Observable; - private _annotations$ = new BehaviorSubject([]); @ViewChild('annotationsElement') private readonly _annotationsElement: ElementRef; @ViewChild('quickNavigation') private readonly _quickNavigationElement: ElementRef; @@ -89,11 +88,6 @@ export class FileWorkloadComponent { this.title$ = this._title$; } - @Input() - set annotations(value: AnnotationWrapper[]) { - this._annotations$.next(value); - } - get activeAnnotations(): AnnotationWrapper[] | undefined { return this.displayedAnnotations.get(this.activeViewerPage); } @@ -142,7 +136,7 @@ export class FileWorkloadComponent { const primary$ = this.filterService.getFilterModels$('primaryFilters'); 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)), ); } diff --git a/apps/red-ui/src/app/modules/file-preview/components/pdf-viewer/pdf-viewer.component.ts b/apps/red-ui/src/app/modules/file-preview/components/pdf-viewer/pdf-viewer.component.ts index b18b4d949..73b1367dc 100644 --- a/apps/red-ui/src/app/modules/file-preview/components/pdf-viewer/pdf-viewer.component.ts +++ b/apps/red-ui/src/app/modules/file-preview/components/pdf-viewer/pdf-viewer.component.ts @@ -41,6 +41,7 @@ import { PageRotationService } from '../../services/page-rotation.service'; import { ALLOWED_KEYBOARD_SHORTCUTS, HeaderElements, TextPopups } from '../../shared/constants'; import { FilePreviewDialogService } from '../../services/file-preview-dialog.service'; import { loadCompareDocumentWrapper } from '../../../dossier/utils/compare-mode.utils'; +import { FileDataService } from '../../services/file-data.service'; import Tools = Core.Tools; import TextTool = Tools.TextTool; import Annotation = Core.Annotations.Annotation; @@ -60,7 +61,6 @@ function getDivider(hiddenOn?: readonly ('desktop' | 'mobile' | 'tablet')[]) { export class PdfViewerComponent extends AutoUnsubscribe implements OnInit, OnChanges { @Input() dossier: Dossier; @Input() canPerformActions = false; - @Input() annotations: AnnotationWrapper[]; @Output() readonly fileReady = new EventEmitter(); @Output() readonly annotationSelected = new EventEmitter(); @Output() readonly manualAnnotationRequested = new EventEmitter(); @@ -89,6 +89,7 @@ export class PdfViewerComponent extends AutoUnsubscribe implements OnInit, OnCha private readonly _loadingService: LoadingService, private readonly _fileManagementService: FileManagementService, private readonly _pageRotationService: PageRotationService, + private readonly _fileDataService: FileDataService, readonly stateService: FilePreviewStateService, readonly viewModeService: ViewModeService, readonly multiSelectService: MultiSelectService, @@ -226,18 +227,19 @@ export class PdfViewerComponent extends AutoUnsubscribe implements OnInit, OnCha this.pdf.disableHotkeys(); 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; this.annotationSelected.emit(nextAnnotations.map(ann => ann.Id)); if (action === 'deselected') { - this._toggleRectangleAnnotationAction(true); - } else { - if (!this.multiSelectService.isEnabled) { - this.pdf.deselectAnnotations(this.annotations.filter(wrapper => !nextAnnotations.find(ann => ann.Id === wrapper.id))); - } - this._configureAnnotationSpecificActions(annotations); - this._toggleRectangleAnnotationAction(annotations.length === 1 && annotations[0].ReadOnly); + return this._toggleRectangleAnnotationAction(true); } + + if (!this.multiSelectService.isEnabled) { + const visibleAnnotations = await this._fileDataService.visibleAnnotations; + this.pdf.deselectAnnotations(visibleAnnotations.filter(wrapper => !nextAnnotations.find(ann => ann.Id === wrapper.id))); + } + this.#configureAnnotationSpecificActions(annotations); + this._toggleRectangleAnnotationAction(annotations.length === 1 && annotations[0].ReadOnly); }); 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) { 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([]); if (annotationWrappers.length === 0) { @@ -513,7 +516,7 @@ export class PdfViewerComponent extends AutoUnsubscribe implements OnInit, OnCha this.annotationManager.showAnnotations(viewerAnnotations); } this.annotationManager.deselectAllAnnotations(); - this._annotationActionsService.updateHiddenAnnotation(this.annotations, viewerAnnotations, allAreVisible); + this._fileDataService.updateHiddenAnnotations(viewerAnnotations, allAreVisible); }); }, }, diff --git a/apps/red-ui/src/app/modules/file-preview/file-preview-screen.component.html b/apps/red-ui/src/app/modules/file-preview/file-preview-screen.component.html index 5a078278f..4769f0c3e 100644 --- a/apps/red-ui/src/app/modules/file-preview/file-preview-screen.component.html +++ b/apps/red-ui/src/app/modules/file-preview/file-preview-screen.component.html @@ -69,7 +69,6 @@ (pageChanged)="viewerPageChanged($event)" (viewerReady)="viewerReady()" *ngIf="displayPdfViewer" - [annotations]="visibleAnnotations" [canPerformActions]="canPerformAnnotationActions$ | async" [class.hidden]="!ready" [dossier]="dossier" @@ -95,7 +94,6 @@ *ngIf="!file.excluded" [activeViewerPage]="activeViewerPage" [annotationActionsTemplate]="annotationActionsTemplate" - [annotations]="visibleAnnotations" [dialogRef]="dialogRef" [file]="file" [selectedAnnotations]="selectedAnnotations" diff --git a/apps/red-ui/src/app/modules/file-preview/file-preview-screen.component.ts b/apps/red-ui/src/app/modules/file-preview/file-preview-screen.component.ts index 3d9b9318b..771feb1d0 100644 --- a/apps/red-ui/src/app/modules/file-preview/file-preview-screen.component.ts +++ b/apps/red-ui/src/app/modules/file-preview/file-preview-screen.component.ts @@ -125,14 +125,6 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni return this._pageRotationService.hasRotations(); } - get visibleAnnotations(): AnnotationWrapper[] { - return this._fileDataService.getVisibleAnnotations(this._viewModeService.viewMode); - } - - get allAnnotations(): AnnotationWrapper[] { - return this._fileDataService.allAnnotations; - } - private get _canPerformAnnotationActions$() { const viewMode$ = this._viewModeService.viewMode$.pipe(tap(() => this.#deactivateMultiSelect())); @@ -154,15 +146,17 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni 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 redactions = annotations.filter(a => a.getCustomData('redaction')); switch (this._viewModeService.viewMode) { case 'STANDARD': { this._setAnnotationsColor(redactions, 'annotationColor'); + const wrappers = await this._fileDataService.annotations; + const ocrAnnotationIds = wrappers.filter(a => a.isOCR).map(a => a.id); + const standardEntries = annotations .filter(a => a.getCustomData('changeLogRemoved') === 'false') .filter(a => !ocrAnnotationIds.includes(a.Id)); @@ -192,14 +186,14 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni case 'TEXT_HIGHLIGHTS': { this._loadingService.start(); this._pdf.hideAnnotations(annotations); - await this._fileDataService.loadTextHighlights(); - await this._annotationDrawService.drawAnnotations(this._fileDataService.textHighlightAnnotations); + const highlights = await this._fileDataService.loadTextHighlights(); + await this._annotationDrawService.drawAnnotations(highlights); this._loadingService.stop(); } } await this._stampPDF(); - this.rebuildFilters(); + await this.rebuildFilters(); } ngOnDetach(): void { @@ -237,7 +231,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni this.displayPdfViewer = true; } - rebuildFilters(deletePreviousAnnotations = false): void { + async rebuildFilters(deletePreviousAnnotations = false) { const startTime = new Date().getTime(); if (deletePreviousAnnotations) { this._pdf.deleteAnnotations(); @@ -246,7 +240,8 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni } 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; this._filterService.addFilterGroup({ slug: 'primaryFilters', @@ -266,9 +261,10 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni 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 - .map(id => this.visibleAnnotations.find(annotationWrapper => annotationWrapper.id === id)) + .map(id => visibleAnnotations.find(annotation => annotation.id === id)) .filter(ann => ann !== undefined); if (this.selectedAnnotations.length > 1) { this.multiSelectService.activate(); @@ -447,10 +443,10 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni download(await firstValueFrom(originalFile), file.filename); } - #deactivateMultiSelect(): void { + async #deactivateMultiSelect() { this.multiSelectService.deactivate(); this._pdf.deselectAllAnnotations(); - this.handleAnnotationSelected([]); + await this.handleAnnotationSelected([]); } private _setExcludedPageStyles() { @@ -498,7 +494,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni } 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( document, this._pdf.PDFNet, @@ -605,7 +601,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni 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(); 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 || []; - this.rebuildFilters(); + await this.rebuildFilters(); const startTime = new Date().getTime(); - const annotations = this.allAnnotations; + const annotations = await this._fileDataService.annotations; const newAnnotations = newAnnotationsFilter ? annotations.filter(newAnnotationsFilter) : annotations; if (currentFilters) { - this._handleDeltaAnnotationFilters(currentFilters, this.visibleAnnotations); + const visibleAnnotations = await this._fileDataService.visibleAnnotations; + this._handleDeltaAnnotationFilters(currentFilters, visibleAnnotations); } await this._annotationDrawService.drawAnnotations(newAnnotations); diff --git a/apps/red-ui/src/app/modules/file-preview/services/annotation-actions.service.ts b/apps/red-ui/src/app/modules/file-preview/services/annotation-actions.service.ts index 70702b1e1..02722dbb5 100644 --- a/apps/red-ui/src/app/modules/file-preview/services/annotation-actions.service.ts +++ b/apps/red-ui/src/app/modules/file-preview/services/annotation-actions.service.ts @@ -24,7 +24,6 @@ import { MatDialog } from '@angular/material/dialog'; import { FilePreviewStateService } from './file-preview-state.service'; import { PdfViewer } from './pdf-viewer.service'; import { FilePreviewDialogService } from './file-preview-dialog.service'; -import Annotation = Core.Annotations.Annotation; import Quad = Core.Math.Quad; @Injectable() @@ -414,12 +413,6 @@ export class AnnotationActionsService { 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) { $event?.stopPropagation(); diff --git a/apps/red-ui/src/app/modules/file-preview/services/annotation-draw.service.ts b/apps/red-ui/src/app/modules/file-preview/services/annotation-draw.service.ts index c2fbf02cd..ab8fcbd7b 100644 --- a/apps/red-ui/src/app/modules/file-preview/services/annotation-draw.service.ts +++ b/apps/red-ui/src/app/modules/file-preview/services/annotation-draw.service.ts @@ -14,6 +14,7 @@ import { DossiersService } from '@services/dossiers/dossiers.service'; import { PdfViewer } from './pdf-viewer.service'; import { FilePreviewStateService } from './file-preview-state.service'; import { ViewModeService } from './view-mode.service'; +import { FileDataService } from './file-data.service'; import Annotation = Core.Annotations.Annotation; @Injectable() @@ -31,6 +32,7 @@ export class AnnotationDrawService { private readonly _pdf: PdfViewer, private readonly _state: FilePreviewStateService, private readonly _viewModeService: ViewModeService, + private readonly _fileDataService: FileDataService, ) {} drawAnnotations(annotationWrappers: AnnotationWrapper[]) { @@ -200,7 +202,7 @@ export class AnnotationDrawService { annotationWrapper.isChangeLogRemoved || (this._skippedService.hideSkipped && annotationWrapper.isSkipped) || annotationWrapper.isOCR || - annotationWrapper.hidden; + this._fileDataService.isHidden(annotationWrapper.annotationId); annotation.setCustomData('redact-manager', 'true'); annotation.setCustomData('redaction', String(annotationWrapper.previewAnnotation)); annotation.setCustomData('skipped', String(annotationWrapper.isSkipped)); diff --git a/apps/red-ui/src/app/modules/file-preview/services/file-data.service.ts b/apps/red-ui/src/app/modules/file-preview/services/file-data.service.ts index eb9574377..d3198e293 100644 --- a/apps/red-ui/src/app/modules/file-preview/services/file-data.service.ts +++ b/apps/red-ui/src/app/modules/file-preview/services/file-data.service.ts @@ -1,66 +1,100 @@ import { ChangeType, File, + ImportedRedaction, IRedactionLog, IRedactionLogEntry, IViewedPage, LogEntryStatus, ManualRedactionType, - TextHighlightResponse, ViewMode, + ViewModes, } from '@red/domain'; import { AnnotationWrapper } from '../../../models/file/annotation.wrapper'; 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 { Injectable } from '@angular/core'; import { FilePreviewStateService } from './file-preview-state.service'; import { ViewedPagesService } from '../../../services/entity-services/viewed-pages.service'; import { UserPreferenceService } from '../../../services/user-preference.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 { 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 { 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() export class FileDataService { - static readonly DELTA_VIEW_TIME = 10 * 60 * 1000; // 10 minutes; viewedPages: IViewedPage[] = []; - allAnnotations: AnnotationWrapper[] = []; readonly hasChangeLog$ = new BehaviorSubject(false); missingTypes = new Set(); - textHighlightAnnotations: AnnotationWrapper[] = []; shouldUpdateAnnotations = false; + readonly annotations$: Observable; + readonly visibleAnnotations$: Observable; + readonly hiddenAnnotations = new Set(); + #redactionLog: IRedactionLog; #redactionLogHash = ''; + readonly #redactionLog$ = new BehaviorSubject({}); + readonly #textHighlights$ = new BehaviorSubject([]); constructor( private readonly _state: FilePreviewStateService, private readonly _viewedPagesService: ViewedPagesService, + private readonly _viewModeService: ViewModeService, private readonly _userPreferenceService: UserPreferenceService, private readonly _dictionariesMapService: DictionariesMapService, private readonly _permissionsService: PermissionsService, private readonly _redactionLogService: RedactionLogService, private readonly _textHighlightsService: TextHighlightService, private readonly _toaster: Toaster, - ) {} - - set textHighlights(textHighlightResponse: TextHighlightResponse) { - const highlights = []; - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - for (const color of Object.keys(textHighlightResponse.redactionPerColor)) { - for (const entry of textHighlightResponse.redactionPerColor[color]) { - const annotation = AnnotationWrapper.fromHighlight(color, entry); - highlights.push(annotation); - } - } - this.textHighlightAnnotations = highlights; + ) { + this.annotations$ = this.#annotations$; + this.visibleAnnotations$ = this._viewModeService.viewMode$.pipe( + switchMap(viewMode => + iif( + () => viewMode === ViewModes.TEXT_HIGHLIGHTS, + this.#textHighlights$, + this.annotations$.pipe(map(annotations => this.getVisibleAnnotations(annotations, viewMode))), + ), + ), + log('visible annotations: '), + 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; const hash = require('object-hash'); const newRedactionLogHash = hash(redactionLog.redactionLogEntry ?? []); @@ -73,7 +107,6 @@ export class FileDataService { this.shouldUpdateAnnotations = true; this.#redactionLogHash = newRedactionLogHash; this.missingTypes.clear(); - await this.#buildAllAnnotations(); } async load(file: File) { @@ -90,8 +123,11 @@ export class FileDataService { async loadTextHighlights() { const { dossierId, fileId } = this._state; - const highlights = this._textHighlightsService.getTextHighlights(dossierId, fileId).pipe(catchError(() => of({}))); - this.textHighlights = await firstValueFrom(highlights); + const redactionPerColor = await firstValueFrom(this._textHighlightsService.getTextHighlights(dossierId, fileId)); + const textHighlights = this.#buildTextHighlights(redactionPerColor); + this.#textHighlights$.next(textHighlights); + + return textHighlights; } getViewedPagesFor(file: File) { @@ -105,60 +141,61 @@ export class FileDataService { 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)), catchError(() => of({})), + tap(redactionLog => this.#redactionLog$.next(redactionLog)), ); - return this.setRedactionLog(await firstValueFrom(redactionLog$)); + this.setRedactionLog(await firstValueFrom(redactionLog$)); } - getVisibleAnnotations(viewMode: ViewMode) { - if (viewMode === 'TEXT_HIGHLIGHTS') { - return this.textHighlightAnnotations; - } - - return this.allAnnotations.filter(annotation => { + getVisibleAnnotations(annotations: AnnotationWrapper[], viewMode: ViewMode) { + return annotations.filter(annotation => { if (viewMode === 'STANDARD') { return !annotation.isChangeLogRemoved; - } else if (viewMode === 'DELTA') { - return annotation.isChangeLogEntry; - } else { - return annotation.previewAnnotation; } + + if (viewMode === 'DELTA') { + return annotation.isChangeLogEntry; + } + + return annotation.previewAnnotation; }); } - async #buildAllAnnotations() { - const file = await this._state.file; - const entries: RedactionLogEntry[] = this.#convertData(file); + updateHiddenAnnotations(viewerAnnotations: Annotation[], hidden: boolean) { + const annotationId = viewerAnnotations[0].Id; + if (hidden) { + this.hiddenAnnotations.add(annotationId); + } else { + this.hiddenAnnotations.delete(annotationId); + } + } - const previousAnnotations = [...this.allAnnotations]; - this.allAnnotations = entries - .map(entry => AnnotationWrapper.fromData(entry)) - .filter(ann => ann.manual || !file.excludedPages.includes(ann.pageNumber)); + isHidden(annotationId: string) { + return this.hiddenAnnotations.has(annotationId); + } - if (!this._userPreferenceService.areDevFeaturesEnabled) { - this.allAnnotations = this.allAnnotations.filter(annotation => !annotation.isFalsePositive); + #buildTextHighlights(redactionPerColor: Record): AnnotationWrapper[] { + if (!redactionPerColor) { + return []; } - this._setHiddenPropertyToNewAnnotations(this.allAnnotations, previousAnnotations); + const highlights = Object.entries(redactionPerColor); + return highlights.flatMap(([color, entries]) => entries.map(entry => AnnotationWrapper.fromHighlight(color, entry))); } - private _setHiddenPropertyToNewAnnotations(newAnnotations: AnnotationWrapper[], oldAnnotations: AnnotationWrapper[]) { - newAnnotations.forEach(newAnnotation => { - const oldAnnotation = oldAnnotations.find(a => a.annotationId === newAnnotation.annotationId); - if (oldAnnotation) { - newAnnotation.hidden = oldAnnotation.hidden; - } - }); + #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(file: File): RedactionLogEntry[] { + #convertData(redactionLog: IRedactionLog, file: File): RedactionLogEntry[] { let result: RedactionLogEntry[] = []; - const reasonAnnotationIds: { [key: string]: RedactionLogEntry[] } = {}; 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 dictionaryData = _dictionaryData.find(dict => dict.type === redactionLogEntry.type); if (!dictionaryData) { @@ -171,7 +208,7 @@ export class FileDataService { changeLogValues.changeLogType, changeLogValues.isChangeLogEntry, changeLogValues.hidden, - this.#redactionLog.legalBasis, + redactionLog.legalBasis, !!dictionaryData?.hint, ); @@ -229,7 +266,7 @@ export class FileDataService { // page has been seen -> let's see if it's a change 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 const relevantChanges = viableChanges.filter(change => moment(change.dateTime).valueOf() > viewTime); // at least one unseen change diff --git a/libs/red-domain/src/lib/shared/view-mode.ts b/libs/red-domain/src/lib/shared/view-mode.ts index 6db1dbf5a..5f7ee9f9d 100644 --- a/libs/red-domain/src/lib/shared/view-mode.ts +++ b/libs/red-domain/src/lib/shared/view-mode.ts @@ -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; diff --git a/libs/red-domain/src/lib/text-highlight/text-highlight.response.ts b/libs/red-domain/src/lib/text-highlight/text-highlight.response.ts index e87e48612..6a79ec21c 100644 --- a/libs/red-domain/src/lib/text-highlight/text-highlight.response.ts +++ b/libs/red-domain/src/lib/text-highlight/text-highlight.response.ts @@ -5,5 +5,5 @@ export interface TextHighlightResponse { dossierId?: string; fileId?: string; operation?: TextHighlightOperation; - redactionPerColor?: { [key: string]: ImportedRedaction[] }; + redactionPerColor?: Record; }