red-ui/apps/red-ui/src/app/modules/file-preview/file-preview-screen.component.ts
2023-03-16 20:15:49 +02:00

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]/);
}
}