RED-6829: update pdf event listeners to run in ng zone

This commit is contained in:
Dan Percic 2023-06-12 01:06:40 +03:00
parent f3e7811523
commit 04cc33b18e
14 changed files with 194 additions and 199 deletions

View File

@ -47,7 +47,7 @@
<router-outlet></router-outlet>
<redaction-pdf-viewer [style.visibility]="(documentViewer.loaded$ | async) ? 'visible' : 'hidden'"></redaction-pdf-viewer>
<redaction-pdf-viewer [style.visibility]="documentViewer.loaded() ? 'visible' : 'hidden'"></redaction-pdf-viewer>
<iqser-skeleton [templates]="{ dashboard: dashboardSkeleton, dossier: dossierSkeleton }"></iqser-skeleton>

View File

@ -1,62 +1,58 @@
<ng-container *ngIf="componentContext$ | async as ctx">
<div class="right-title heading" translate="file-preview.tabs.document-info.label">
<div class="right-title heading" translate="file-preview.tabs.document-info.label">
<div>
<iqser-circle-button
(action)="edit()"
*ngIf="permissionsService.canEditFileAttributes(_state.file(), _state.dossier())"
[tooltip]="'file-preview.tabs.document-info.edit' | translate"
buttonId="edit-document-info-btn"
icon="iqser:edit"
tooltipPosition="before"
></iqser-circle-button>
<iqser-circle-button
(action)="documentInfoService.hide()"
[tooltip]="'file-preview.tabs.document-info.close' | translate"
buttonId="close-document-info-btn"
icon="iqser:close"
tooltipPosition="before"
></iqser-circle-button>
</div>
</div>
<div class="right-content" iqserHasScrollbar>
<div class="section">
<div *ngFor="let attr of _fileAttributes()" class="attribute">
<div class="small-label">{{ attr.label }}:</div>
<div>{{ attr.value ? (isDate(attr) ? (attr.value | date : 'd MMM yyyy') : attr.value) : '-' }}</div>
</div>
</div>
<div class="section small-label stats-subtitle">
<div>
<iqser-circle-button
(action)="edit()"
*ngIf="permissionsService.canEditFileAttributes(_state.file(), _state.dossier())"
[tooltip]="'file-preview.tabs.document-info.edit' | translate"
buttonId="edit-document-info-btn"
icon="iqser:edit"
tooltipPosition="before"
></iqser-circle-button>
<mat-icon svgIcon="red:folder"></mat-icon>
<span
[innerHTML]="'file-preview.tabs.document-info.details.dossier' | translate : { dossierName: _state.dossier().dossierName }"
></span>
</div>
<iqser-circle-button
(action)="documentInfoService.hide()"
[tooltip]="'file-preview.tabs.document-info.close' | translate"
buttonId="close-document-info-btn"
icon="iqser:close"
tooltipPosition="before"
></iqser-circle-button>
<div>
<mat-icon svgIcon="iqser:document"></mat-icon>
<span>{{ 'file-preview.tabs.document-info.details.pages' | translate : { pages: _state.file().numberOfPages } }}</span>
</div>
<div *ngIf="_state.file().added | date : 'mediumDate' as added">
<mat-icon svgIcon="red:calendar"></mat-icon>
<span [innerHTML]="'file-preview.tabs.document-info.details.created-on' | translate : { date: added }"></span>
</div>
<div *ngIf="_state.dossier().dueDate | date : 'mediumDate' as dueDate">
<mat-icon svgIcon="red:lightning"></mat-icon>
<span [innerHTML]="'file-preview.tabs.document-info.details.due' | translate : { date: dueDate }"></span>
</div>
<div>
<mat-icon svgIcon="red:template"></mat-icon>
{{ _dossierTemplate().name }}
</div>
</div>
<div class="right-content" iqserHasScrollbar>
<div class="section">
<div *ngFor="let attr of ctx.fileAttributes" class="attribute">
<div class="small-label">{{ attr.label }}:</div>
<div>{{ attr.value ? (isDate(attr) ? (attr.value | date : 'd MMM yyyy') : attr.value) : '-' }}</div>
</div>
</div>
<div class="section small-label stats-subtitle">
<div>
<mat-icon svgIcon="red:folder"></mat-icon>
<span
[innerHTML]="
'file-preview.tabs.document-info.details.dossier' | translate : { dossierName: _state.dossier().dossierName }
"
></span>
</div>
<div>
<mat-icon svgIcon="iqser:document"></mat-icon>
<span>{{ 'file-preview.tabs.document-info.details.pages' | translate : { pages: _state.file().numberOfPages } }}</span>
</div>
<div *ngIf="_state.file().added | date : 'mediumDate' as added">
<mat-icon svgIcon="red:calendar"></mat-icon>
<span [innerHTML]="'file-preview.tabs.document-info.details.created-on' | translate : { date: added }"></span>
</div>
<div *ngIf="_state.dossier().dueDate | date : 'mediumDate' as dueDate">
<mat-icon svgIcon="red:lightning"></mat-icon>
<span [innerHTML]="'file-preview.tabs.document-info.details.due' | translate : { date: dueDate }"></span>
</div>
<div>
<mat-icon svgIcon="red:template"></mat-icon>
{{ ctx.dossierTemplateName }}
</div>
</div>
</div>
</ng-container>
</div>

View File

@ -1,14 +1,13 @@
import { Component, inject } from '@angular/core';
import { Component, computed, inject } from '@angular/core';
import { DossierTemplatesService } from '@services/dossier-templates/dossier-templates.service';
import { DocumentInfoService } from '../../services/document-info.service';
import { combineLatest, switchMap } from 'rxjs';
import { PermissionsService } from '@services/permissions.service';
import { FilePreviewStateService } from '../../services/file-preview-state.service';
import { map } from 'rxjs/operators';
import { type FileAttributeConfigType, FileAttributeConfigTypes } from '@red/domain';
import { FilePreviewDialogService } from '../../services/file-preview-dialog.service';
import { FileAttributesService } from '@services/entity-services/file-attributes.service';
import { ContextComponent } from '@iqser/common-ui';
import { toSignal } from '@angular/core/rxjs-interop';
interface FileAttribute {
label: string;
@ -17,7 +16,6 @@ interface FileAttribute {
}
interface Context {
readonly dossierTemplateName: string;
readonly fileAttributes: FileAttribute[];
}
@ -28,28 +26,22 @@ interface Context {
})
export class DocumentInfoComponent extends ContextComponent<Context> {
protected readonly _state = inject(FilePreviewStateService);
protected readonly _dossierTemplate = toSignal(inject(DossierTemplatesService).getEntityChanged$(this._state.dossierTemplateId));
protected readonly _fileAttributesConfig = toSignal(inject(FileAttributesService).fileAttributesConfig$);
protected readonly _attributes = toSignal(
this.documentInfoService.fileAttributes$(this._state.fileId, this._state.dossierId, this._state.dossierTemplateId),
);
protected readonly _fileAttributes = computed(() => {
this._fileAttributesConfig();
return this._attributes();
});
constructor(
fileAttributesService: FileAttributesService,
readonly permissionsService: PermissionsService,
readonly documentInfoService: DocumentInfoService,
private readonly _dialogService: FilePreviewDialogService,
private readonly _dossierTemplatesService: DossierTemplatesService,
) {
super();
const fileAttributes$ = combineLatest([this._state.file$, this._state.dossier$, fileAttributesService.fileAttributesConfig$]).pipe(
switchMap(([file, dossier]) => this.documentInfoService.fileAttributes$(file.id, dossier.id, dossier.dossierTemplateId)),
);
const dossierTemplateName$ = this._state.dossier$.pipe(
switchMap(dossier => this._dossierTemplatesService.getEntityChanged$(dossier.dossierTemplateId)),
map(dossierTemplate => dossierTemplate.name),
);
super._initContext({
dossierTemplateName: dossierTemplateName$,
fileAttributes: fileAttributes$,
});
}
edit() {

View File

@ -26,6 +26,7 @@
></div>
<iqser-popup-filter
*ngIf="documentInfoService.hidden()"
[actionsTemplate]="annotationFilterActionTemplate"
[attr.help-mode-key]="'workload_in_editor'"
[primaryFiltersSlug]="'primaryFilters'"

View File

@ -75,7 +75,6 @@ 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';
import { toObservable } from '@angular/core/rxjs-interop';
const textActions = [TextPopups.ADD_DICTIONARY, TextPopups.ADD_FALSE_POSITIVE];
@ -147,32 +146,41 @@ export class FilePreviewScreenComponent
const file = this.state.file();
this._fileDataService.loadAnnotations(file).then();
});
effect(
() => {
if (this._documentViewer.loaded()) {
this._pageRotationService.clearRotations();
this._viewerHeaderService.disable(ROTATION_ACTION_BUTTONS);
this.viewerReady().then();
}
},
{ allowSignalWrites: true },
);
effect(() => {
if (this._documentViewer.pageComplete()) {
this.#setExcludedPageStyles();
}
});
effect(() => {
const selectedText = this._documentViewer.selectedText();
const canPerformActions = this.pdfProxyService.canPerformActions();
const isCurrentPageExcluded = this.state.file().isPageExcluded(this.pdf.currentPage());
if ((selectedText.length > 2 || this._isJapaneseString(selectedText)) && canPerformActions && !isCurrentPageExcluded) {
this.pdf.enable(textActions);
} else {
this.pdf.disable(textActions);
}
});
}
get changed() {
return this._pageRotationService.hasRotations();
}
get #textSelected$() {
const textSelected$ = combineLatest([
this._documentViewer.textSelected$,
toObservable(this.pdfProxyService.canPerformActions, { injector: this._injector }),
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()));
@ -440,17 +448,6 @@ export class FilePreviewScreenComponent
}
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(),
@ -471,7 +468,7 @@ export class FilePreviewScreenComponent
),
);
return combineLatest([currentPageAnnotations$, documentLoaded$]).pipe(
return combineLatest([currentPageAnnotations$, this._documentViewer.loaded$]).pipe(
filter(([, loaded]) => loaded),
map(([annotations]) => annotations),
map(annotations => this.drawChangedAnnotations(...annotations)),
@ -608,23 +605,10 @@ export class FilePreviewScreenComponent
.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();

View File

@ -23,12 +23,15 @@ export class DocumentInfoService {
) {
this.hidden = computed(() => !this.#show$());
this.shown = this.#show$.asReadonly();
effect(() => {
if (this.shown()) {
this._multiSelectService.deactivate();
this._excludedPagesService.hide();
}
});
effect(
() => {
if (this.shown()) {
this._multiSelectService.deactivate();
this._excludedPagesService.hide();
}
},
{ allowSignalWrites: true },
);
}
fileAttributes$(fileId: string, dossierId: string, dossierTemplateId: string) {

View File

@ -15,6 +15,7 @@ import { TranslateService } from '@ngx-translate/core';
import { DictionariesMapService } from '@services/entity-services/dictionaries-map.service';
import { DossierDictionariesMapService } from '@services/entity-services/dossier-dictionaries-map.service';
import { toSignal } from '@angular/core/rxjs-interop';
import { ViewModeService } from './view-mode.service';
const ONE_MEGABYTE = 1024 * 1024;
@ -64,6 +65,7 @@ export class FilePreviewStateService {
private readonly _dictionariesMapService: DictionariesMapService,
private readonly _translateService: TranslateService,
private readonly _loadingService: LoadingService,
private readonly _viewModeService: ViewModeService,
) {
const dossiersService = dossiersServiceResolver(_injector, router);
this.dossier$ = dossiersService.getEntityChanged$(this.dossierId);
@ -82,12 +84,20 @@ export class FilePreviewStateService {
this.blob$ = this.#blob$;
this.dossierDictionary = toSignal(this._dossierDictionariesMapService.watch$(this.dossierId, 'dossier_redaction'));
this.#dossierFileChange = toSignal(this.#dossierFilesChange$());
this.#dossierFileChange = toSignal(this.#dossierFilesChange$);
effect(() => {
if (this.#dossierFileChange()) {
this._filesService.loadAll(this.dossierId);
}
});
effect(
() => {
if (this._viewModeService.isEarmarks() && !this.file().hasHighlights) {
this._viewModeService.switchToStandard();
}
},
{ allowSignalWrites: true },
);
}
get dictionaries(): Dictionary[] {
@ -113,6 +123,13 @@ export class FilePreviewStateService {
);
}
get #dossierFilesChange$() {
return this._dossiersService.dossierFileChanges$.pipe(
filter(dossierId => dossierId === this.dossierId),
map(() => true),
);
}
reloadBlob(): void {
this.#reloadBlob$.next(true);
}
@ -127,13 +144,6 @@ export class FilePreviewStateService {
return `${remainingTime} ${seconds}`;
}
#dossierFilesChange$() {
return this._dossiersService.dossierFileChanges$.pipe(
filter(dossierId => dossierId === this.dossierId),
map(() => true),
);
}
#downloadOriginalFile(cacheIdentifier: string, wipeCaches = true): Observable<Blob> {
const downloadFile$ = this.#getFileToDownload(cacheIdentifier);
const obs = wipeCaches ? from(wipeCache('files')) : of({});

View File

@ -184,7 +184,7 @@ export class PdfProxyService {
private _addManualRedactionOfType(type: ManualRedactionEntryType) {
const selectedQuads: Record<string, Quad[]> = this._pdf.documentViewer.getSelectedTextQuads();
const text = this._documentViewer.selectedText;
const text = this._documentViewer.selectedText();
const manualRedactionEntry = this._getManualRedaction(selectedQuads, text, true);
this.manualAnnotationRequested$.next({ manualRedactionEntry, type });
}
@ -208,7 +208,7 @@ export class PdfProxyService {
this._pdf.enable(TEXT_POPUPS_TO_TOGGLE);
this._viewerHeaderService.enable(HEADER_ITEMS_TO_TOGGLE);
if (this._documentViewer.selectedText.length > 2) {
if (this._documentViewer.selectedText().length > 2) {
this._pdf.enable([TextPopups.ADD_DICTIONARY, TextPopups.ADD_FALSE_POSITIVE]);
}
} else {

View File

@ -1,4 +1,4 @@
<div [style.visibility]="(documentViewer.loaded$ | async) ? 'visible' : 'hidden'" class="pagination noselect">
<div [style.visibility]="documentViewer.loaded() ? 'visible' : 'hidden'" class="pagination noselect">
<div (click)="pdf.navigatePreviousPage()">
<mat-icon class="chevron-icon" svgIcon="red:nav-prev"></mat-icon>
</div>
@ -8,7 +8,7 @@
#pageInput
(change)="pdf.navigateTo(pageInput.value)"
[max]="pdf.totalPages()"
[value]="pdf.currentPage$ | async"
[value]="pdf.currentPage()"
class="page-number-input"
id="currentPageNumber"
min="1"

View File

@ -66,8 +66,7 @@ export class AnnotationDrawService {
const annotations = annotationWrappers
?.map(annotation => this._computeAnnotation(annotation, hideSkipped, totalPages, dossierTemplateId))
.filterTruthy();
const documentLoaded = await firstValueFrom(this._documentViewer.loaded$);
if (!documentLoaded) {
if (!this._documentViewer.loaded()) {
return;
}
await this._annotationManager.add(annotations);

View File

@ -1,32 +1,45 @@
import { inject, Injectable } from '@angular/core';
import { inject, Injectable, NgZone, Signal, signal } from '@angular/core';
import { Core } from '@pdftron/webviewer';
import { NGXLogger } from 'ngx-logger';
import { fromEvent, merge, Observable } from 'rxjs';
import { debounceTime, filter, map, tap } from 'rxjs/operators';
import { fromEvent, Observable } from 'rxjs';
import { filter, tap } from 'rxjs/operators';
import { ActivatedRoute } from '@angular/router';
import { PdfViewer } from './pdf-viewer.service';
import { UserPreferenceService } from '@users/user-preference.service';
import { log, shareDistinctLast, shareLast } from '@iqser/common-ui';
import { log } from '@iqser/common-ui';
import { stopAndPrevent, stopAndPreventIfNotAllowed } from '../utils/functions';
import { RotationType, RotationTypes } from '@red/domain';
import { AnnotationToolNames } from '../utils/constants';
import { toObservable } from '@angular/core/rxjs-interop';
import DocumentViewer = Core.DocumentViewer;
import Color = Core.Annotations.Color;
import Quad = Core.Math.Quad;
@Injectable()
export class REDDocumentViewer {
loaded$: Observable<boolean>;
pageComplete$: Observable<boolean>;
readonly loaded$: Observable<boolean>;
keyUp$: Observable<KeyboardEvent>;
textSelected$: Observable<string>;
selectedText = '';
readonly selectedText: Signal<string>;
readonly loaded: Signal<boolean>;
readonly pageComplete: Signal<unknown | undefined>;
#document: DocumentViewer;
readonly #loaded = signal(false);
readonly #pageComplete = signal<unknown | undefined>(undefined);
readonly #selectedText = signal('');
readonly #logger = inject(NGXLogger);
readonly #userPreferenceService = inject(UserPreferenceService);
readonly #pdf = inject(PdfViewer);
readonly #activatedRoute = inject(ActivatedRoute);
readonly #ngZone = inject(NgZone);
constructor() {
this.loaded = this.#loaded.asReadonly();
this.loaded$ = toObservable(this.#loaded);
this.pageComplete = this.#pageComplete.asReadonly();
this.selectedText = this.#selectedText.asReadonly();
}
get PDFDoc() {
return this.document?.getPDFDoc();
@ -40,30 +53,6 @@ export class REDDocumentViewer {
return this.document?.getPageRotation(this.#document.getCurrentPage());
}
get #documentUnloaded$() {
const event$ = fromEvent(this.#document, 'documentUnloaded');
const toBool$ = event$.pipe(map(() => false));
return toBool$.pipe(tap(() => this.#logger.info('[PDF] Document unloaded')));
}
get #documentLoaded$() {
const event$ = fromEvent(this.#document, 'documentLoaded');
const toBool$ = event$.pipe(map(() => true));
return toBool$.pipe(
tap(() =>
this.#pdf.runWithCleanup(async () => {
await this.#flattenAnnotations();
this.#setCurrentPage();
this.#setInitialDisplayMode();
this.updateTooltipsVisibility();
}),
),
tap(() => this.#logger.info('[PDF] Document loaded')),
);
}
get #keyUp$() {
return fromEvent<KeyboardEvent>(this.#document, 'keyUp').pipe(
tap(stopAndPreventIfNotAllowed),
@ -81,22 +70,6 @@ export class REDDocumentViewer {
);
}
get #pageComplete$() {
return fromEvent(this.#document, 'pageComplete').pipe(debounceTime(300));
}
get #textSelected$(): Observable<string> {
return fromEvent<[Quad, string, number]>(this.#document, 'textSelected').pipe(
tap(([, selectedText]) => (this.selectedText = selectedText)),
tap(([, , pageNumber]) => this.#disableTextPopupIfCompareMode(pageNumber)),
map(([, selectedText]) => selectedText),
);
}
get #loaded$() {
return merge(this.#documentUnloaded$, this.#documentLoaded$).pipe(shareDistinctLast());
}
clearSelection() {
this.#document.clearSelection();
this.#pdf.disable('textPopup');
@ -120,10 +93,8 @@ export class REDDocumentViewer {
init(document: DocumentViewer) {
this.#document = document;
this.loaded$ = this.#loaded$;
this.pageComplete$ = this.#pageComplete$.pipe(shareLast());
this.#listenForDocEvents();
this.keyUp$ = this.#keyUp$;
this.textSelected$ = this.#textSelected$;
}
async blob() {
@ -172,6 +143,45 @@ export class REDDocumentViewer {
}
}
#listenForDocEvents() {
this.#document.addEventListener('textSelected', ([, selectedText, pageNumber]: [Quad, string, number]) => {
this.#ngZone.run(() => {
this.#disableTextPopupIfCompareMode(pageNumber);
this.#selectedText.set(selectedText);
});
});
this.#document.addEventListener('pageComplete', event => {
this.#ngZone.run(() => {
setTimeout(() => {
this.#pageComplete.set(event);
}, 300);
});
});
this.#document.addEventListener('documentUnloaded', () => {
this.#ngZone.run(() => {
this.#logger.info('[PDF] Document unloaded');
this.#loaded.set(false);
});
});
this.#document.addEventListener('documentLoaded', () => {
this.#ngZone.run(() => {
this.#logger.info('[PDF] Document loaded');
this.#pdf.runWithCleanup(() => {
this.#flattenAnnotations().then();
this.#setCurrentPage();
this.#setInitialDisplayMode();
this.updateTooltipsVisibility();
});
this.#loaded.set(true);
});
});
}
async #flattenAnnotations() {
const pdfDoc = await this.PDFDoc;
await pdfDoc.flattenAnnotations(false);

View File

@ -162,7 +162,7 @@ export class FileActionsComponent implements OnChanges {
type: ActionTypes.circleBtn,
action: () => this._documentInfoService.toggle(),
tooltip: _('file-preview.document-info'),
ariaExpanded: toObservable(this._documentInfoService.shown, { injector: this._injector }),
ariaExpanded: toObservable(this._documentInfoService?.shown, { injector: this._injector }),
icon: 'red:status-info',
show: !!this._documentInfoService,
},
@ -171,7 +171,7 @@ export class FileActionsComponent implements OnChanges {
type: ActionTypes.circleBtn,
action: () => this._excludedPagesService.toggle(),
tooltip: _('file-preview.exclude-pages'),
ariaExpanded: toObservable(this._excludedPagesService.shown, { injector: this._injector }),
ariaExpanded: toObservable(this._excludedPagesService?.shown, { injector: this._injector }),
showDot: !!this.file.excludedPages?.length,
icon: 'red:exclude-pages',
show:

View File

@ -44,7 +44,7 @@
<iqser-circle-button
(menuClosed)="expanded = false"
(menuOpened)="expanded = true"
*ngIf="hiddenButtons.length > 0"
*ngIf="hiddenButtons?.length > 0"
[attr.aria-expanded]="expanded"
[icon]="'iqser:more-actions'"
[matMenuTriggerFor]="hiddenButtonsMenu"

View File

@ -19,9 +19,9 @@ import { MatDialog } from '@angular/material/dialog';
styleUrls: ['./expandable-file-actions.component.scss'],
})
export class ExpandableFileActionsComponent implements OnChanges {
@Input({ required: true }) actions: Action[];
@Input() maxWidth: number;
@Input() minWidth: number;
@Input() actions: Action[];
@Input() buttonType: CircleButtonType;
@Input() tooltipPosition: IqserTooltipPosition;