RED-6829: view modes to signals

This commit is contained in:
Dan Percic 2023-06-09 18:04:38 +03:00
parent e7e6ab3e66
commit 2601822a32
10 changed files with 131 additions and 135 deletions

View File

@ -134,14 +134,14 @@ export const appModuleFactory = (config: AppConfig) => {
features: { features: {
ANNOTATIONS: { ANNOTATIONS: {
color: 'aqua', color: 'aqua',
enabled: false, enabled: true,
level: NgxLoggerLevel.DEBUG, level: NgxLoggerLevel.DEBUG,
}, },
FILTERS: { FILTERS: {
enabled: false, enabled: true,
}, },
PDF: { PDF: {
enabled: false, enabled: true,
}, },
FILE: { FILE: {
enabled: false, enabled: false,

View File

@ -13,6 +13,7 @@ import {
IManualChange, IManualChange,
IPoint, IPoint,
IRectangle, IRectangle,
LogEntryEngine,
LogEntryStatuses, LogEntryStatuses,
LowLevelFilterTypes, LowLevelFilterTypes,
ManualRedactionTypes, ManualRedactionTypes,
@ -23,7 +24,7 @@ import {
SuperTypes, SuperTypes,
} from '@red/domain'; } from '@red/domain';
import { RedactionLogEntry } from '@models/file/redaction-log.entry'; 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'; import { chronologicallyBy, timestampOf } from '../../modules/file-preview/services/file-data.service';
export class AnnotationWrapper implements IListable, Record<string, unknown> { export class AnnotationWrapper implements IListable, Record<string, unknown> {
@ -52,7 +53,7 @@ export class AnnotationWrapper implements IListable, Record<string, unknown> {
legalBasisChangeValue?: string; legalBasisChangeValue?: string;
rectangle?: boolean; rectangle?: boolean;
section?: string; section?: string;
reference: List; reference: string[];
imported?: boolean; imported?: boolean;
image?: boolean; image?: boolean;
manual?: boolean; manual?: boolean;
@ -62,7 +63,7 @@ export class AnnotationWrapper implements IListable, Record<string, unknown> {
textBefore?: string; textBefore?: string;
isChangeLogEntry = false; isChangeLogEntry = false;
changeLogType?: 'ADDED' | 'REMOVED' | 'CHANGED'; changeLogType?: 'ADDED' | 'REMOVED' | 'CHANGED';
engines?: string[]; engines?: LogEntryEngine[];
hasBeenResized: boolean; hasBeenResized: boolean;
hasBeenRecategorized: boolean; hasBeenRecategorized: boolean;
hasLegalBasisChanged: boolean; hasLegalBasisChanged: boolean;

View File

@ -38,7 +38,7 @@ export class AnnotationsListComponent extends HasScrollbarDirective implements O
} }
ngOnChanges(): void { ngOnChanges(): void {
if (this._viewModeService.isEarmarks) { if (this._viewModeService.isEarmarks()) {
this._updateEarmarksGroups(); this._updateEarmarksGroups();
} }
@ -78,7 +78,7 @@ export class AnnotationsListComponent extends HasScrollbarDirective implements O
} }
showHighlightGroup(idx: number): EarmarkGroup { 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<AnnotationWrapper>) => listItem.item.id; protected readonly _trackBy = (index: number, listItem: ListItem<AnnotationWrapper>) => listItem.item.id;

View File

@ -133,7 +133,7 @@ export class FileWorkloadComponent extends AutoUnsubscribe implements OnDestroy
private get _isEarmarks$(): Observable<boolean> { private get _isEarmarks$(): Observable<boolean> {
return this.viewModeService.viewMode$.pipe( return this.viewModeService.viewMode$.pipe(
tap(() => this._scrollViews()), tap(() => this._scrollViews()),
map(() => this.viewModeService.isEarmarks), map(() => this.viewModeService.isEarmarks()),
); );
} }
@ -373,7 +373,7 @@ export class FileWorkloadComponent extends AutoUnsubscribe implements OnDestroy
return; return;
} }
if (this._viewModeService.isRedacted) { if (this._viewModeService.isRedacted()) {
annotations = annotations.filter(a => !bool(a.isChangeLogRemoved)); annotations = annotations.filter(a => !bool(a.isChangeLogRemoved));
annotations = this._suggestionsService.filterWorkloadSuggestionsInPreview(annotations); annotations = this._suggestionsService.filterWorkloadSuggestionsInPreview(annotations);
} }

View File

@ -48,7 +48,7 @@ export class ViewSwitchComponent {
if (viewMode === ViewModes.REDACTED) { if (viewMode === ViewModes.REDACTED) {
return this.#switchToRedactedView(); return this.#switchToRedactedView();
} }
this.viewModeService.viewMode = viewMode; this.viewModeService.switchTo(viewMode);
} }
async #switchToRedactedView() { async #switchToRedactedView() {

View File

@ -171,7 +171,7 @@ export class FilePreviewScreenComponent
} }
get #earmarks$() { 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( const earmarks$ = isEarmarksViewMode$.pipe(
tap(() => this._loadingService.start()), tap(() => this._loadingService.start()),
@ -181,7 +181,7 @@ export class FilePreviewScreenComponent
); );
const currentPageIfEarmarksView$ = combineLatest([this.pdf.currentPage$, this._viewModeService.viewMode$]).pipe( const currentPageIfEarmarksView$ = combineLatest([this.pdf.currentPage$, this._viewModeService.viewMode$]).pipe(
filter(() => this._viewModeService.isEarmarks), filter(() => this._viewModeService.isEarmarks()),
map(([page]) => page), map(([page]) => page),
); );
@ -213,12 +213,12 @@ export class FilePreviewScreenComponent
} }
async updateViewMode(): Promise<void> { async updateViewMode(): Promise<void> {
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 annotations = this._annotationManager.get(a => bool(a.getCustomData('redact-manager')));
const redactions = annotations.filter(a => bool(a.getCustomData('redaction'))); const redactions = annotations.filter(a => bool(a.getCustomData('redaction')));
switch (this._viewModeService.viewMode) { switch (this._viewModeService.viewMode()) {
case ViewModes.STANDARD: { case ViewModes.STANDARD: {
const wrappers = await this._fileDataService.annotations; const wrappers = await this._fileDataService.annotations;
const ocrAnnotationIds = wrappers.filter(a => a.isOCR).map(a => a.id); 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(); await this._stampService.stampPDF();
this._logger.info('[PDF] Rebuild filters');
this.#rebuildFilters(); this.#rebuildFilters();
this._logger.info('[PDF] Update done');
} }
ngOnDetach() { ngOnDetach() {

View File

@ -10,7 +10,7 @@ import {
ViewModes, ViewModes,
} from '@red/domain'; } from '@red/domain';
import { AnnotationWrapper } from '@models/file/annotation.wrapper'; 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 { RedactionLogEntry } from '@models/file/redaction-log.entry';
import { Injectable, OnDestroy } from '@angular/core'; import { Injectable, OnDestroy } from '@angular/core';
import { FilePreviewStateService } from './file-preview-state.service'; import { FilePreviewStateService } from './file-preview-state.service';
@ -76,11 +76,9 @@ export class FileDataService extends EntitiesService<AnnotationWrapper, Annotati
this.#subscription = this._viewModeService.viewMode$ this.#subscription = this._viewModeService.viewMode$
.pipe( .pipe(
switchMap(viewMode => switchMap(viewMode =>
iif( viewMode === ViewModes.TEXT_HIGHLIGHTS
() => viewMode === ViewModes.TEXT_HIGHLIGHTS, ? this.#earmarks$.pipe(map(textHighlights => ([] as AnnotationWrapper[]).concat(...textHighlights.values())))
this.#earmarks$.pipe(map(textHighlights => ([] as AnnotationWrapper[]).concat(...textHighlights.values()))), : this.annotations$.pipe(map(annotations => this.#getVisibleAnnotations(annotations, viewMode))),
this.annotations$.pipe(map(annotations => this.#getVisibleAnnotations(annotations, viewMode))),
),
), ),
tap(annotations => this.setEntities(annotations)), tap(annotations => this.setEntities(annotations)),
) )
@ -93,6 +91,7 @@ export class FileDataService extends EntitiesService<AnnotationWrapper, Annotati
get #annotations$() { get #annotations$() {
return this.#redactionLog$.pipe( return this.#redactionLog$.pipe(
tap(() => console.time('buildAnnotations')),
withLatestFrom(this._state.file$), withLatestFrom(this._state.file$),
tap(([redactionLog, file]) => this.#buildRemovedRedactions(redactionLog, file)), tap(([redactionLog, file]) => this.#buildRemovedRedactions(redactionLog, file)),
switchMap(([redactionLog, file]) => this.#buildAnnotations(redactionLog, file)), switchMap(([redactionLog, file]) => this.#buildAnnotations(redactionLog, file)),
@ -100,6 +99,7 @@ export class FileDataService extends EntitiesService<AnnotationWrapper, Annotati
map(annotations => map(annotations =>
this._userPreferenceService.areDevFeaturesEnabled ? annotations : annotations.filter(a => !a.isFalsePositive), this._userPreferenceService.areDevFeaturesEnabled ? annotations : annotations.filter(a => !a.isFalsePositive),
), ),
tap(() => console.timeEnd('buildAnnotations')),
shareLast(), shareLast(),
); );
} }
@ -129,10 +129,11 @@ export class FileDataService extends EntitiesService<AnnotationWrapper, Annotati
async loadAnnotations(file: File) { async loadAnnotations(file: File) {
if (!file || file.isUnprocessed) { if (!file || file.isUnprocessed) {
this._logger.info('[ANNOTATIONS] File is null or unprocessed, skipping annotations loading');
return; return;
} }
this._logger.info('[ANNOTATIONS] Load annotations'); this._logger.info('[ANNOTATIONS] Loading annotations...');
await this.#loadViewedPages(file); await this.#loadViewedPages(file);
await this.loadRedactionLog(); await this.loadRedactionLog();
@ -155,9 +156,15 @@ export class FileDataService extends EntitiesService<AnnotationWrapper, Annotati
return earmarks; return earmarks;
} }
loadRedactionLog() { async loadRedactionLog() {
this._logger.info('[REDACTION-LOG] Loading redaction log...');
console.time('redaction-log');
const redactionLog$ = this._redactionLogService.getRedactionLog(this._state.dossierId, this._state.fileId); const redactionLog$ = this._redactionLogService.getRedactionLog(this._state.dossierId, this._state.fileId);
return firstValueFrom(redactionLog$.pipe(tap(redactionLog => 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() { #checkMissingTypes() {
@ -173,10 +180,12 @@ export class FileDataService extends EntitiesService<AnnotationWrapper, Annotati
async #loadViewedPages(file: File) { async #loadViewedPages(file: File) {
if (!this._permissionsService.canMarkPagesAsViewed(file)) { if (!this._permissionsService.canMarkPagesAsViewed(file)) {
this._viewedPagesMapService.set(file.fileId, []); this._viewedPagesMapService.set(file.fileId, []);
this._logger.info('[VIEWED-PAGES] Cannot mark pages as viewed, skip loading viewed pages');
return; return;
} }
this.#originalViewedPages = await this._viewedPagesService.load(file.dossierId, file.fileId); this.#originalViewedPages = await this._viewedPagesService.load(file.dossierId, file.fileId);
this._logger.info('[VIEWED-PAGES] Loaded viewed pages', this.#originalViewedPages);
this._viewedPagesMapService.set(file.fileId, this.#originalViewedPages); this._viewedPagesMapService.set(file.fileId, this.#originalViewedPages);
} }
@ -196,19 +205,28 @@ export class FileDataService extends EntitiesService<AnnotationWrapper, Annotati
async #buildAnnotations(redactionLog: IRedactionLog, file: File) { async #buildAnnotations(redactionLog: IRedactionLog, file: File) {
const entries = await this.#convertData(redactionLog, file); const entries = await this.#convertData(redactionLog, file);
const annotations = entries.map(entry => const dictionaries = this._state.dictionaries;
AnnotationWrapper.fromData(entry, this._state.dictionaries, this._defaultColorsService.find(this._state.dossierTemplateId)), 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<void> { async #buildRemovedRedactions(redactionLog: IRedactionLog, file: File): Promise<void> {
if (redactionLog.redactionLogEntry) {
const redactionLogCopy = JSON.parse(JSON.stringify(redactionLog)); const redactionLogCopy = JSON.parse(JSON.stringify(redactionLog));
redactionLogCopy.redactionLogEntry = redactionLogCopy.redactionLogEntry?.reduce((filtered, entry) => { redactionLogCopy.redactionLogEntry = redactionLogCopy.redactionLogEntry?.reduce((filtered, entry) => {
const lastChange = entry.manualChanges.at(-1); const lastChange = entry.manualChanges.at(-1);
if ( if (
lastChange?.annotationStatus === LogEntryStatuses.REQUESTED && lastChange?.annotationStatus === LogEntryStatuses.REQUESTED &&
!entry.hint && !entry.hint &&
@ -220,29 +238,33 @@ export class FileDataService extends EntitiesService<AnnotationWrapper, Annotati
} }
return filtered; return filtered;
}, []); }, []);
const annotations = await this.#buildAnnotations(redactionLogCopy, file); const annotations = await this.#buildAnnotations(redactionLogCopy, file);
this._suggestionsService.removedRedactions = annotations.filter(a => !a.isSkipped); this._suggestionsService.removedRedactions = annotations.filter(a => !a.isSkipped);
} }
}
async #convertData(redactionLog: IRedactionLog, file: File) { async #convertData(redactionLog: IRedactionLog, file: File) {
if (!redactionLog.redactionLogEntry) {
return [];
}
const result: RedactionLogEntry[] = []; const result: RedactionLogEntry[] = [];
const sourceIdAnnotationIds: { [key: string]: RedactionLogEntry[] } = {}; const sourceIdAnnotationIds: { [key: string]: RedactionLogEntry[] } = {};
const dictionaries = this._state.dictionaries;
let checkDictionary = true; let checkDictionary = true;
if (redactionLog.redactionLogEntry) {
for (const redactionLogEntry of redactionLog.redactionLogEntry) { for (const redactionLogEntry of redactionLog.redactionLogEntry) {
const changeLogValues = this.#getChangeLogValues(redactionLogEntry, file); const changeLogValues = this.#getChangeLogValues(redactionLogEntry, file);
if (changeLogValues.hidden) { if (changeLogValues.hidden) {
continue; continue;
} }
let dictionary = this._state.dictionaries.find(dict => dict.type === redactionLogEntry.type); let dictionary = dictionaries.find(dict => dict.type === redactionLogEntry.type);
if (!dictionary && checkDictionary) { if (!dictionary && checkDictionary) {
const dictionaryRequest = this._dictionaryService.loadDictionaryDataForDossierTemplate(this._state.dossierTemplateId); const dictionaryRequest = this._dictionaryService.loadDictionaryDataForDossierTemplate(this._state.dossierTemplateId);
await firstValueFrom(dictionaryRequest); await firstValueFrom(dictionaryRequest);
checkDictionary = false; checkDictionary = false;
dictionary = this._state.dictionaries.find(dict => dict.type === redactionLogEntry.type); dictionary = dictionaries.find(dict => dict.type === redactionLogEntry.type);
} }
if (!dictionary) { if (!dictionary) {
@ -266,7 +288,6 @@ export class FileDataService extends EntitiesService<AnnotationWrapper, Annotati
result.push(redactionLogEntryWrapper); result.push(redactionLogEntryWrapper);
} }
}
const sourceKeys = Object.keys(sourceIdAnnotationIds); const sourceKeys = Object.keys(sourceIdAnnotationIds);
return result.filter(r => !sourceKeys.includes(r.id)); return result.filter(r => !sourceKeys.includes(r.id));

View File

@ -1,5 +1,5 @@
import { ChangeDetectorRef, inject, Injectable, NgZone } from '@angular/core'; 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 { Core } from '@pdftron/webviewer';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { import {
@ -196,7 +196,7 @@ export class PdfProxyService {
private _handleCustomActions() { private _handleCustomActions() {
const isCurrentPageExcluded = this._state.file.isPageExcluded(this._pdf.currentPage); 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]); this._viewerHeaderService.enable([HeaderElements.TOGGLE_READABLE_REDACTIONS]);
} else { } else {
this._viewerHeaderService.disable([HeaderElements.TOGGLE_READABLE_REDACTIONS]); this._viewerHeaderService.disable([HeaderElements.TOGGLE_READABLE_REDACTIONS]);

View File

@ -8,8 +8,8 @@ import { PdfViewer } from '../../pdf-viewer/services/pdf-viewer.service';
import { REDDocumentViewer } from '../../pdf-viewer/services/document-viewer.service'; import { REDDocumentViewer } from '../../pdf-viewer/services/document-viewer.service';
import { LicenseService } from '@services/license.service'; import { LicenseService } from '@services/license.service';
import { WatermarksMapService } from '@services/entity-services/watermarks-map.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 { WATERMARK_HORIZONTAL_ALIGNMENTS, WATERMARK_VERTICAL_ALIGNMENTS } from '@red/domain';
import PDFNet = Core.PDFNet;
@Injectable() @Injectable()
export class StampService { export class StampService {
@ -39,7 +39,7 @@ export class StampService {
return; return;
} }
if (this._viewModeService.isRedacted) { if (this._viewModeService.isRedacted()) {
const { dossierTemplateId, previewWatermarkId } = this._state.dossier; const { dossierTemplateId, previewWatermarkId } = this._state.dossier;
if (previewWatermarkId) { if (previewWatermarkId) {
await this._stampPreview(pdfDoc, dossierTemplateId, previewWatermarkId); await this._stampPreview(pdfDoc, dossierTemplateId, previewWatermarkId);

View File

@ -1,77 +1,48 @@
import { Injectable } from '@angular/core'; import { computed, Injectable, Signal, signal } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { ViewMode, ViewModes } from '@red/domain'; import { ViewMode, ViewModes } from '@red/domain';
import { map } from 'rxjs/operators'; import { toObservable } from '@angular/core/rxjs-interop';
import { shareDistinctLast } from '@iqser/common-ui';
@Injectable() @Injectable()
export class ViewModeService { export class ViewModeService {
readonly viewMode$: Observable<ViewMode>; readonly viewMode$: Observable<ViewMode>;
readonly isRedacted$: Observable<boolean>; readonly viewMode: Signal<ViewMode>;
readonly isStandard$: Observable<boolean>; readonly isEarmarks = computed(() => this.viewMode() === ViewModes.TEXT_HIGHLIGHTS);
readonly isDelta$: Observable<boolean>; readonly isRedacted = computed(() => this.viewMode() === ViewModes.REDACTED);
readonly isStandard = computed(() => this.viewMode() === ViewModes.STANDARD);
readonly #viewMode$ = new BehaviorSubject<ViewMode>('STANDARD'); readonly isDelta = computed(() => this.viewMode() === ViewModes.DELTA);
readonly #viewMode = signal<ViewMode>(ViewModes.STANDARD);
constructor() { constructor() {
this.viewMode$ = this.#viewMode$.asObservable(); this.viewMode$ = toObservable(this.#viewMode);
this.isRedacted$ = this._is('REDACTED'); this.viewMode = this.#viewMode.asReadonly();
this.isStandard$ = this._is('STANDARD');
this.isDelta$ = this._is('DELTA');
} }
get onlyPagesWithAnnotations(): boolean { get onlyPagesWithAnnotations() {
return ([ViewModes.DELTA, ViewModes.TEXT_HIGHLIGHTS] as ViewMode[]).includes(this.viewMode); return ([ViewModes.DELTA, ViewModes.TEXT_HIGHLIGHTS] as ViewMode[]).includes(this.#viewMode());
} }
get viewMode() { is(viewMode: ViewMode) {
return this.#viewMode$.value; return this.viewMode() === viewMode;
} }
set viewMode(mode: ViewMode) { switchTo(viewMode: ViewMode) {
this.#viewMode$.next(mode); this.#viewMode.set(viewMode);
}
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';
} }
switchToStandard() { switchToStandard() {
this._switchTo('STANDARD'); this.switchTo(ViewModes.STANDARD);
} }
switchToDelta() { switchToDelta() {
this._switchTo('DELTA'); this.switchTo(ViewModes.DELTA);
} }
switchToRedacted() { switchToRedacted() {
this._switchTo('REDACTED'); this.switchTo(ViewModes.REDACTED);
} }
switchToHighlights() { switchToHighlights() {
this._switchTo('TEXT_HIGHLIGHTS'); this.switchTo(ViewModes.TEXT_HIGHLIGHTS);
}
private _switchTo(mode: ViewMode) {
this.#viewMode$.next(mode);
}
private _is(mode: ViewMode) {
return this.viewMode$.pipe(
map(value => value === mode),
shareDistinctLast(),
);
} }
} }