From 2601822a32d379b17e51c3a4d1221ebf2008e20c Mon Sep 17 00:00:00 2001 From: Dan Percic Date: Fri, 9 Jun 2023 18:04:38 +0300 Subject: [PATCH] RED-6829: view modes to signals --- apps/red-ui/src/app/app.module.ts | 6 +- .../src/app/models/file/annotation.wrapper.ts | 7 +- .../annotations-list.component.ts | 4 +- .../file-workload/file-workload.component.ts | 4 +- .../view-switch/view-switch.component.ts | 2 +- .../file-preview-screen.component.ts | 11 +- .../services/file-data.service.ts | 153 ++++++++++-------- .../services/pdf-proxy.service.ts | 4 +- .../file-preview/services/stamp.service.ts | 4 +- .../services/view-mode.service.ts | 71 +++----- 10 files changed, 131 insertions(+), 135 deletions(-) diff --git a/apps/red-ui/src/app/app.module.ts b/apps/red-ui/src/app/app.module.ts index b85a55d48..e4d0d0f72 100644 --- a/apps/red-ui/src/app/app.module.ts +++ b/apps/red-ui/src/app/app.module.ts @@ -134,14 +134,14 @@ export const appModuleFactory = (config: AppConfig) => { features: { ANNOTATIONS: { color: 'aqua', - enabled: false, + enabled: true, level: NgxLoggerLevel.DEBUG, }, FILTERS: { - enabled: false, + enabled: true, }, PDF: { - enabled: false, + enabled: true, }, FILE: { enabled: false, diff --git a/apps/red-ui/src/app/models/file/annotation.wrapper.ts b/apps/red-ui/src/app/models/file/annotation.wrapper.ts index f03c7d713..3b394cf67 100644 --- a/apps/red-ui/src/app/models/file/annotation.wrapper.ts +++ b/apps/red-ui/src/app/models/file/annotation.wrapper.ts @@ -13,6 +13,7 @@ import { IManualChange, IPoint, IRectangle, + LogEntryEngine, LogEntryStatuses, LowLevelFilterTypes, ManualRedactionTypes, @@ -23,7 +24,7 @@ import { SuperTypes, } from '@red/domain'; import { RedactionLogEntry } from '@models/file/redaction-log.entry'; -import { IListable, List } from '@iqser/common-ui'; +import { IListable } from '@iqser/common-ui'; import { chronologicallyBy, timestampOf } from '../../modules/file-preview/services/file-data.service'; export class AnnotationWrapper implements IListable, Record { @@ -52,7 +53,7 @@ export class AnnotationWrapper implements IListable, Record { legalBasisChangeValue?: string; rectangle?: boolean; section?: string; - reference: List; + reference: string[]; imported?: boolean; image?: boolean; manual?: boolean; @@ -62,7 +63,7 @@ export class AnnotationWrapper implements IListable, Record { textBefore?: string; isChangeLogEntry = false; changeLogType?: 'ADDED' | 'REMOVED' | 'CHANGED'; - engines?: string[]; + engines?: LogEntryEngine[]; hasBeenResized: boolean; hasBeenRecategorized: boolean; hasLegalBasisChanged: boolean; diff --git a/apps/red-ui/src/app/modules/file-preview/components/annotations-list/annotations-list.component.ts b/apps/red-ui/src/app/modules/file-preview/components/annotations-list/annotations-list.component.ts index 7bc381fb2..4d499e40e 100644 --- a/apps/red-ui/src/app/modules/file-preview/components/annotations-list/annotations-list.component.ts +++ b/apps/red-ui/src/app/modules/file-preview/components/annotations-list/annotations-list.component.ts @@ -38,7 +38,7 @@ export class AnnotationsListComponent extends HasScrollbarDirective implements O } ngOnChanges(): void { - if (this._viewModeService.isEarmarks) { + if (this._viewModeService.isEarmarks()) { this._updateEarmarksGroups(); } @@ -78,7 +78,7 @@ export class AnnotationsListComponent extends HasScrollbarDirective implements O } showHighlightGroup(idx: number): EarmarkGroup { - return this._viewModeService.isEarmarks && this.earmarkGroups$.value.find(h => h.startIdx === idx); + return this._viewModeService.isEarmarks() && this.earmarkGroups$.value.find(h => h.startIdx === idx); } protected readonly _trackBy = (index: number, listItem: ListItem) => listItem.item.id; 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 522a373ce..9a5847844 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 @@ -133,7 +133,7 @@ export class FileWorkloadComponent extends AutoUnsubscribe implements OnDestroy private get _isEarmarks$(): Observable { return this.viewModeService.viewMode$.pipe( tap(() => this._scrollViews()), - map(() => this.viewModeService.isEarmarks), + map(() => this.viewModeService.isEarmarks()), ); } @@ -373,7 +373,7 @@ export class FileWorkloadComponent extends AutoUnsubscribe implements OnDestroy return; } - if (this._viewModeService.isRedacted) { + if (this._viewModeService.isRedacted()) { annotations = annotations.filter(a => !bool(a.isChangeLogRemoved)); annotations = this._suggestionsService.filterWorkloadSuggestionsInPreview(annotations); } diff --git a/apps/red-ui/src/app/modules/file-preview/components/view-switch/view-switch.component.ts b/apps/red-ui/src/app/modules/file-preview/components/view-switch/view-switch.component.ts index ba927b32e..d17fbd564 100644 --- a/apps/red-ui/src/app/modules/file-preview/components/view-switch/view-switch.component.ts +++ b/apps/red-ui/src/app/modules/file-preview/components/view-switch/view-switch.component.ts @@ -48,7 +48,7 @@ export class ViewSwitchComponent { if (viewMode === ViewModes.REDACTED) { return this.#switchToRedactedView(); } - this.viewModeService.viewMode = viewMode; + this.viewModeService.switchTo(viewMode); } async #switchToRedactedView() { 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 b8101a72c..8c826f479 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 @@ -171,7 +171,7 @@ export class FilePreviewScreenComponent } get #earmarks$() { - const isEarmarksViewMode$ = this._viewModeService.viewMode$.pipe(filter(() => this._viewModeService.isEarmarks)); + const isEarmarksViewMode$ = this._viewModeService.viewMode$.pipe(filter(() => this._viewModeService.isEarmarks())); const earmarks$ = isEarmarksViewMode$.pipe( tap(() => this._loadingService.start()), @@ -181,7 +181,7 @@ export class FilePreviewScreenComponent ); const currentPageIfEarmarksView$ = combineLatest([this.pdf.currentPage$, this._viewModeService.viewMode$]).pipe( - filter(() => this._viewModeService.isEarmarks), + filter(() => this._viewModeService.isEarmarks()), map(([page]) => page), ); @@ -213,12 +213,12 @@ export class FilePreviewScreenComponent } async updateViewMode(): Promise { - this._logger.info(`[PDF] Update ${this._viewModeService.viewMode} view mode`); + this._logger.info(`[PDF] Update ${this._viewModeService.viewMode()} view mode`); const annotations = this._annotationManager.get(a => bool(a.getCustomData('redact-manager'))); const redactions = annotations.filter(a => bool(a.getCustomData('redaction'))); - switch (this._viewModeService.viewMode) { + switch (this._viewModeService.viewMode()) { case ViewModes.STANDARD: { const wrappers = await this._fileDataService.annotations; const ocrAnnotationIds = wrappers.filter(a => a.isOCR).map(a => a.id); @@ -260,8 +260,11 @@ export class FilePreviewScreenComponent } } + this._logger.info('[PDF] Stamp pdf'); await this._stampService.stampPDF(); + this._logger.info('[PDF] Rebuild filters'); this.#rebuildFilters(); + this._logger.info('[PDF] Update done'); } ngOnDetach() { 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 25c7793f1..ae9005f35 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 @@ -10,7 +10,7 @@ import { ViewModes, } from '@red/domain'; import { AnnotationWrapper } from '@models/file/annotation.wrapper'; -import { BehaviorSubject, firstValueFrom, iif, Observable, Subject, Subscription } from 'rxjs'; +import { BehaviorSubject, firstValueFrom, Observable, Subject, Subscription } from 'rxjs'; import { RedactionLogEntry } from '@models/file/redaction-log.entry'; import { Injectable, OnDestroy } from '@angular/core'; import { FilePreviewStateService } from './file-preview-state.service'; @@ -76,11 +76,9 @@ export class FileDataService extends EntitiesService - iif( - () => viewMode === ViewModes.TEXT_HIGHLIGHTS, - this.#earmarks$.pipe(map(textHighlights => ([] as AnnotationWrapper[]).concat(...textHighlights.values()))), - this.annotations$.pipe(map(annotations => this.#getVisibleAnnotations(annotations, viewMode))), - ), + viewMode === ViewModes.TEXT_HIGHLIGHTS + ? this.#earmarks$.pipe(map(textHighlights => ([] as AnnotationWrapper[]).concat(...textHighlights.values()))) + : this.annotations$.pipe(map(annotations => this.#getVisibleAnnotations(annotations, viewMode))), ), tap(annotations => this.setEntities(annotations)), ) @@ -93,6 +91,7 @@ export class FileDataService extends EntitiesService console.time('buildAnnotations')), withLatestFrom(this._state.file$), tap(([redactionLog, file]) => this.#buildRemovedRedactions(redactionLog, file)), switchMap(([redactionLog, file]) => this.#buildAnnotations(redactionLog, file)), @@ -100,6 +99,7 @@ export class FileDataService extends EntitiesService this._userPreferenceService.areDevFeaturesEnabled ? annotations : annotations.filter(a => !a.isFalsePositive), ), + tap(() => console.timeEnd('buildAnnotations')), shareLast(), ); } @@ -129,10 +129,11 @@ export class FileDataService extends EntitiesService this.#redactionLog$.next(redactionLog)))); + const redactionLog = await firstValueFrom(redactionLog$); + this.#redactionLog$.next(redactionLog); + console.timeEnd('redaction-log'); + this._logger.info('[REDACTION-LOG] Redaction log loaded', redactionLog); + return redactionLog; } #checkMissingTypes() { @@ -173,10 +180,12 @@ export class FileDataService extends EntitiesService - AnnotationWrapper.fromData(entry, this._state.dictionaries, this._defaultColorsService.find(this._state.dossierTemplateId)), - ); + const dictionaries = this._state.dictionaries; + const defaultColors = this._defaultColorsService.find(this._state.dossierTemplateId); + const annotations: AnnotationWrapper[] = []; - return annotations.filter(ann => ann.manual || !file.excludedPages.includes(ann.pageNumber)); + for (const entry of entries) { + const pageNumber = entry.positions[0]?.page; + const manual = entry.manualChanges?.length > 0; + if (!manual && file.excludedPages.includes(pageNumber)) { + continue; + } + + annotations.push(AnnotationWrapper.fromData(entry, dictionaries, defaultColors)); + } + + return annotations; } async #buildRemovedRedactions(redactionLog: IRedactionLog, file: File): Promise { - if (redactionLog.redactionLogEntry) { - const redactionLogCopy = JSON.parse(JSON.stringify(redactionLog)); - redactionLogCopy.redactionLogEntry = redactionLogCopy.redactionLogEntry?.reduce((filtered, entry) => { - const lastChange = entry.manualChanges.at(-1); + const redactionLogCopy = JSON.parse(JSON.stringify(redactionLog)); - if ( - lastChange?.annotationStatus === LogEntryStatuses.REQUESTED && - !entry.hint && - !entry.reason.includes('requested to force hint') - ) { - entry.manualChanges.pop(); - entry.reason = null; - filtered.push(entry); - } - return filtered; - }, []); - const annotations = await this.#buildAnnotations(redactionLogCopy, file); - this._suggestionsService.removedRedactions = annotations.filter(a => !a.isSkipped); - } + redactionLogCopy.redactionLogEntry = redactionLogCopy.redactionLogEntry?.reduce((filtered, entry) => { + const lastChange = entry.manualChanges.at(-1); + if ( + lastChange?.annotationStatus === LogEntryStatuses.REQUESTED && + !entry.hint && + !entry.reason.includes('requested to force hint') + ) { + entry.manualChanges.pop(); + entry.reason = null; + filtered.push(entry); + } + return filtered; + }, []); + + const annotations = await this.#buildAnnotations(redactionLogCopy, file); + this._suggestionsService.removedRedactions = annotations.filter(a => !a.isSkipped); } async #convertData(redactionLog: IRedactionLog, file: File) { + if (!redactionLog.redactionLogEntry) { + return []; + } + const result: RedactionLogEntry[] = []; const sourceIdAnnotationIds: { [key: string]: RedactionLogEntry[] } = {}; + const dictionaries = this._state.dictionaries; let checkDictionary = true; - if (redactionLog.redactionLogEntry) { - for (const redactionLogEntry of redactionLog.redactionLogEntry) { - const changeLogValues = this.#getChangeLogValues(redactionLogEntry, file); - if (changeLogValues.hidden) { - continue; - } - - let dictionary = this._state.dictionaries.find(dict => dict.type === redactionLogEntry.type); - if (!dictionary && checkDictionary) { - const dictionaryRequest = this._dictionaryService.loadDictionaryDataForDossierTemplate(this._state.dossierTemplateId); - await firstValueFrom(dictionaryRequest); - checkDictionary = false; - dictionary = this._state.dictionaries.find(dict => dict.type === redactionLogEntry.type); - } - - if (!dictionary) { - this.missingTypes.add(redactionLogEntry.type); - continue; - } - - const redactionLogEntryWrapper: RedactionLogEntry = new RedactionLogEntry( - redactionLogEntry, - changeLogValues.changeLogType, - redactionLog.legalBasis ?? [], - !!dictionary?.hint, - ); - - if (redactionLogEntry.sourceId) { - if (!sourceIdAnnotationIds[redactionLogEntry.sourceId]) { - sourceIdAnnotationIds[redactionLogEntry.sourceId] = []; - } - sourceIdAnnotationIds[redactionLogEntry.sourceId].push(redactionLogEntryWrapper); - } - - result.push(redactionLogEntryWrapper); + for (const redactionLogEntry of redactionLog.redactionLogEntry) { + const changeLogValues = this.#getChangeLogValues(redactionLogEntry, file); + if (changeLogValues.hidden) { + continue; } + + let dictionary = dictionaries.find(dict => dict.type === redactionLogEntry.type); + if (!dictionary && checkDictionary) { + const dictionaryRequest = this._dictionaryService.loadDictionaryDataForDossierTemplate(this._state.dossierTemplateId); + await firstValueFrom(dictionaryRequest); + checkDictionary = false; + dictionary = dictionaries.find(dict => dict.type === redactionLogEntry.type); + } + + if (!dictionary) { + this.missingTypes.add(redactionLogEntry.type); + continue; + } + + const redactionLogEntryWrapper: RedactionLogEntry = new RedactionLogEntry( + redactionLogEntry, + changeLogValues.changeLogType, + redactionLog.legalBasis ?? [], + !!dictionary?.hint, + ); + + if (redactionLogEntry.sourceId) { + if (!sourceIdAnnotationIds[redactionLogEntry.sourceId]) { + sourceIdAnnotationIds[redactionLogEntry.sourceId] = []; + } + sourceIdAnnotationIds[redactionLogEntry.sourceId].push(redactionLogEntryWrapper); + } + + result.push(redactionLogEntryWrapper); } const sourceKeys = Object.keys(sourceIdAnnotationIds); diff --git a/apps/red-ui/src/app/modules/file-preview/services/pdf-proxy.service.ts b/apps/red-ui/src/app/modules/file-preview/services/pdf-proxy.service.ts index b7cd0b2d4..d7181a405 100644 --- a/apps/red-ui/src/app/modules/file-preview/services/pdf-proxy.service.ts +++ b/apps/red-ui/src/app/modules/file-preview/services/pdf-proxy.service.ts @@ -1,5 +1,5 @@ import { ChangeDetectorRef, inject, Injectable, NgZone } from '@angular/core'; -import { IHeaderElement, IManualRedactionEntry, ViewModes } from '@red/domain'; +import { IHeaderElement, IManualRedactionEntry } from '@red/domain'; import { Core } from '@pdftron/webviewer'; import { TranslateService } from '@ngx-translate/core'; import { @@ -196,7 +196,7 @@ export class PdfProxyService { private _handleCustomActions() { const isCurrentPageExcluded = this._state.file.isPageExcluded(this._pdf.currentPage); - if (this._viewModeService.viewMode === ViewModes.REDACTED) { + if (this._viewModeService.isRedacted()) { this._viewerHeaderService.enable([HeaderElements.TOGGLE_READABLE_REDACTIONS]); } else { this._viewerHeaderService.disable([HeaderElements.TOGGLE_READABLE_REDACTIONS]); diff --git a/apps/red-ui/src/app/modules/file-preview/services/stamp.service.ts b/apps/red-ui/src/app/modules/file-preview/services/stamp.service.ts index 76f483d3f..deab39e4c 100644 --- a/apps/red-ui/src/app/modules/file-preview/services/stamp.service.ts +++ b/apps/red-ui/src/app/modules/file-preview/services/stamp.service.ts @@ -8,8 +8,8 @@ import { PdfViewer } from '../../pdf-viewer/services/pdf-viewer.service'; import { REDDocumentViewer } from '../../pdf-viewer/services/document-viewer.service'; import { LicenseService } from '@services/license.service'; import { WatermarksMapService } from '@services/entity-services/watermarks-map.service'; -import PDFNet = Core.PDFNet; import { WATERMARK_HORIZONTAL_ALIGNMENTS, WATERMARK_VERTICAL_ALIGNMENTS } from '@red/domain'; +import PDFNet = Core.PDFNet; @Injectable() export class StampService { @@ -39,7 +39,7 @@ export class StampService { return; } - if (this._viewModeService.isRedacted) { + if (this._viewModeService.isRedacted()) { const { dossierTemplateId, previewWatermarkId } = this._state.dossier; if (previewWatermarkId) { await this._stampPreview(pdfDoc, dossierTemplateId, previewWatermarkId); diff --git a/apps/red-ui/src/app/modules/file-preview/services/view-mode.service.ts b/apps/red-ui/src/app/modules/file-preview/services/view-mode.service.ts index 58e0ec505..429fa88f6 100644 --- a/apps/red-ui/src/app/modules/file-preview/services/view-mode.service.ts +++ b/apps/red-ui/src/app/modules/file-preview/services/view-mode.service.ts @@ -1,77 +1,48 @@ -import { Injectable } from '@angular/core'; -import { BehaviorSubject, Observable } from 'rxjs'; +import { computed, Injectable, Signal, signal } from '@angular/core'; +import { Observable } from 'rxjs'; import { ViewMode, ViewModes } from '@red/domain'; -import { map } from 'rxjs/operators'; -import { shareDistinctLast } from '@iqser/common-ui'; +import { toObservable } from '@angular/core/rxjs-interop'; @Injectable() export class ViewModeService { readonly viewMode$: Observable; - readonly isRedacted$: Observable; - readonly isStandard$: Observable; - readonly isDelta$: Observable; - - readonly #viewMode$ = new BehaviorSubject('STANDARD'); + readonly viewMode: Signal; + readonly isEarmarks = computed(() => this.viewMode() === ViewModes.TEXT_HIGHLIGHTS); + readonly isRedacted = computed(() => this.viewMode() === ViewModes.REDACTED); + readonly isStandard = computed(() => this.viewMode() === ViewModes.STANDARD); + readonly isDelta = computed(() => this.viewMode() === ViewModes.DELTA); + readonly #viewMode = signal(ViewModes.STANDARD); constructor() { - this.viewMode$ = this.#viewMode$.asObservable(); - this.isRedacted$ = this._is('REDACTED'); - this.isStandard$ = this._is('STANDARD'); - this.isDelta$ = this._is('DELTA'); + this.viewMode$ = toObservable(this.#viewMode); + this.viewMode = this.#viewMode.asReadonly(); } - get onlyPagesWithAnnotations(): boolean { - return ([ViewModes.DELTA, ViewModes.TEXT_HIGHLIGHTS] as ViewMode[]).includes(this.viewMode); + get onlyPagesWithAnnotations() { + return ([ViewModes.DELTA, ViewModes.TEXT_HIGHLIGHTS] as ViewMode[]).includes(this.#viewMode()); } - get viewMode() { - return this.#viewMode$.value; + is(viewMode: ViewMode) { + return this.viewMode() === viewMode; } - set viewMode(mode: ViewMode) { - this.#viewMode$.next(mode); - } - - get isStandard() { - return this.#viewMode$.value === 'STANDARD'; - } - - get isDelta() { - return this.#viewMode$.value === 'DELTA'; - } - - get isRedacted() { - return this.#viewMode$.value === 'REDACTED'; - } - - get isEarmarks() { - return this.#viewMode$.value === 'TEXT_HIGHLIGHTS'; + switchTo(viewMode: ViewMode) { + this.#viewMode.set(viewMode); } switchToStandard() { - this._switchTo('STANDARD'); + this.switchTo(ViewModes.STANDARD); } switchToDelta() { - this._switchTo('DELTA'); + this.switchTo(ViewModes.DELTA); } switchToRedacted() { - this._switchTo('REDACTED'); + this.switchTo(ViewModes.REDACTED); } switchToHighlights() { - this._switchTo('TEXT_HIGHLIGHTS'); - } - - private _switchTo(mode: ViewMode) { - this.#viewMode$.next(mode); - } - - private _is(mode: ViewMode) { - return this.viewMode$.pipe( - map(value => value === mode), - shareDistinctLast(), - ); + this.switchTo(ViewModes.TEXT_HIGHLIGHTS); } }