784 lines
33 KiB
TypeScript
784 lines
33 KiB
TypeScript
import {
|
|
AfterViewInit,
|
|
ChangeDetectorRef,
|
|
Component,
|
|
ElementRef,
|
|
HostListener,
|
|
NgZone,
|
|
OnDestroy,
|
|
OnInit,
|
|
TemplateRef,
|
|
ViewChild,
|
|
} from '@angular/core';
|
|
import { ActivatedRoute, ActivatedRouteSnapshot, NavigationExtras, Router } from '@angular/router';
|
|
import {
|
|
AutoUnsubscribe,
|
|
bool,
|
|
CircleButtonTypes,
|
|
ConfirmationDialogInput,
|
|
ConfirmOptions,
|
|
CustomError,
|
|
Debounce,
|
|
ErrorService,
|
|
FilterService,
|
|
HelpModeService,
|
|
List,
|
|
LoadingService,
|
|
NestedFilter,
|
|
OnAttach,
|
|
OnDetach,
|
|
processFilters,
|
|
Toaster,
|
|
} from '@iqser/common-ui';
|
|
import { MatDialogState } from '@angular/material/dialog';
|
|
import { ManualRedactionEntryWrapper } from '@models/file/manual-redaction-entry.wrapper';
|
|
import { AnnotationWrapper } from '@models/file/annotation.wrapper';
|
|
import { AnnotationDrawService } from '../pdf-viewer/services/annotation-draw.service';
|
|
import { AnnotationProcessingService } from './services/annotation-processing.service';
|
|
import { Dictionary, File, ViewModes } from '@red/domain';
|
|
import { PermissionsService } from '@services/permissions.service';
|
|
import { combineLatest, firstValueFrom, from, Observable, of, pairwise } from 'rxjs';
|
|
import { PreferencesKeys, UserPreferenceService } from '@users/user-preference.service';
|
|
import { byId, byPage, download, handleFilterDelta, hasChanges } from '../../utils';
|
|
import { FilesService } from '@services/files/files.service';
|
|
import { FileManagementService } from '@services/files/file-management.service';
|
|
import { catchError, filter, map, startWith, switchMap, tap, withLatestFrom } from 'rxjs/operators';
|
|
import { FilesMapService } from '@services/files/files-map.service';
|
|
import { ViewModeService } from './services/view-mode.service';
|
|
import { ReanalysisService } from '@services/reanalysis.service';
|
|
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
|
|
import { FilePreviewStateService } from './services/file-preview-state.service';
|
|
import { filePreviewScreenProviders } from './file-preview-providers';
|
|
import { ManualRedactionService } from './services/manual-redaction.service';
|
|
import { DossiersService } from '@services/dossiers/dossiers.service';
|
|
import { PageRotationService } from '../pdf-viewer/services/page-rotation.service';
|
|
import { ComponentCanDeactivate } from '@guards/can-deactivate.guard';
|
|
import { FilePreviewDialogService } from './services/file-preview-dialog.service';
|
|
import { FileDataService } from './services/file-data.service';
|
|
import { ALL_HOTKEYS, TextPopups } from './utils/constants';
|
|
import { NGXLogger } from 'ngx-logger';
|
|
import { StampService } from './services/stamp.service';
|
|
import { PdfViewer } from '../pdf-viewer/services/pdf-viewer.service';
|
|
import { REDAnnotationManager } from '../pdf-viewer/services/annotation-manager.service';
|
|
import { ViewerHeaderService } from '../pdf-viewer/services/viewer-header.service';
|
|
import { ROTATION_ACTION_BUTTONS, ViewerEvents } from '../pdf-viewer/utils/constants';
|
|
import { SkippedService } from './services/skipped.service';
|
|
import { REDDocumentViewer } from '../pdf-viewer/services/document-viewer.service';
|
|
import { AnnotationsListingService } from './services/annotations-listing.service';
|
|
import { PdfProxyService } from './services/pdf-proxy.service';
|
|
import { ConfigService } from '@services/config.service';
|
|
import { ReadableRedactionsService } from '../pdf-viewer/services/readable-redactions.service';
|
|
import { ROLES } from '@users/roles';
|
|
import { SuggestionsService } from './services/suggestions.service';
|
|
|
|
const textActions = [TextPopups.ADD_DICTIONARY, TextPopups.ADD_FALSE_POSITIVE];
|
|
|
|
@Component({
|
|
templateUrl: './file-preview-screen.component.html',
|
|
styleUrls: ['./file-preview-screen.component.scss'],
|
|
providers: filePreviewScreenProviders,
|
|
})
|
|
export class FilePreviewScreenComponent
|
|
extends AutoUnsubscribe
|
|
implements AfterViewInit, OnInit, OnDestroy, OnAttach, OnDetach, ComponentCanDeactivate
|
|
{
|
|
readonly circleButtonTypes = CircleButtonTypes;
|
|
readonly roles = ROLES;
|
|
fullScreen = false;
|
|
readonly fileId = this.state.fileId;
|
|
readonly dossierId = this.state.dossierId;
|
|
readonly file$ = this.state.file$.pipe(tap(file => this._fileDataService.loadAnnotations(file)));
|
|
width: number;
|
|
@ViewChild('annotationFilterTemplate', {
|
|
read: TemplateRef,
|
|
static: false,
|
|
})
|
|
private readonly _filterTemplate: TemplateRef<unknown>;
|
|
@ViewChild('actionsWrapper', { static: false }) private _actionsWrapper: ElementRef;
|
|
|
|
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 _activatedRoute: ActivatedRoute,
|
|
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 _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 _fileManagementService: FileManagementService,
|
|
private readonly _readableRedactionsService: ReadableRedactionsService,
|
|
private readonly _helpModeService: HelpModeService,
|
|
private readonly _suggestionsService: SuggestionsService,
|
|
) {
|
|
super();
|
|
document.documentElement.addEventListener('fullscreenchange', () => {
|
|
if (!document.fullscreenElement) {
|
|
this.fullScreen = false;
|
|
}
|
|
});
|
|
|
|
this.pdf.instance.UI.hotkeys.on('command+f, ctrl+f', e => {
|
|
e.preventDefault();
|
|
this.pdf.focusSearch();
|
|
this.pdf.activateSearch();
|
|
});
|
|
}
|
|
|
|
get changed() {
|
|
return this._pageRotationService.hasRotations;
|
|
}
|
|
|
|
get #textSelected$() {
|
|
const textSelected$ = combineLatest([
|
|
this._documentViewer.textSelected$,
|
|
this.pdfProxyService.canPerformAnnotationActions$,
|
|
this.state.file$,
|
|
]);
|
|
|
|
return textSelected$.pipe(
|
|
tap(([selectedText, canPerformActions, file]) => {
|
|
const isCurrentPageExcluded = file.isPageExcluded(this.pdf.currentPage);
|
|
|
|
if ((selectedText.length > 2 || this._isJapaneseString(selectedText)) && canPerformActions && !isCurrentPageExcluded) {
|
|
this.pdf.enable(textActions);
|
|
} else {
|
|
this.pdf.disable(textActions);
|
|
}
|
|
}),
|
|
);
|
|
}
|
|
|
|
get #earmarks$() {
|
|
const isEarmarksViewMode$ = this._viewModeService.viewMode$.pipe(filter(() => this._viewModeService.isEarmarks));
|
|
|
|
const earmarks$ = isEarmarksViewMode$.pipe(
|
|
tap(() => this._loadingService.start()),
|
|
switchMap(() => this._fileDataService.loadEarmarks()),
|
|
switchMap(() => this._fileDataService.earmarks$),
|
|
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),
|
|
switchMap(args => this._annotationDrawService.draw(...args)),
|
|
);
|
|
}
|
|
|
|
deleteEarmarksOnViewChange$() {
|
|
const isChangingFromEarmarksViewMode$ = this._viewModeService.viewMode$.pipe(
|
|
pairwise(),
|
|
filter(([oldViewMode]) => oldViewMode === ViewModes.TEXT_HIGHLIGHTS),
|
|
);
|
|
|
|
return isChangingFromEarmarksViewMode$.pipe(
|
|
withLatestFrom(this._fileDataService.earmarks$),
|
|
map(([, earmarks]) => this.deleteAnnotations(earmarks.get(this.pdf.currentPage) ?? [], [])),
|
|
);
|
|
}
|
|
|
|
async save() {
|
|
await this._pageRotationService.applyRotation();
|
|
this._viewerHeaderService.disable(ROTATION_ACTION_BUTTONS);
|
|
}
|
|
|
|
async updateViewMode(): Promise<void> {
|
|
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) {
|
|
case ViewModes.STANDARD: {
|
|
this._readableRedactionsService.setAnnotationsColor(redactions, 'annotationColor');
|
|
const wrappers = await 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),
|
|
);
|
|
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.setPreviewAnnotationsOpacity(redactions);
|
|
this._readableRedactionsService.setPreviewAnnotationsColor(redactions);
|
|
this._annotationManager.show(redactions);
|
|
this._annotationManager.hide(nonRedactionEntries);
|
|
this._suggestionsService.hideSuggestionsInPreview(redactions);
|
|
|
|
break;
|
|
}
|
|
case ViewModes.TEXT_HIGHLIGHTS: {
|
|
this._annotationManager.hide(annotations);
|
|
}
|
|
}
|
|
|
|
await this._stampService.stampPDF();
|
|
this.#rebuildFilters();
|
|
}
|
|
|
|
ngOnDetach() {
|
|
this._viewerHeaderService.resetCompareButtons();
|
|
this._viewerHeaderService.enableLoadAllAnnotations(); // Reset the button state (since the viewer is reused between files)
|
|
super.ngOnDetach();
|
|
this._changeRef.markForCheck();
|
|
}
|
|
|
|
async ngOnAttach(previousRoute: ActivatedRouteSnapshot) {
|
|
if (!this.state.file.canBeOpened) {
|
|
return this._navigateToDossier();
|
|
}
|
|
|
|
this._viewModeService.switchToStandard();
|
|
|
|
await this.ngOnInit();
|
|
await this._fileDataService.loadRedactionLog();
|
|
this._viewerHeaderService.updateElements();
|
|
const page = previousRoute.queryParams.page ?? '1';
|
|
await this.#updateQueryParamsPage(Number(page));
|
|
await this.viewerReady(page);
|
|
}
|
|
|
|
async ngOnInit(): Promise<void> {
|
|
const file = this.state.file;
|
|
|
|
if (!file) {
|
|
return this._handleDeletedFile();
|
|
}
|
|
|
|
this._loadingService.start();
|
|
await this.userPreferenceService.saveLastOpenedFileForDossier(this.dossierId, this.fileId);
|
|
this._subscribeToFileUpdates();
|
|
|
|
if (file?.analysisRequired && !file.excludedFromAutomaticAnalysis) {
|
|
const reanalyzeFiles = this._reanalysisService.reanalyzeFilesForDossier([file], this.dossierId, { force: true });
|
|
await firstValueFrom(reanalyzeFiles);
|
|
}
|
|
|
|
this.pdfProxyService.configureElements();
|
|
}
|
|
|
|
ngAfterViewInit() {
|
|
const _observer = new ResizeObserver((entries: ResizeObserverEntry[]) => {
|
|
this._updateItemWidth(entries[0]);
|
|
});
|
|
_observer.observe(this._actionsWrapper.nativeElement);
|
|
}
|
|
|
|
openManualAnnotationDialog(manualRedactionEntryWrapper: ManualRedactionEntryWrapper) {
|
|
return this._ngZone.run(() => {
|
|
const file = this.state.file;
|
|
|
|
this.state.dialogRef = this._dialogService.openDialog(
|
|
'manualAnnotation',
|
|
null,
|
|
{ manualRedactionEntryWrapper, dossierId: this.dossierId, file },
|
|
(result: { annotations: ManualRedactionEntryWrapper[]; dictionary?: Dictionary }) => {
|
|
const selectedAnnotations = this._annotationManager.selected;
|
|
if (selectedAnnotations.length > 0) {
|
|
this._annotationManager.delete([selectedAnnotations[0].Id]);
|
|
}
|
|
|
|
const add$ = this._manualRedactionService.addAnnotation(
|
|
result.annotations.map(w => w.manualRedactionEntry).filter(e => e.positions[0].page <= file.numberOfPages),
|
|
this.dossierId,
|
|
this.fileId,
|
|
result.dictionary?.label,
|
|
);
|
|
|
|
const addAndReload$ = add$.pipe(switchMap(() => this._filesService.reload(this.dossierId, file)));
|
|
return firstValueFrom(addAndReload$.pipe(catchError(() => of(undefined))));
|
|
},
|
|
);
|
|
});
|
|
}
|
|
|
|
toggleFullScreen() {
|
|
this.fullScreen = !this.fullScreen;
|
|
if (this.fullScreen) {
|
|
this._openFullScreen();
|
|
} else {
|
|
this.closeFullScreen();
|
|
}
|
|
}
|
|
|
|
@HostListener('window:keyup', ['$event'])
|
|
handleKeyEvent($event: KeyboardEvent) {
|
|
if (this._router.url.indexOf('/file/') < 0) {
|
|
return;
|
|
}
|
|
|
|
if (!ALL_HOTKEYS.includes($event.key) || this.state.dialogRef?.getState() === MatDialogState.OPEN) {
|
|
return;
|
|
}
|
|
|
|
if (['Escape'].includes($event.key)) {
|
|
this.fullScreen = false;
|
|
this.closeFullScreen();
|
|
this.pdf.deactivateSearch();
|
|
this._changeRef.markForCheck();
|
|
}
|
|
|
|
if (['f', 'F'].includes($event.key)) {
|
|
// if you type in an input, don't toggle full-screen
|
|
if ($event.target instanceof HTMLInputElement || $event.target instanceof HTMLTextAreaElement) {
|
|
return;
|
|
}
|
|
this.toggleFullScreen();
|
|
return;
|
|
}
|
|
|
|
if (['h', 'H'].includes($event.key)) {
|
|
if ($event.target instanceof HTMLInputElement || $event.target instanceof HTMLTextAreaElement) {
|
|
return;
|
|
}
|
|
this._ngZone.run(() => {
|
|
window.focus();
|
|
this._helpModeService.activateHelpMode(false);
|
|
});
|
|
return;
|
|
}
|
|
}
|
|
|
|
async viewerReady(pageNumber: string = this._activatedRoute.snapshot.queryParams.page) {
|
|
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();
|
|
}
|
|
|
|
closeFullScreen() {
|
|
if (!!document.fullscreenElement && document.exitFullscreen) {
|
|
document.exitFullscreen().then();
|
|
}
|
|
}
|
|
|
|
async downloadOriginalFile({ cacheIdentifier, dossierId, fileId, filename }: File) {
|
|
const originalFile = this._fileManagementService.downloadOriginal(dossierId, fileId, 'response', cacheIdentifier);
|
|
download(await firstValueFrom(originalFile), filename);
|
|
}
|
|
|
|
openRSSView(file: File) {
|
|
this._dialogService.openDialog('rss', null, { file });
|
|
}
|
|
|
|
loadAnnotations() {
|
|
const documentLoaded$ = this._documentViewer.loaded$.pipe(
|
|
tap(loaded => {
|
|
if (!loaded) {
|
|
return;
|
|
}
|
|
this._pageRotationService.clearRotations();
|
|
this._viewerHeaderService.disable(ROTATION_ACTION_BUTTONS);
|
|
return this.viewerReady();
|
|
}),
|
|
);
|
|
|
|
const annotations$ = this._fileDataService.annotations$.pipe(
|
|
startWith([] as AnnotationWrapper[]),
|
|
pairwise(),
|
|
tap(annotations => this.deleteAnnotations(...annotations)),
|
|
);
|
|
|
|
const currentPage$ = this.pdf.currentPage$.pipe(tap(() => this._annotationManager.showHidden()));
|
|
|
|
const currentPageIfNotHighlightsView$ = combineLatest([currentPage$, this._viewModeService.viewMode$]).pipe(
|
|
filter(([, viewMode]) => viewMode !== ViewModes.TEXT_HIGHLIGHTS),
|
|
map(([page]) => page),
|
|
);
|
|
|
|
const currentPageAnnotations$ = combineLatest([currentPageIfNotHighlightsView$, annotations$]).pipe(
|
|
map(
|
|
([page, [oldAnnotations, newAnnotations]]) =>
|
|
[oldAnnotations.filter(byPage(page)), newAnnotations.filter(byPage(page))] as const,
|
|
),
|
|
);
|
|
|
|
return combineLatest([currentPageAnnotations$, documentLoaded$]).pipe(
|
|
filter(([, loaded]) => loaded),
|
|
map(([annotations]) => annotations),
|
|
switchMap(annotations => this.drawChangedAnnotations(...annotations)),
|
|
tap(([, newAnnotations]) => this.#highlightSelectedAnnotations(newAnnotations)),
|
|
tap(() => this.updateViewMode()),
|
|
);
|
|
}
|
|
|
|
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[]) {
|
|
const annotationsToDraw = this.#getAnnotationsToDraw(oldAnnotations, newAnnotations);
|
|
this._logger.info('[ANNOTATIONS] To draw: ', annotationsToDraw);
|
|
this._annotationManager.delete(annotationsToDraw);
|
|
await this._cleanupAndRedrawAnnotations(annotationsToDraw);
|
|
return [oldAnnotations, newAnnotations];
|
|
}
|
|
|
|
@Debounce(30)
|
|
private _updateItemWidth(entry: ResizeObserverEntry): void {
|
|
this.width = entry.contentRect.width;
|
|
this._changeRef.detectChanges();
|
|
}
|
|
|
|
#getAnnotationsToDraw(oldAnnotations: AnnotationWrapper[], newAnnotations: AnnotationWrapper[]) {
|
|
const currentPage = this.pdf.currentPage;
|
|
const currentPageAnnotations = this._annotationManager.get(a => a.getPageNumber() === currentPage);
|
|
const existingAnnotations = currentPageAnnotations
|
|
.map(a => oldAnnotations.find(byId(a.Id)) || newAnnotations.find(byId(a.Id)))
|
|
.filter(a => !!a);
|
|
|
|
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);
|
|
}
|
|
|
|
if (this.userPreferenceService.areDevFeaturesEnabled) {
|
|
this.#logDiff(oldAnnotation, newAnnotation);
|
|
}
|
|
|
|
return true;
|
|
});
|
|
}
|
|
|
|
#logDiff(oldAnnotation: AnnotationWrapper, newAnnotation: AnnotationWrapper) {
|
|
import('@iqser/common-ui').then(commonUi => {
|
|
this._logger.info('[ANNOTATIONS] Changed annotation: ', {
|
|
value: oldAnnotation.value,
|
|
before: commonUi.deepDiffObj(newAnnotation, oldAnnotation),
|
|
after: commonUi.deepDiffObj(oldAnnotation, newAnnotation),
|
|
});
|
|
});
|
|
}
|
|
|
|
private _setExcludedPageStyles() {
|
|
const file = this._filesMapService.get(this.dossierId, this.fileId);
|
|
setTimeout(() => {
|
|
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');
|
|
}
|
|
}
|
|
}, 100);
|
|
}
|
|
|
|
private _subscribeToFileUpdates(): void {
|
|
this.addActiveScreenSubscription = this.loadAnnotations().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 = combineLatest([this._viewModeService.viewMode$, this.state.file$])
|
|
.pipe(
|
|
filter(([viewMode, file]) => viewMode === ViewModes.TEXT_HIGHLIGHTS && !file.hasHighlights),
|
|
tap(() => this._viewModeService.switchToStandard()),
|
|
)
|
|
.subscribe();
|
|
|
|
this.addActiveScreenSubscription = this._documentViewer.pageComplete$.subscribe(() => {
|
|
this._setExcludedPageStyles();
|
|
});
|
|
|
|
this.addActiveScreenSubscription = this._documentViewer.keyUp$.subscribe($event => {
|
|
this.handleKeyEvent($event);
|
|
});
|
|
|
|
this.addActiveScreenSubscription = this.#textSelected$.subscribe();
|
|
|
|
this.addActiveScreenSubscription = this.#earmarks$.subscribe();
|
|
this.addActiveScreenSubscription = this.deleteEarmarksOnViewChange$().subscribe();
|
|
|
|
this.addActiveScreenSubscription = this.state.dossierFileChange$.subscribe();
|
|
|
|
this.addActiveScreenSubscription = this.state.blob$
|
|
.pipe(
|
|
switchMap(blob => from(this._documentViewer.lock()).pipe(map(() => blob))),
|
|
tap(() => this._errorService.clear()),
|
|
tap(blob => this.pdf.loadDocument(blob, this.state.file, () => this.state.reloadBlob())),
|
|
)
|
|
.subscribe();
|
|
|
|
this.addActiveScreenSubscription = this.pdfProxyService.manualAnnotationRequested$.subscribe($event => {
|
|
this.openManualAnnotationDialog($event);
|
|
});
|
|
|
|
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(() => this._fileDataService.annotations),
|
|
switchMap<AnnotationWrapper[], Observable<readonly [boolean, AnnotationWrapper[]]>>(annotations => {
|
|
const showWarning = !this.userPreferenceService.getBool(PreferencesKeys.loadAllAnnotationsWarning);
|
|
const annotationsExceedThreshold = annotations.length >= this.configService.values.ANNOTATIONS_THRESHOLD;
|
|
|
|
if (annotationsExceedThreshold && showWarning) {
|
|
const data = new ConfirmationDialogInput({
|
|
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,
|
|
},
|
|
});
|
|
|
|
const ref = this._dialogService.openDialog('confirm', null, data);
|
|
return ref.afterClosed().pipe(
|
|
switchMap(async (result: ConfirmOptions) => {
|
|
const doNotShowWarningAgain = result === ConfirmOptions.SECOND_CONFIRM;
|
|
if (doNotShowWarningAgain) {
|
|
await this.userPreferenceService.save(PreferencesKeys.loadAllAnnotationsWarning, 'true');
|
|
await this.userPreferenceService.reload();
|
|
}
|
|
const shouldLoad = [ConfirmOptions.CONFIRM, ConfirmOptions.SECOND_CONFIRM].includes(result);
|
|
return [shouldLoad, annotations] as const;
|
|
}),
|
|
);
|
|
}
|
|
|
|
return of([true, annotations] as const);
|
|
}),
|
|
switchMap(async ([confirmed, annotations]) => {
|
|
if (confirmed) {
|
|
await this.drawChangedAnnotations([], annotations);
|
|
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._documentViewer.loaded$, this.state.file$])
|
|
.pipe(
|
|
tap(([viewMode]) =>
|
|
viewMode === 'STANDARD' || viewMode === 'TEXT_HIGHLIGHTS'
|
|
? this._viewerHeaderService.updateRotationButtons(true)
|
|
: this._viewerHeaderService.updateRotationButtons(false),
|
|
),
|
|
)
|
|
.subscribe();
|
|
}
|
|
|
|
private _handleDeletedDossier(): void {
|
|
const error = new CustomError(
|
|
_('error.deleted-entity.file-dossier.label'),
|
|
_('error.deleted-entity.file-dossier.action'),
|
|
'iqser:expand',
|
|
);
|
|
this._errorService.set(error);
|
|
}
|
|
|
|
private _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);
|
|
}
|
|
|
|
private async _cleanupAndRedrawAnnotations(newAnnotations: List<AnnotationWrapper>) {
|
|
if (!newAnnotations.length) {
|
|
return;
|
|
}
|
|
|
|
const currentFilters = this._filterService.getGroup('primaryFilters')?.filters || [];
|
|
this.#rebuildFilters();
|
|
|
|
if (currentFilters) {
|
|
this._handleDeltaAnnotationFilters(currentFilters);
|
|
}
|
|
|
|
await this._annotationDrawService.draw(newAnnotations, this._skippedService.hideSkipped, this.state.dossierTemplateId);
|
|
}
|
|
|
|
private _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,
|
|
});
|
|
}
|
|
|
|
private _openFullScreen() {
|
|
const documentElement = document.documentElement;
|
|
if (documentElement.requestFullscreen) {
|
|
documentElement.requestFullscreen().then();
|
|
}
|
|
}
|
|
|
|
private _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);
|
|
}
|
|
|
|
private _isJapaneseString(text: string) {
|
|
return text.match(/[\u3000-\u303f\u3040-\u309f\u30a0-\u30ff\uff00-\uff9f\u4e00-\u9faf\u3400-\u4dbf]/);
|
|
}
|
|
}
|