820 lines
36 KiB
TypeScript
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');
|
|
}
|
|
}
|