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: {
ANNOTATIONS: {
color: 'aqua',
enabled: false,
enabled: true,
level: NgxLoggerLevel.DEBUG,
},
FILTERS: {
enabled: false,
enabled: true,
},
PDF: {
enabled: false,
enabled: true,
},
FILE: {
enabled: false,

View File

@ -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<string, unknown> {
@ -52,7 +53,7 @@ export class AnnotationWrapper implements IListable, Record<string, unknown> {
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<string, unknown> {
textBefore?: string;
isChangeLogEntry = false;
changeLogType?: 'ADDED' | 'REMOVED' | 'CHANGED';
engines?: string[];
engines?: LogEntryEngine[];
hasBeenResized: boolean;
hasBeenRecategorized: boolean;
hasLegalBasisChanged: boolean;

View File

@ -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<AnnotationWrapper>) => listItem.item.id;

View File

@ -133,7 +133,7 @@ export class FileWorkloadComponent extends AutoUnsubscribe implements OnDestroy
private get _isEarmarks$(): Observable<boolean> {
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);
}

View File

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

View File

@ -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<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 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() {

View File

@ -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<AnnotationWrapper, Annotati
this.#subscription = this._viewModeService.viewMode$
.pipe(
switchMap(viewMode =>
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<AnnotationWrapper, Annotati
get #annotations$() {
return this.#redactionLog$.pipe(
tap(() => 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<AnnotationWrapper, Annotati
map(annotations =>
this._userPreferenceService.areDevFeaturesEnabled ? annotations : annotations.filter(a => !a.isFalsePositive),
),
tap(() => console.timeEnd('buildAnnotations')),
shareLast(),
);
}
@ -129,10 +129,11 @@ export class FileDataService extends EntitiesService<AnnotationWrapper, Annotati
async loadAnnotations(file: File) {
if (!file || file.isUnprocessed) {
this._logger.info('[ANNOTATIONS] File is null or unprocessed, skipping annotations loading');
return;
}
this._logger.info('[ANNOTATIONS] Load annotations');
this._logger.info('[ANNOTATIONS] Loading annotations...');
await this.#loadViewedPages(file);
await this.loadRedactionLog();
@ -155,9 +156,15 @@ export class FileDataService extends EntitiesService<AnnotationWrapper, Annotati
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);
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() {
@ -173,10 +180,12 @@ export class FileDataService extends EntitiesService<AnnotationWrapper, Annotati
async #loadViewedPages(file: File) {
if (!this._permissionsService.canMarkPagesAsViewed(file)) {
this._viewedPagesMapService.set(file.fileId, []);
this._logger.info('[VIEWED-PAGES] Cannot mark pages as viewed, skip loading viewed pages');
return;
}
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);
}
@ -196,76 +205,88 @@ export class FileDataService extends EntitiesService<AnnotationWrapper, Annotati
async #buildAnnotations(redactionLog: IRedactionLog, file: File) {
const entries = await this.#convertData(redactionLog, file);
const annotations = entries.map(entry =>
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<void> {
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);

View File

@ -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]);

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 { 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);

View File

@ -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<ViewMode>;
readonly isRedacted$: Observable<boolean>;
readonly isStandard$: Observable<boolean>;
readonly isDelta$: Observable<boolean>;
readonly #viewMode$ = new BehaviorSubject<ViewMode>('STANDARD');
readonly viewMode: Signal<ViewMode>;
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<ViewMode>(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);
}
}