red-ui/apps/red-ui/src/app/modules/file-preview/file-preview-screen.component.ts
2024-10-05 20:51:23 +03:00

820 lines
36 KiB
TypeScript

import { ActivatedRouteSnapshot, NavigationExtras, Router, RouterLink } from '@angular/router';
import { ChangeDetectorRef, Component, effect, NgZone, OnDestroy, OnInit, TemplateRef, untracked, ViewChild } from '@angular/core';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { ComponentCanDeactivate } from '@guards/can-deactivate.guard';
import {
CircleButtonComponent,
CircleButtonTypes,
ConfirmOption,
ConfirmOptions,
CustomError,
DisableStopPropagationDirective,
ErrorService,
getConfig,
IConfirmationDialogData,
IqserAllowDirective,
IqserDialog,
LoadingService,
Toaster,
} from '@iqser/common-ui';
import { copyLocalStorageFiltersValues, FilterService, NestedFilter, processFilters } from '@iqser/common-ui/lib/filtering';
import { AutoUnsubscribe, Bind, bool, List, OnAttach, OnDetach } from '@iqser/common-ui/lib/utils';
import { AnnotationWrapper } from '@models/file/annotation.wrapper';
import { ManualRedactionEntryTypes, ManualRedactionEntryWrapper } from '@models/file/manual-redaction-entry.wrapper';
import { File, ViewModes } from '@red/domain';
import { ConfigService } from '@services/config.service';
import { DossierTemplatesService } from '@services/dossier-templates/dossier-templates.service';
import { DossiersService } from '@services/dossiers/dossiers.service';
import { FilesMapService } from '@services/files/files-map.service';
import { FilesService } from '@services/files/files.service';
import { PermissionsService } from '@services/permissions.service';
import { ReanalysisService } from '@services/reanalysis.service';
import { Roles } from '@users/roles';
import { PreferencesKeys, UserPreferenceService } from '@users/user-preference.service';
import { NGXLogger } from 'ngx-logger';
import { combineLatest, first, firstValueFrom, Observable, of, pairwise } from 'rxjs';
import { catchError, filter, map, startWith, switchMap, tap } from 'rxjs/operators';
import { byId, byPage, handleFilterDelta, hasChanges } from '../../utils';
import { AnnotationDrawService } from '../pdf-viewer/services/annotation-draw.service';
import { REDAnnotationManager } from '../pdf-viewer/services/annotation-manager.service';
import { REDDocumentViewer } from '../pdf-viewer/services/document-viewer.service';
import { PageRotationService } from '../pdf-viewer/services/page-rotation.service';
import { PdfViewer } from '../pdf-viewer/services/pdf-viewer.service';
import { ReadableRedactionsService } from '../pdf-viewer/services/readable-redactions.service';
import { ViewerHeaderService } from '../pdf-viewer/services/viewer-header.service';
import { ROTATION_ACTION_BUTTONS, ViewerEvents } from '../pdf-viewer/utils/constants';
import { AddHintDialogComponent } from './dialogs/add-hint-dialog/add-hint-dialog.component';
import { AddAnnotationDialogComponent } from './dialogs/docu-mine/add-annotation-dialog/add-annotation-dialog.component';
import { RedactTextDialogComponent } from './dialogs/redact-text-dialog/redact-text-dialog.component';
import { filePreviewScreenProviders } from './file-preview-providers';
import { AnnotationProcessingService } from './services/annotation-processing.service';
import { AnnotationsListingService } from './services/annotations-listing.service';
import { FileDataService } from './services/file-data.service';
import { FilePreviewDialogService } from './services/file-preview-dialog.service';
import { FilePreviewStateService } from './services/file-preview-state.service';
import { ManualRedactionService } from './services/manual-redaction.service';
import { PdfProxyService } from './services/pdf-proxy.service';
import { SkippedService } from './services/skipped.service';
import { StampService } from './services/stamp.service';
import { ViewModeService } from './services/view-mode.service';
import { RedactTextData } from './utils/dialog-types';
import { MultiSelectService } from './services/multi-select.service';
import { NgIf } from '@angular/common';
import { ViewSwitchComponent } from './components/view-switch/view-switch.component';
import { ProcessingIndicatorComponent } from '@shared/components/processing-indicator/processing-indicator.component';
import { UserManagementComponent } from './components/user-management/user-management.component';
import { TranslateModule } from '@ngx-translate/core';
import { InitialsAvatarComponent } from '@common-ui/users';
import { FileActionsComponent } from '../shared-dossiers/components/file-actions/file-actions.component';
import { FilePreviewRightContainerComponent } from './components/right-container/file-preview-right-container.component';
import { TypeFilterComponent } from '@shared/components/type-filter/type-filter.component';
import { FileHeaderComponent } from './components/file-header/file-header.component';
import { StructuredComponentManagementComponent } from './components/structured-component-management/structured-component-management.component';
import { DocumentInfoService } from './services/document-info.service';
import { RectangleAnnotationDialog } from './dialogs/rectangle-annotation-dialog/rectangle-annotation-dialog.component';
@Component({
templateUrl: './file-preview-screen.component.html',
styleUrls: ['./file-preview-screen.component.scss'],
providers: filePreviewScreenProviders,
standalone: true,
imports: [
NgIf,
ViewSwitchComponent,
ProcessingIndicatorComponent,
UserManagementComponent,
TranslateModule,
InitialsAvatarComponent,
CircleButtonComponent,
IqserAllowDirective,
FileActionsComponent,
DisableStopPropagationDirective,
RouterLink,
FilePreviewRightContainerComponent,
TypeFilterComponent,
FileHeaderComponent,
StructuredComponentManagementComponent,
],
})
export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnInit, OnDestroy, OnAttach, OnDetach, ComponentCanDeactivate {
readonly circleButtonTypes = CircleButtonTypes;
readonly roles = Roles;
readonly fileId = this.state.fileId;
readonly dossierId = this.state.dossierId;
protected readonly isDocumine = getConfig().IS_DOCUMINE;
@ViewChild('annotationFilterTemplate', {
read: TemplateRef,
static: false,
})
private readonly _filterTemplate: TemplateRef<unknown>;
#loadAllAnnotationsEnabled = false;
constructor(
readonly pdf: PdfViewer,
readonly state: FilePreviewStateService,
readonly permissionsService: PermissionsService,
readonly userPreferenceService: UserPreferenceService,
readonly pdfProxyService: PdfProxyService,
readonly configService: ConfigService,
private readonly _listingService: AnnotationsListingService,
private readonly _router: Router,
private readonly _ngZone: NgZone,
private readonly _logger: NGXLogger,
private readonly _annotationManager: REDAnnotationManager,
private readonly _errorService: ErrorService,
private readonly _filterService: FilterService,
private readonly _loadingService: LoadingService,
private readonly _filesMapService: FilesMapService,
private readonly _dossiersService: DossiersService,
private readonly _skippedService: SkippedService,
private readonly _fileDataService: FileDataService,
private readonly _viewModeService: ViewModeService,
private readonly _documentViewer: REDDocumentViewer,
private readonly _changeRef: ChangeDetectorRef,
private readonly _dialogService: FilePreviewDialogService,
private readonly _iqserDialog: IqserDialog,
private readonly _pageRotationService: PageRotationService,
private readonly _viewerHeaderService: ViewerHeaderService,
private readonly _annotationDrawService: AnnotationDrawService,
private readonly _annotationProcessingService: AnnotationProcessingService,
private readonly _stampService: StampService,
private readonly _reanalysisService: ReanalysisService,
private readonly _toaster: Toaster,
private readonly _manualRedactionService: ManualRedactionService,
private readonly _filesService: FilesService,
private readonly _readableRedactionsService: ReadableRedactionsService,
private readonly _dossierTemplatesService: DossierTemplatesService,
private readonly _multiSelectService: MultiSelectService,
private readonly _documentInfoService: DocumentInfoService,
) {
super();
effect(() => {
const file = this.state.file();
this._fileDataService.loadAnnotations(file).then();
});
effect(() => {
const file = this.state.file();
if (file.analysisRequired && !file.excludedFromAutomaticAnalysis) {
this._reanalysisService.reanalyzeFilesForDossier([file], file.dossierId, { force: true }).then();
}
});
effect(
() => {
if (this._documentViewer.loaded()) {
this._pageRotationService.clearRotations();
this._viewerHeaderService.disable(ROTATION_ACTION_BUTTONS);
this.viewerReady().then();
}
},
{ allowSignalWrites: true },
);
effect(() => {
this.state.updateExcludedPagesStyle();
if (this._documentViewer.pageComplete()) {
this.#setExcludedPageStyles();
}
});
effect(() => {
this._viewModeService.viewMode();
this.#updateViewMode().then();
});
effect(() => {
this.state.updateExcludedPagesStyle();
this._viewModeService.viewMode();
if (_documentViewer.loaded()) {
this._logger.info('[PDF] Stamp pdf');
this._stampService.stampPDF().then();
}
});
effect(() => {
this._documentInfoService.shown();
this.#updateViewerPosition();
});
}
get changed() {
return this._pageRotationService.hasRotations();
}
get #earmarks$() {
const isEarmarksViewMode$ = this._viewModeService.viewMode$.pipe(filter(() => this._viewModeService.isEarmarks()));
const earmarks$ = isEarmarksViewMode$.pipe(
tap(() => this._loadingService.start()),
switchMap(() => this._fileDataService.loadEarmarks()),
tap(() => this.#updateViewMode().then(() => this._loadingService.stop())),
);
const currentPageIfEarmarksView$ = combineLatest([this.pdf.currentPage$, this._viewModeService.viewMode$]).pipe(
filter(() => this._viewModeService.isEarmarks()),
map(([page]) => page),
);
const currentPageEarmarks$ = combineLatest([currentPageIfEarmarksView$, earmarks$]).pipe(
map(([page, earmarks]) => earmarks.get(page)),
);
return currentPageEarmarks$.pipe(
map(earmarks => [earmarks, this._skippedService.hideSkipped(), this.state.dossierTemplateId] as const),
tap(args => this._annotationDrawService.draw(...args)),
);
}
deleteEarmarksOnViewChange$() {
const isChangingFromEarmarksViewMode$ = this._viewModeService.viewMode$.pipe(
pairwise(),
filter(([oldViewMode]) => oldViewMode === ViewModes.TEXT_HIGHLIGHTS),
);
return isChangingFromEarmarksViewMode$.pipe(
map(() => this._fileDataService.earmarks().get(this.pdf.currentPage()) ?? []),
map(earmarks => this.#deleteAnnotations(earmarks, [])),
);
}
async save() {
await this._pageRotationService.applyRotation();
this._viewerHeaderService.disable(ROTATION_ACTION_BUTTONS);
}
ngOnDetach() {
this._viewerHeaderService.resetCompareButtons();
this._viewerHeaderService.enableLoadAllAnnotations(); // Reset the button state (since the viewer is reused between files)
super.ngOnDetach();
this.pdf.instance.UI.hotkeys.off('esc');
this._changeRef.markForCheck();
}
ngOnDestroy() {
this.pdf.instance.UI.hotkeys.off('esc');
super.ngOnDestroy();
}
@Bind()
handleEscInsideViewer($event: KeyboardEvent) {
$event.preventDefault();
if (!!this._annotationManager.selected[0]) {
const doesHaveWrapper = this._fileDataService.find(this._annotationManager.selected[0]?.Id);
if (!doesHaveWrapper) {
this._annotationManager.delete(this._annotationManager.selected[0]?.Id);
} else {
this._annotationManager.deselect(this._annotationManager.selected[0]?.Id);
}
}
if (this._annotationManager.selected.length) {
this._annotationManager.deselectAll();
}
if (this._multiSelectService.active()) {
this._multiSelectService.deactivate();
}
}
async ngOnAttach(previousRoute: ActivatedRouteSnapshot) {
if (!this.state.file().canBeOpened) {
return this.#navigateToDossier();
}
this._viewModeService.switchToStandard();
await this.ngOnInit();
this._viewerHeaderService.updateElements();
const page = previousRoute.queryParams.page ?? '1';
await this.#updateQueryParamsPage(Number(page));
await this.viewerReady(page);
}
async ngOnInit(): Promise<void> {
this.#updateViewerPosition();
const file = this.state.file();
if (!file) {
return this.#handleDeletedFile();
}
this._loadingService.start();
this.userPreferenceService.saveLastOpenedFileForDossier(this.dossierId, this.fileId).then();
this.#subscribeToFileUpdates();
this.pdfProxyService.configureElements();
this.#restoreOldFilters();
this.pdf.instance.UI.hotkeys.on('esc', this.handleEscInsideViewer);
this._viewerHeaderService.resetLayers();
}
async openRectangleAnnotationDialog(manualRedactionEntryWrapper: ManualRedactionEntryWrapper) {
const file = this.state.file();
const data = { manualRedactionEntryWrapper, file, dossierId: this.dossierId };
const result = await this._iqserDialog.openDefault(RectangleAnnotationDialog, { data }).result();
if (!result) {
return;
}
const selectedAnnotations = this._annotationManager.selected;
if (selectedAnnotations.length > 0) {
this._annotationManager.delete([selectedAnnotations[0].Id]);
}
const add$ = this._manualRedactionService.addAnnotation([result.annotation], this.dossierId, this.fileId, {
dictionaryLabel: result.dictionary?.label,
});
const addAndReload$ = add$.pipe(switchMap(() => this._filesService.reload(this.dossierId, file)));
return firstValueFrom(addAndReload$.pipe(catchError(() => of(undefined))));
}
async viewerReady(pageNumber?: string) {
if (pageNumber) {
const file = this.state.file();
let page = parseInt(pageNumber, 10);
if (page < 1 || Number.isNaN(page)) {
page = 1;
await this.#updateQueryParamsPage(page);
} else if (page > file.numberOfPages) {
page = file.numberOfPages;
}
setTimeout(() => {
this.pdf.navigateTo(page);
}, 300);
}
this._loadingService.stop();
this._changeRef.markForCheck();
}
loadAnnotations$() {
const annotations$ = this._fileDataService.annotations$.pipe(
startWith([] as AnnotationWrapper[]),
pairwise(),
tap(annotations => this.#deleteAnnotations(...annotations)),
tap(() => this.#updateFiltersAfterAnnotationsChanged()),
tap(() => this.#updateViewMode()),
);
const currentPageIfNotHighlightsView$ = combineLatest([this.pdf.currentPage$, this._viewModeService.viewMode$]).pipe(
filter(([, viewMode]) => viewMode !== ViewModes.TEXT_HIGHLIGHTS),
map(([page]) => page),
);
const currentPageAnnotations$ = combineLatest([currentPageIfNotHighlightsView$, annotations$]).pipe(
map(([page, [oldAnnotations, newAnnotations]]) =>
this.#loadAllAnnotationsEnabled
? ([oldAnnotations, newAnnotations] as const)
: ([oldAnnotations.filter(byPage(page)), newAnnotations.filter(byPage(page))] as const),
),
);
return combineLatest([currentPageAnnotations$, this._documentViewer.loaded$]).pipe(
filter(([, loaded]) => loaded),
map(([annotations]) => annotations),
switchMap(async ([oldAnnotations, newAnnotations]) => {
await this.#drawChangedAnnotations(oldAnnotations, newAnnotations);
return newAnnotations;
}),
tap(newAnnotations => this.#highlightSelectedAnnotations(newAnnotations)),
);
}
async #updateViewMode(): Promise<void> {
const viewMode = untracked(this._viewModeService.viewMode);
this._logger.info(`[PDF] Update ${viewMode} view mode`);
const annotations = this._annotationManager.get(a => bool(a.getCustomData('redact-manager')));
const redactions = annotations.filter(a => bool(a.getCustomData('redaction')));
switch (viewMode) {
case ViewModes.STANDARD: {
const wrappers = this._fileDataService.annotations();
// TODO: const wrappers = untracked(this._fileDataService.annotations);
const ocrAnnotationIds = wrappers.filter(a => a.isOCR).map(a => a.id);
const standardEntries = annotations
.filter(a => !bool(a.getCustomData('changeLogRemoved')) && !this._annotationManager.isHidden(a.Id))
.filter(a => !ocrAnnotationIds.includes(a.Id));
const nonStandardEntries = annotations.filter(
a =>
bool(a.getCustomData('changeLogRemoved')) ||
this._annotationManager.isHidden(a.Id) ||
(untracked(this._skippedService.hideSkipped) && bool(a.getCustomData('skipped'))),
);
this._readableRedactionsService.setAnnotationsColor(standardEntries, 'annotationColor');
this._readableRedactionsService.setAnnotationsOpacity(standardEntries, true);
this._annotationManager.show(standardEntries);
this._annotationManager.hide(nonStandardEntries);
break;
}
case ViewModes.DELTA: {
const changeLogEntries = annotations.filter(a => bool(a.getCustomData('changeLog')));
const nonChangeLogEntries = annotations.filter(a => !bool(a.getCustomData('changeLog')));
this._readableRedactionsService.setAnnotationsColor(redactions, 'annotationColor');
this._readableRedactionsService.setAnnotationsOpacity(changeLogEntries, true);
this._annotationManager.show(changeLogEntries);
this._annotationManager.hide(nonChangeLogEntries);
break;
}
case ViewModes.REDACTED: {
const nonRedactionEntries = annotations.filter(
a => !bool(a.getCustomData('redaction')) || bool(a.getCustomData('changeLogRemoved')),
);
this._readableRedactionsService.setAnnotationsColor(redactions, 'redactionColor');
this._readableRedactionsService.setAnnotationsOpacity(redactions);
this._annotationManager.show(redactions);
this._annotationManager.hide(nonRedactionEntries);
break;
}
case ViewModes.TEXT_HIGHLIGHTS: {
this._annotationManager.hide(annotations);
}
}
this._logger.info('[PDF] Rebuild filters');
this.#rebuildFilters();
this._logger.info('[PDF] Update done');
}
#deleteAnnotations(oldAnnotations: AnnotationWrapper[], newAnnotations: AnnotationWrapper[]) {
const annotationsToDelete = oldAnnotations.filter(oldAnnotation => {
const newAnnotation = newAnnotations.find(byId(oldAnnotation.id));
return newAnnotation ? hasChanges(oldAnnotation, newAnnotation) : true;
});
this._logger.info('[ANNOTATIONS] To delete: ', annotationsToDelete);
this._annotationManager.delete(annotationsToDelete);
}
async #drawChangedAnnotations(oldAnnotations: AnnotationWrapper[], newAnnotations: AnnotationWrapper[]): Promise<void> {
const annotationsToDraw = this.#getAnnotationsToDraw(oldAnnotations, newAnnotations);
this._logger.info('[ANNOTATIONS] To draw: ', annotationsToDraw);
this._annotationManager.delete(annotationsToDraw);
await this.#cleanupAndRedrawAnnotations(annotationsToDraw);
}
async #openRedactTextDialog(manualRedactionEntryWrapper: ManualRedactionEntryWrapper) {
const file = this.state.file();
const data = this.#getRedactTextDialogData(manualRedactionEntryWrapper, file);
const result = await this.#getRedactTextDialog(data).result();
if (!result) {
return;
}
const hint = manualRedactionEntryWrapper.type === ManualRedactionEntryTypes.HINT;
const add$ = this._manualRedactionService.addAnnotation([result.redaction], this.dossierId, this.fileId, {
hint,
dictionaryLabel: result.dictionary?.label,
bulkLocal: result.bulkLocal,
});
const addAndReload$ = add$.pipe(
tap(() => this._documentViewer.clearSelection()),
switchMap(() => this._filesService.reload(this.dossierId, file)),
);
return firstValueFrom(addAndReload$.pipe(catchError(() => of(undefined))));
}
#getAnnotationsToDraw(oldAnnotations: AnnotationWrapper[], newAnnotations: AnnotationWrapper[]) {
const currentPage = this.pdf.currentPage();
const currentPageAnnotations = this._annotationManager.get(a => a.getPageNumber() === currentPage);
const existingAnnotations = [];
for (const annotation of currentPageAnnotations) {
const oldAnnotation = oldAnnotations.find(byId(annotation.Id));
if (oldAnnotation) {
existingAnnotations.push(oldAnnotation);
continue;
}
const newAnnotation = newAnnotations.find(byId(annotation.Id));
if (newAnnotation) {
existingAnnotations.push(newAnnotation);
}
}
if (existingAnnotations.length > 0) {
return this.#findAnnotationsToDraw(newAnnotations, oldAnnotations, existingAnnotations);
}
return newAnnotations;
}
#rebuildFilters() {
const startTime = new Date().getTime();
const annotationFilters = this._annotationProcessingService.getAnnotationFilter();
const primaryFilters = this._filterService.getGroup('primaryFilters')?.filters;
this._filterService.addFilterGroup({
slug: 'primaryFilters',
filterTemplate: this._filterTemplate,
filters: processFilters(primaryFilters, annotationFilters),
});
const secondaryFilters = this._filterService.getGroup('secondaryFilters')?.filters;
const secondaryAnnotationFilters = this._annotationProcessingService.secondaryAnnotationFilters;
this._filterService.addFilterGroup({
slug: 'secondaryFilters',
filterTemplate: this._filterTemplate,
filters: processFilters(secondaryFilters, secondaryAnnotationFilters),
});
this._logger.info(`[FILTERS] Rebuild time: ${new Date().getTime() - startTime} ms`);
}
async #updateQueryParamsPage(page: number): Promise<void> {
const extras: NavigationExtras = {
queryParams: { page },
queryParamsHandling: 'merge',
replaceUrl: true,
};
await this._router.navigate([], extras);
this._changeRef.markForCheck();
}
#findAnnotationsToDraw(
newAnnotations: AnnotationWrapper[],
oldAnnotations: AnnotationWrapper[],
existingAnnotations: AnnotationWrapper[],
) {
function selectToDrawIfDoesNotExist(newAnnotation: AnnotationWrapper) {
return !existingAnnotations.some(byId(newAnnotation.id));
}
return newAnnotations.filter(newAnnotation => {
const oldAnnotation = oldAnnotations.find(byId(newAnnotation.id));
if (!oldAnnotation || !hasChanges(oldAnnotation, newAnnotation)) {
return selectToDrawIfDoesNotExist(newAnnotation);
}
return true;
});
}
#setExcludedPageStyles() {
const file = this._filesMapService.get(this.dossierId, this.fileId);
const iframeDoc = this.pdf.instance.UI.iframeWindow.document;
const currentPage = this.pdf.currentPage();
const elementId = `pageWidgetContainer${currentPage}`;
const pageContainer = iframeDoc.getElementById(elementId);
if (pageContainer) {
if (file.excludedPages.includes(currentPage)) {
pageContainer?.classList?.add('excluded-page');
} else {
pageContainer?.classList?.remove('excluded-page');
}
}
}
#annotationsExceedingThresholdWarning(annotations: AnnotationWrapper[]): Observable<readonly [boolean, AnnotationWrapper[]]> {
const data = {
question: _('load-all-annotations-threshold-exceeded'),
checkboxes: [
{
label: _('load-all-annotations-threshold-exceeded-checkbox'),
value: false,
},
],
checkboxesValidation: false,
translateParams: {
threshold: this.configService.values.ANNOTATIONS_THRESHOLD,
},
} as IConfirmationDialogData;
const ref = this._dialogService.openDialog('confirm', data);
return ref.afterClosed().pipe(
switchMap(async (result: ConfirmOption) => {
const doNotShowWarningAgain = result === ConfirmOptions.CONFIRM_WITH_ACTION;
if (doNotShowWarningAgain) {
await this.userPreferenceService.save(PreferencesKeys.loadAllAnnotationsWarning, 'true');
await this.userPreferenceService.reload();
}
const validOptions: number[] = [ConfirmOptions.CONFIRM, ConfirmOptions.CONFIRM_WITH_ACTION];
const shouldLoad = validOptions.includes(result);
return [shouldLoad, annotations] as const;
}),
);
}
#subscribeToFileUpdates(): void {
this.addActiveScreenSubscription = this.loadAnnotations$().subscribe();
this.addActiveScreenSubscription = this._viewerHeaderService.layersUpdated.subscribe();
this.addActiveScreenSubscription = this._dossiersService
.getEntityDeleted$(this.dossierId)
.pipe(tap(() => this.#handleDeletedDossier()))
.subscribe();
this.addActiveScreenSubscription = this._filesMapService
.watchDeleted$(this.fileId)
.pipe(tap(() => this.#handleDeletedFile()))
.subscribe();
this.addActiveScreenSubscription = this.#earmarks$.subscribe();
this.addActiveScreenSubscription = this.deleteEarmarksOnViewChange$().subscribe();
this.addActiveScreenSubscription = this.state.blob$
.pipe(
tap(() => this._errorService.clear()),
switchMap(blob => this.pdf.loadDocument(blob, this.state.file(), () => this.state.reloadBlob())),
)
.subscribe();
this.addActiveScreenSubscription = this.pdfProxyService.manualAnnotationRequested$.subscribe($event => {
this.openRectangleAnnotationDialog($event).then();
});
this.addActiveScreenSubscription = this.pdfProxyService.redactTextRequested$.subscribe($event => {
this.#openRedactTextDialog($event).then();
});
this.addActiveScreenSubscription = this.pdfProxyService.pageChanged$.subscribe(page =>
this._ngZone.run(() => this.#updateQueryParamsPage(page)),
);
this.addActiveScreenSubscription = this._viewerHeaderService.toggleLoadAnnotations$.subscribe();
this.addActiveScreenSubscription = this.pdfProxyService.annotationSelected$.subscribe();
this.addActiveScreenSubscription = this._viewerHeaderService.events$
.pipe(
filter(event => event.type === ViewerEvents.LOAD_ALL_ANNOTATIONS),
switchMap(() => {
// TODO: this switchMap is ugly, to be refactored
const annotations = untracked(this._fileDataService.annotations);
const showWarning = !this.userPreferenceService.getBool(PreferencesKeys.loadAllAnnotationsWarning);
const annotationsExceedThreshold = annotations.length >= this.configService.values.ANNOTATIONS_THRESHOLD;
if (annotationsExceedThreshold && showWarning) {
return this.#annotationsExceedingThresholdWarning(annotations);
}
return of([true, annotations] as const);
}),
filter(([confirmed]) => confirmed),
map(([, annotations]) => {
this.#loadAllAnnotationsEnabled = true;
this.#drawChangedAnnotations([], annotations).then(() => {
this._toaster.success(_('load-all-annotations-success'));
this._viewerHeaderService.disableLoadAllAnnotations();
});
}),
)
.subscribe();
this.addActiveScreenSubscription = this._readableRedactionsService.active$
.pipe(switchMap(() => this.#updateViewMode()))
.subscribe();
this.addActiveScreenSubscription = combineLatest([this._viewModeService.viewMode$, this.state.file$, this._documentViewer.loaded$])
.pipe(
map(([viewMode, file]) => {
if (viewMode === 'REDACTED' && !this._readableRedactionsService.active) {
this._readableRedactionsService.setCustomDrawHandler();
} else {
this._readableRedactionsService.restoreDraw();
}
return ['STANDARD', 'TEXT_HIGHLIGHTS'].includes(viewMode) && this.permissionsService.canRotatePage(file);
}),
tap(canRotate =>
canRotate ? this._viewerHeaderService.enableRotationButtons() : this._viewerHeaderService.disableRotationButtons(),
),
)
.subscribe();
}
#handleDeletedDossier(): void {
const error = new CustomError(
_('error.deleted-entity.file-dossier.label'),
_('error.deleted-entity.file-dossier.action'),
'iqser:expand',
);
this._errorService.set(error);
}
#handleDeletedFile(): void {
const error = new CustomError(
_('error.deleted-entity.file.label'),
_('error.deleted-entity.file.action'),
'iqser:expand',
null,
() => this.#navigateToDossier(),
);
this._errorService.set(error);
}
async #cleanupAndRedrawAnnotations(newAnnotations: List<AnnotationWrapper>): Promise<void> {
if (!newAnnotations.length) {
return undefined;
}
await this._annotationDrawService.draw(newAnnotations, this._skippedService.hideSkipped(), this.state.dossierTemplateId);
}
#updateFiltersAfterAnnotationsChanged(): void {
const currentFilters = this._filterService.getGroup('primaryFilters')?.filters || [];
this.#rebuildFilters();
if (currentFilters) {
setTimeout(() => {
this.#handleDeltaAnnotationFilters(currentFilters);
}, 100);
}
}
#handleDeltaAnnotationFilters(currentFilters: NestedFilter[]) {
const primaryFilterGroup = this._filterService.getGroup('primaryFilters');
const primaryFilters = primaryFilterGroup.filters;
const secondaryFilters = this._filterService.getGroup('secondaryFilters').filters;
const hasAnyFilterSet = [...primaryFilters, ...secondaryFilters].some(f => f.checked || f.indeterminate);
const autoExpandFilters = this.userPreferenceService.getAutoExpandFiltersOnActions();
const isReviewer = this.permissionsService.isFileAssignee(this.state.file());
const shouldExpandFilters = hasAnyFilterSet && autoExpandFilters && isReviewer;
if (!shouldExpandFilters) {
return;
}
const newPageSpecificFilters = this._annotationProcessingService.getAnnotationFilter();
handleFilterDelta(currentFilters, newPageSpecificFilters, primaryFilters);
this._filterService.addFilterGroup({
...primaryFilterGroup,
filters: primaryFilters,
});
}
#navigateToDossier() {
this._logger.info('Navigating to ', this.state.dossier().dossierName);
return this._router.navigate([this.state.dossier().routerLink]);
}
#highlightSelectedAnnotations(newAnnotations: AnnotationWrapper[]) {
const annotationsIds = newAnnotations.map(annotation => annotation.id);
const selected = this._listingService.selected.filter(a => annotationsIds.includes(a.id));
const annotations = this._annotationManager.get(selected);
this._annotationManager.select(annotations);
}
#restoreOldFilters() {
combineLatest([
this._filterService.getGroup$('primaryFilters').pipe(first(filterGroup => !!filterGroup?.filters.length)),
this._filterService.getGroup$('secondaryFilters').pipe(first(secondaryFilters => !!secondaryFilters?.filters.length)),
]).subscribe(([primaryFilters, secondaryFilters]) => {
const localStorageFiltersString = localStorage.getItem('workload-filters') ?? '{}';
const localStorageFilters = JSON.parse(localStorageFiltersString)[this.fileId];
if (localStorageFilters) {
copyLocalStorageFiltersValues(primaryFilters.filters, localStorageFilters.primaryFilters);
copyLocalStorageFiltersValues(secondaryFilters.filters, localStorageFilters.secondaryFilters);
}
});
}
#getRedactTextDialog(data: RedactTextData) {
if (this.isDocumine) {
return this._iqserDialog.openDefault(AddAnnotationDialogComponent, { data });
}
const hint = data.manualRedactionEntryWrapper.type === ManualRedactionEntryTypes.HINT;
if (hint) {
return this._iqserDialog.openDefault(AddHintDialogComponent, { data });
}
return this._iqserDialog.openDefault(RedactTextDialogComponent, { data });
}
#getRedactTextDialogData(manualRedactionEntryWrapper: ManualRedactionEntryWrapper, file: File): RedactTextData {
const dossierTemplate = this._dossierTemplatesService.find(this.state.dossierTemplateId);
const isApprover = this.permissionsService.isApprover(this.state.dossier());
const applyDictionaryUpdatesToAllDossiersByDefault = dossierTemplate.applyDictionaryUpdatesToAllDossiersByDefault;
return {
manualRedactionEntryWrapper,
dossierId: this.dossierId,
file,
applyToAllDossiers: isApprover ? applyDictionaryUpdatesToAllDossiersByDefault : false,
isApprover,
isPageExcluded: this.state.file().isPageExcluded(this.pdf.currentPage()),
};
}
#updateViewerPosition() {
if (this.isDocumine) {
if (this._documentInfoService.shown()) {
document.getElementById('viewer')?.classList?.add('document-info');
} else {
document.getElementById('viewer')?.classList?.remove('document-info');
}
document.getElementById('viewer')?.classList?.add('documine-viewer');
return;
}
document.getElementById('viewer')?.classList?.add('redaction-viewer');
}
}