Pull request #333: File preview state service
Merge in RED/ui from refactor-fp/state to master * commit '527d4933a07ba99565e5a6c4e7d6223f2aa3b6e4': Fixes Fixed errors on document reload WIP Fixed reload document on file data change Fixed canSwitchToDeltaView State service
This commit is contained in:
commit
cfafc88121
@ -2,33 +2,45 @@ import { Dictionary, File, IRedactionLog, IRedactionLogEntry, IViewedPage, ViewM
|
||||
import { AnnotationWrapper } from './annotation.wrapper';
|
||||
import { RedactionLogEntryWrapper } from './redaction-log-entry.wrapper';
|
||||
import * as moment from 'moment';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
|
||||
export class FileDataModel {
|
||||
static readonly DELTA_VIEW_TIME = 10 * 60 * 1000; // 10 minutes;
|
||||
|
||||
hasChangeLog: boolean;
|
||||
allAnnotations: AnnotationWrapper[];
|
||||
readonly hasChangeLog$ = new BehaviorSubject<boolean>(false);
|
||||
readonly blob$ = new BehaviorSubject<Blob>(undefined);
|
||||
readonly file$ = new BehaviorSubject<File>(undefined);
|
||||
|
||||
constructor(
|
||||
public file: File,
|
||||
public fileData: Blob,
|
||||
private readonly _file: File,
|
||||
private readonly _blob: Blob,
|
||||
private _redactionLog: IRedactionLog,
|
||||
public viewedPages?: IViewedPage[],
|
||||
private _dictionaryData?: { [p: string]: Dictionary },
|
||||
private _areDevFeaturesEnabled?: boolean,
|
||||
) {
|
||||
this.file$.next(_file);
|
||||
this.blob$.next(_blob);
|
||||
this._buildAllAnnotations();
|
||||
}
|
||||
|
||||
get file(): File {
|
||||
return this.file$.value;
|
||||
}
|
||||
|
||||
set file(file: File) {
|
||||
this.file$.next(file);
|
||||
}
|
||||
|
||||
get redactionLog(): IRedactionLog {
|
||||
return this._redactionLog;
|
||||
}
|
||||
|
||||
set redactionLog(redactionLog: IRedactionLog) {
|
||||
this._redactionLog = redactionLog;
|
||||
this._buildAllAnnotations();
|
||||
}
|
||||
|
||||
get redactionLog() {
|
||||
return this._redactionLog;
|
||||
}
|
||||
|
||||
getVisibleAnnotations(viewMode: ViewMode) {
|
||||
return this.allAnnotations.filter(annotation => {
|
||||
if (viewMode === 'STANDARD') {
|
||||
@ -47,7 +59,7 @@ export class FileDataModel {
|
||||
const previousAnnotations = this.allAnnotations || [];
|
||||
this.allAnnotations = entries
|
||||
.map(entry => AnnotationWrapper.fromData(entry))
|
||||
.filter(ann => ann.manual || !this.file.excludedPages.includes(ann.pageNumber));
|
||||
.filter(ann => ann.manual || !this._file.excludedPages.includes(ann.pageNumber));
|
||||
|
||||
if (!this._areDevFeaturesEnabled) {
|
||||
this.allAnnotations = this.allAnnotations.filter(annotation => !annotation.isFalsePositive);
|
||||
@ -121,8 +133,8 @@ export class FileDataModel {
|
||||
}
|
||||
|
||||
private _isChangeLogEntry(redactionLogEntry: IRedactionLogEntry, wrapper: RedactionLogEntryWrapper) {
|
||||
if (this.file.numberOfAnalyses > 1) {
|
||||
var viableChanges = redactionLogEntry.changes.filter(c => c.analysisNumber > 1);
|
||||
if (this._file.numberOfAnalyses > 1) {
|
||||
const viableChanges = redactionLogEntry.changes.filter(c => c.analysisNumber > 1);
|
||||
viableChanges.sort((a, b) => moment(a.dateTime).valueOf() - moment(b.dateTime).valueOf());
|
||||
|
||||
const lastChange = viableChanges.length >= 1 ? viableChanges[viableChanges.length - 1] : undefined;
|
||||
@ -141,7 +153,7 @@ export class FileDataModel {
|
||||
wrapper.changeLogType = relevantChanges[relevantChanges.length - 1].type;
|
||||
wrapper.isChangeLogEntry = true;
|
||||
viewedPage.showAsUnseen = moment(viewedPage.viewedTime).valueOf() < moment(lastChange.dateTime).valueOf();
|
||||
this.hasChangeLog = true;
|
||||
this.hasChangeLog$.next(true);
|
||||
} else {
|
||||
// no relevant changes - hide removed anyway
|
||||
wrapper.isChangeLogEntry = false;
|
||||
|
||||
@ -10,7 +10,6 @@ import { SharedModule } from '@shared/shared.module';
|
||||
import { DossiersRoutingModule } from './dossiers-routing.module';
|
||||
import { FileUploadDownloadModule } from '@upload-download/file-upload-download.module';
|
||||
import { DossiersDialogService } from './services/dossiers-dialog.service';
|
||||
import { PdfViewerDataService } from './services/pdf-viewer-data.service';
|
||||
import { ManualAnnotationService } from './services/manual-annotation.service';
|
||||
import { AnnotationProcessingService } from './services/annotation-processing.service';
|
||||
import { EditDossierDialogComponent } from './dialogs/edit-dossier-dialog/edit-dossier-dialog.component';
|
||||
@ -54,7 +53,7 @@ const components = [
|
||||
...dialogs,
|
||||
];
|
||||
|
||||
const services = [DossiersDialogService, ManualAnnotationService, PdfViewerDataService, AnnotationProcessingService];
|
||||
const services = [DossiersDialogService, ManualAnnotationService, AnnotationProcessingService];
|
||||
|
||||
@NgModule({
|
||||
declarations: [...components],
|
||||
|
||||
@ -96,7 +96,6 @@
|
||||
[file]="file"
|
||||
[number]="pageNumber"
|
||||
[showDottedIcon]="hasOnlyManualRedactionsAndIsExcluded(pageNumber)"
|
||||
[viewedPages]="viewedPages"
|
||||
></redaction-page-indicator>
|
||||
</div>
|
||||
|
||||
|
||||
@ -28,7 +28,7 @@ import { PermissionsService } from '@services/permissions.service';
|
||||
import { WebViewerInstance } from '@pdftron/webviewer';
|
||||
import { BehaviorSubject, combineLatest, Observable } from 'rxjs';
|
||||
import { map, tap } from 'rxjs/operators';
|
||||
import { File, IViewedPage } from '@red/domain';
|
||||
import { File } from '@red/domain';
|
||||
import { ExcludedPagesService } from '../../services/excluded-pages.service';
|
||||
import { MultiSelectService } from '../../services/multi-select.service';
|
||||
import { DocumentInfoService } from '../../services/document-info.service';
|
||||
@ -52,7 +52,6 @@ export class FileWorkloadComponent {
|
||||
@Input() activeViewerPage: number;
|
||||
@Input() shouldDeselectAnnotationsOnPageChange: boolean;
|
||||
@Input() dialogRef: MatDialogRef<unknown>;
|
||||
@Input() viewedPages: IViewedPage[];
|
||||
@Input() file!: File;
|
||||
@Input() annotationActionsTemplate: TemplateRef<unknown>;
|
||||
@Input() viewer: WebViewerInstance;
|
||||
@ -225,7 +224,7 @@ export class FileWorkloadComponent {
|
||||
|
||||
scrollAnnotationsToPage(page: number, mode: 'always' | 'if-needed' = 'if-needed'): void {
|
||||
if (this._annotationsElement) {
|
||||
const elements = this._annotationsElement.nativeElement.querySelectorAll(`div[anotation-page-header="${page}"]`);
|
||||
const elements: HTMLElement[] = this._annotationsElement.nativeElement.querySelectorAll(`div[anotation-page-header="${page}"]`);
|
||||
FileWorkloadComponent._scrollToFirstElement(elements, mode);
|
||||
}
|
||||
}
|
||||
@ -235,7 +234,7 @@ export class FileWorkloadComponent {
|
||||
if (!this.selectedAnnotations || this.selectedAnnotations.length === 0 || !this._annotationsElement) {
|
||||
return;
|
||||
}
|
||||
const elements = this._annotationsElement.nativeElement.querySelectorAll(
|
||||
const elements: HTMLElement[] = this._annotationsElement.nativeElement.querySelectorAll(
|
||||
`div[annotation-id="${this._firstSelectedAnnotation?.id}"].active`,
|
||||
);
|
||||
FileWorkloadComponent._scrollToFirstElement(elements);
|
||||
@ -412,7 +411,7 @@ export class FileWorkloadComponent {
|
||||
|
||||
private _scrollQuickNavigationToPage(page: number) {
|
||||
if (this._quickNavigationElement) {
|
||||
const elements = this._quickNavigationElement.nativeElement.querySelectorAll(`#quick-nav-page-${page}`);
|
||||
const elements: HTMLElement[] = this._quickNavigationElement.nativeElement.querySelectorAll(`#quick-nav-page-${page}`);
|
||||
FileWorkloadComponent._scrollToFirstElement(elements);
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,6 +6,7 @@ import { ViewedPagesService } from '@services/entity-services/viewed-pages.servi
|
||||
import { File, IViewedPage } from '@red/domain';
|
||||
import { AutoUnsubscribe } from '@iqser/common-ui';
|
||||
import { FilesMapService } from '@services/entity-services/files-map.service';
|
||||
import { FilePreviewStateService } from '../../services/file-preview-state.service';
|
||||
|
||||
@Component({
|
||||
selector: 'redaction-page-indicator',
|
||||
@ -18,7 +19,6 @@ export class PageIndicatorComponent extends AutoUnsubscribe implements OnDestroy
|
||||
@Input() active = false;
|
||||
@Input() showDottedIcon = false;
|
||||
@Input() number: number;
|
||||
@Input() viewedPages: IViewedPage[];
|
||||
@Input() activeSelection = false;
|
||||
|
||||
@Output() readonly pageSelected = new EventEmitter<number>();
|
||||
@ -33,25 +33,17 @@ export class PageIndicatorComponent extends AutoUnsubscribe implements OnDestroy
|
||||
private readonly _configService: ConfigService,
|
||||
private readonly _changeDetectorRef: ChangeDetectorRef,
|
||||
private readonly _permissionService: PermissionsService,
|
||||
private readonly _stateService: FilePreviewStateService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
get activePage() {
|
||||
return this.viewedPages?.find(p => p.page === this.number);
|
||||
return this._viewedPages.find(p => p.page === this.number);
|
||||
}
|
||||
|
||||
private _setReadState() {
|
||||
const readBefore = this.read;
|
||||
const activePage = this.activePage;
|
||||
if (!activePage) {
|
||||
this.read = false;
|
||||
} else {
|
||||
// console.log('setting read to',activePage.showAsUnseen, !activePage.showAsUnseen);
|
||||
this.read = !activePage.showAsUnseen;
|
||||
}
|
||||
// console.log(this.number, readBefore, activePage, this.read);
|
||||
this._changeDetectorRef.detectChanges();
|
||||
private get _viewedPages(): IViewedPage[] {
|
||||
return this._stateService.fileData?.viewedPages || [];
|
||||
}
|
||||
|
||||
ngOnChanges() {
|
||||
@ -87,20 +79,33 @@ export class PageIndicatorComponent extends AutoUnsubscribe implements OnDestroy
|
||||
}
|
||||
}
|
||||
|
||||
private _setReadState() {
|
||||
const readBefore = this.read;
|
||||
const activePage = this.activePage;
|
||||
if (!activePage) {
|
||||
this.read = false;
|
||||
} else {
|
||||
// console.log('setting read to',activePage.showAsUnseen, !activePage.showAsUnseen);
|
||||
this.read = !activePage.showAsUnseen;
|
||||
}
|
||||
// console.log(this.number, readBefore, activePage, this.read);
|
||||
this._changeDetectorRef.detectChanges();
|
||||
}
|
||||
|
||||
private async _markPageRead() {
|
||||
await this._viewedPagesService.addPage({ page: this.number }, this.file.dossierId, this.file.fileId).toPromise();
|
||||
if (this.activePage) {
|
||||
this.activePage.showAsUnseen = false;
|
||||
} else {
|
||||
this.viewedPages?.push({ page: this.number, fileId: this.file.fileId });
|
||||
this._viewedPages.push({ page: this.number, fileId: this.file.fileId });
|
||||
}
|
||||
this._setReadState();
|
||||
}
|
||||
|
||||
private async _markPageUnread() {
|
||||
await this._viewedPagesService.removePage(this.file.dossierId, this.file.fileId, this.number).toPromise();
|
||||
this.viewedPages?.splice(
|
||||
this.viewedPages?.findIndex(p => p.page === this.number),
|
||||
this._viewedPages.splice(
|
||||
this._viewedPages.findIndex(p => p.page === this.number),
|
||||
1,
|
||||
);
|
||||
this._setReadState();
|
||||
|
||||
@ -27,7 +27,7 @@ import { AnnotationActionsService } from '../../services/annotation-actions.serv
|
||||
import { UserPreferenceService } from '@services/user-preference.service';
|
||||
import { BASE_HREF } from '../../../../../../tokens';
|
||||
import { ConfigService } from '@services/config.service';
|
||||
import { ConfirmationDialogInput, LoadingService } from '@iqser/common-ui';
|
||||
import { AutoUnsubscribe, ConfirmationDialogInput, LoadingService, shareDistinctLast } from '@iqser/common-ui';
|
||||
import { DossiersDialogService } from '../../../../services/dossiers-dialog.service';
|
||||
import { loadCompareDocumentWrapper } from '../../../../utils/compare-mode.utils';
|
||||
import { PdfViewerUtils } from '../../../../utils/pdf-viewer.utils';
|
||||
@ -36,11 +36,13 @@ import { ActivatedRoute } from '@angular/router';
|
||||
import { toPosition } from '../../../../utils/pdf-calculation.utils';
|
||||
import { ViewModeService } from '../../services/view-mode.service';
|
||||
import { MultiSelectService } from '../../services/multi-select.service';
|
||||
import { FilePreviewStateService } from '../../services/file-preview-state.service';
|
||||
import { filter, switchMap, tap } from 'rxjs/operators';
|
||||
import Tools = Core.Tools;
|
||||
import TextTool = Tools.TextTool;
|
||||
import Annotation = Core.Annotations.Annotation;
|
||||
|
||||
const ALLOWED_KEYBOARD_SHORTCUTS = ['+', '-', 'p', 'r', 'Escape'] as const;
|
||||
const ALLOWED_KEYBOARD_SHORTCUTS: readonly string[] = ['+', '-', 'p', 'r', 'Escape'] as const;
|
||||
const dataElements = {
|
||||
ADD_REDACTION: 'add-redaction',
|
||||
ADD_DICTIONARY: 'add-dictionary',
|
||||
@ -61,8 +63,7 @@ const dataElements = {
|
||||
templateUrl: './pdf-viewer.component.html',
|
||||
styleUrls: ['./pdf-viewer.component.scss'],
|
||||
})
|
||||
export class PdfViewerComponent implements OnInit, OnChanges {
|
||||
@Input() fileData: Blob;
|
||||
export class PdfViewerComponent extends AutoUnsubscribe implements OnInit, OnChanges {
|
||||
@Input() file: File;
|
||||
@Input() dossier: Dossier;
|
||||
@Input() canPerformActions = false;
|
||||
@ -95,9 +96,12 @@ export class PdfViewerComponent implements OnInit, OnChanges {
|
||||
private readonly _annotationActionsService: AnnotationActionsService,
|
||||
private readonly _configService: ConfigService,
|
||||
private readonly _loadingService: LoadingService,
|
||||
private readonly _stateService: FilePreviewStateService,
|
||||
readonly viewModeService: ViewModeService,
|
||||
readonly multiSelectService: MultiSelectService,
|
||||
) {}
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
private get _toggleTooltipsBtnTitle(): string {
|
||||
return this._translateService.instant(_('pdf-viewer.toggle-tooltips'), {
|
||||
@ -115,7 +119,17 @@ export class PdfViewerComponent implements OnInit, OnChanges {
|
||||
|
||||
async ngOnInit() {
|
||||
this._setReadyAndInitialState = this._setReadyAndInitialState.bind(this);
|
||||
await this.loadViewer();
|
||||
await this._loadViewer();
|
||||
|
||||
this.addActiveScreenSubscription = this._stateService.fileData$
|
||||
.pipe(
|
||||
filter(fileData => !!fileData),
|
||||
switchMap(fileData => fileData.blob$),
|
||||
// Skip document reload if file content hasn't changed
|
||||
shareDistinctLast(),
|
||||
tap(() => this._loadDocument()),
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
@ -123,16 +137,12 @@ export class PdfViewerComponent implements OnInit, OnChanges {
|
||||
return;
|
||||
}
|
||||
|
||||
if (changes.fileData) {
|
||||
this._loadDocument();
|
||||
}
|
||||
|
||||
if (changes.canPerformActions) {
|
||||
this._handleCustomActions();
|
||||
}
|
||||
}
|
||||
|
||||
uploadFile(files: any) {
|
||||
uploadFile(files: FileList) {
|
||||
const fileToCompare = files[0];
|
||||
this.compareFileInput.nativeElement.value = null;
|
||||
|
||||
@ -148,7 +158,7 @@ export class PdfViewerComponent implements OnInit, OnChanges {
|
||||
await pdfNet.initialize(environment.licenseKey ? atob(environment.licenseKey) : null);
|
||||
|
||||
const compareDocument = await pdfNet.PDFDoc.createFromBuffer(fileReader.result as ArrayBuffer);
|
||||
const currentDocument = await pdfNet.PDFDoc.createFromBuffer(await this.fileData.arrayBuffer());
|
||||
const currentDocument = await pdfNet.PDFDoc.createFromBuffer(await this._stateService.fileData.blob$.value.arrayBuffer());
|
||||
|
||||
const loadCompareDocument = async () => {
|
||||
this._loadingService.start();
|
||||
@ -201,7 +211,7 @@ export class PdfViewerComponent implements OnInit, OnChanges {
|
||||
this.viewModeService.compareMode = false;
|
||||
const pdfNet = this.instance.Core.PDFNet;
|
||||
await pdfNet.initialize(environment.licenseKey ? atob(environment.licenseKey) : null);
|
||||
const currentDocument = await pdfNet.PDFDoc.createFromBuffer(await this.fileData.arrayBuffer());
|
||||
const currentDocument = await pdfNet.PDFDoc.createFromBuffer(await this._stateService.fileData.blob$.value.arrayBuffer());
|
||||
this.instance.UI.loadDocument(currentDocument, {
|
||||
filename: this.file ? this.file.filename : 'document.pdf',
|
||||
});
|
||||
@ -210,7 +220,7 @@ export class PdfViewerComponent implements OnInit, OnChanges {
|
||||
this.utils.navigateToPage(1);
|
||||
}
|
||||
|
||||
async loadViewer() {
|
||||
private async _loadViewer() {
|
||||
this.instance = await WebViewer(
|
||||
{
|
||||
licenseKey: environment.licenseKey ? atob(environment.licenseKey) : null,
|
||||
@ -219,7 +229,7 @@ export class PdfViewerComponent implements OnInit, OnChanges {
|
||||
css: this._convertPath('/assets/pdftron/stylesheet.css'),
|
||||
backendType: 'ems',
|
||||
},
|
||||
this.viewer.nativeElement,
|
||||
this.viewer.nativeElement as HTMLElement,
|
||||
);
|
||||
|
||||
this.documentViewer = this.instance.Core.documentViewer;
|
||||
@ -231,7 +241,7 @@ export class PdfViewerComponent implements OnInit, OnChanges {
|
||||
this.utils.disableHotkeys();
|
||||
this._configureTextPopup();
|
||||
|
||||
this.annotationManager.addEventListener('annotationSelected', (annotations, action) => {
|
||||
this.annotationManager.addEventListener('annotationSelected', (annotations: Annotation[], action) => {
|
||||
this.annotationSelected.emit(this.annotationManager.getSelectedAnnotations().map(ann => ann.Id));
|
||||
if (action === 'deselected') {
|
||||
this._toggleRectangleAnnotationAction(true);
|
||||
@ -241,7 +251,7 @@ export class PdfViewerComponent implements OnInit, OnChanges {
|
||||
}
|
||||
});
|
||||
|
||||
this.annotationManager.addEventListener('annotationChanged', annotations => {
|
||||
this.annotationManager.addEventListener('annotationChanged', (annotations: Annotation[]) => {
|
||||
// when a rectangle is drawn,
|
||||
// it returns one annotation with tool name 'AnnotationCreateRectangle;
|
||||
// this will auto select rectangle after drawing
|
||||
@ -251,7 +261,7 @@ export class PdfViewerComponent implements OnInit, OnChanges {
|
||||
}
|
||||
});
|
||||
|
||||
this.documentViewer.addEventListener('pageNumberUpdated', pageNumber => {
|
||||
this.documentViewer.addEventListener('pageNumberUpdated', (pageNumber: number) => {
|
||||
if (this.shouldDeselectAnnotationsOnPageChange) {
|
||||
this.utils.deselectAllAnnotations();
|
||||
}
|
||||
@ -261,9 +271,9 @@ export class PdfViewerComponent implements OnInit, OnChanges {
|
||||
|
||||
this.documentViewer.addEventListener('documentLoaded', this._setReadyAndInitialState);
|
||||
|
||||
this.documentViewer.addEventListener('keyUp', $event => {
|
||||
this.documentViewer.addEventListener('keyUp', ($event: KeyboardEvent) => {
|
||||
// arrows and full-screen
|
||||
if ($event.target?.tagName?.toLowerCase() !== 'input') {
|
||||
if (($event.target as HTMLElement)?.tagName?.toLowerCase() !== 'input') {
|
||||
if ($event.key.startsWith('Arrow') || $event.key === 'f') {
|
||||
this._ngZone.run(() => {
|
||||
this.keyUp.emit($event);
|
||||
@ -273,7 +283,7 @@ export class PdfViewerComponent implements OnInit, OnChanges {
|
||||
}
|
||||
}
|
||||
|
||||
if (ALLOWED_KEYBOARD_SHORTCUTS.indexOf($event.key) < 0) {
|
||||
if (!ALLOWED_KEYBOARD_SHORTCUTS.includes($event.key)) {
|
||||
$event.preventDefault();
|
||||
$event.stopPropagation();
|
||||
}
|
||||
@ -301,8 +311,6 @@ export class PdfViewerComponent implements OnInit, OnChanges {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this._loadDocument();
|
||||
}
|
||||
|
||||
private _setInitialDisplayMode() {
|
||||
@ -568,7 +576,7 @@ export class PdfViewerComponent implements OnInit, OnChanges {
|
||||
}
|
||||
|
||||
private _addManualRedactionOfType(type: ManualRedactionEntryType) {
|
||||
const selectedQuads = this.documentViewer.getSelectedTextQuads();
|
||||
const selectedQuads: Readonly<Record<string, Core.Math.Quad[]>> = this.documentViewer.getSelectedTextQuads();
|
||||
const text = this.documentViewer.getSelectedText();
|
||||
const manualRedaction = this._getManualRedaction(selectedQuads, text, true);
|
||||
this.manualAnnotationRequested.emit(new ManualRedactionEntryWrapper(selectedQuads, manualRedaction, type));
|
||||
@ -624,7 +632,7 @@ export class PdfViewerComponent implements OnInit, OnChanges {
|
||||
for (const quad of quads[key]) {
|
||||
const page = parseInt(key, 10);
|
||||
const pageHeight = this.documentViewer.getPageHeight(page);
|
||||
entry.positions.push(toPosition(page, pageHeight, convertQuads ? this.utils.translateQuads(page, quad) : quad));
|
||||
entry.positions.push(toPosition(page, pageHeight, convertQuads ? this.utils.translateQuad(page, quad) : quad));
|
||||
}
|
||||
}
|
||||
|
||||
@ -634,11 +642,7 @@ export class PdfViewerComponent implements OnInit, OnChanges {
|
||||
}
|
||||
|
||||
private _loadDocument() {
|
||||
if (!this.fileData) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.instance.UI.loadDocument(this.fileData, {
|
||||
this.instance.UI.loadDocument(this._stateService.fileData.blob$.value, {
|
||||
filename: this.file ? this.file.filename : 'document.pdf',
|
||||
});
|
||||
}
|
||||
@ -647,7 +651,7 @@ export class PdfViewerComponent implements OnInit, OnChanges {
|
||||
this._ngZone.run(() => {
|
||||
this.utils.ready = true;
|
||||
this.viewerReady.emit(this.instance);
|
||||
const routePageNumber = this._activatedRoute.snapshot.queryParams.page;
|
||||
const routePageNumber: number = this._activatedRoute.snapshot.queryParams.page;
|
||||
this.pageChanged.emit(routePageNumber || 1);
|
||||
this._setInitialDisplayMode();
|
||||
this._updateTooltipsVisibility();
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<ng-container *ngIf="viewModeService.viewMode$ | async as viewMode">
|
||||
<div
|
||||
<button
|
||||
(click)="switchView.emit('STANDARD')"
|
||||
[class.active]="viewModeService.isStandard"
|
||||
[matTooltip]="'file-preview.standard-tooltip' | translate"
|
||||
@ -7,27 +7,27 @@
|
||||
iqserHelpMode="standard-view"
|
||||
>
|
||||
{{ 'file-preview.standard' | translate }}
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<div
|
||||
(click)="canSwitchToDeltaView && switchView.emit('DELTA')"
|
||||
<button
|
||||
(click)="switchView.emit('DELTA')"
|
||||
[class.active]="viewModeService.isDelta"
|
||||
[class.disabled]="!canSwitchToDeltaView"
|
||||
[disabled]="(canSwitchToDeltaView$ | async) === false"
|
||||
[matTooltip]="'file-preview.delta-tooltip' | translate"
|
||||
class="red-tab"
|
||||
iqserHelpMode="delta-view"
|
||||
>
|
||||
{{ 'file-preview.delta' | translate }}
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<div
|
||||
<button
|
||||
(click)="canSwitchToRedactedView && switchView.emit('REDACTED')"
|
||||
[class.active]="viewModeService.isRedacted"
|
||||
[class.disabled]="!canSwitchToRedactedView"
|
||||
[disabled]="!canSwitchToRedactedView"
|
||||
[matTooltip]="'file-preview.redacted-tooltip' | translate"
|
||||
class="red-tab"
|
||||
iqserHelpMode="preview-view"
|
||||
>
|
||||
{{ 'file-preview.redacted' | translate }}
|
||||
</div>
|
||||
</button>
|
||||
</ng-container>
|
||||
|
||||
@ -1,10 +1,12 @@
|
||||
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core';
|
||||
import { File, ViewMode } from '@red/domain';
|
||||
import { ViewModeService } from '../../services/view-mode.service';
|
||||
import { FileDataModel } from '@models/file/file-data.model';
|
||||
import { FilePreviewStateService } from '../../services/file-preview-state.service';
|
||||
import { Observable } from 'rxjs';
|
||||
import { filter, switchMap } from 'rxjs/operators';
|
||||
|
||||
@Component({
|
||||
selector: 'redaction-view-switch [file] [fileData]',
|
||||
selector: 'redaction-view-switch [file]',
|
||||
templateUrl: './view-switch.component.html',
|
||||
styleUrls: ['./view-switch.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
@ -12,19 +14,18 @@ import { FileDataModel } from '@models/file/file-data.model';
|
||||
export class ViewSwitchComponent implements OnChanges {
|
||||
@Output() readonly switchView = new EventEmitter<ViewMode>();
|
||||
@Input() file: File;
|
||||
@Input() fileData: FileDataModel;
|
||||
|
||||
canSwitchToDeltaView = false;
|
||||
readonly canSwitchToDeltaView$: Observable<boolean>;
|
||||
canSwitchToRedactedView = false;
|
||||
|
||||
constructor(readonly viewModeService: ViewModeService) {}
|
||||
constructor(readonly viewModeService: ViewModeService, private readonly _stateService: FilePreviewStateService) {
|
||||
this.canSwitchToDeltaView$ = this._stateService.fileData$.pipe(
|
||||
filter(fileData => !!fileData),
|
||||
switchMap(fileData => fileData?.hasChangeLog$),
|
||||
);
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges) {
|
||||
if (changes.fileData) {
|
||||
const fileData = changes.fileData.currentValue as FileDataModel;
|
||||
this.canSwitchToDeltaView = fileData?.hasChangeLog;
|
||||
}
|
||||
|
||||
if (changes.file) {
|
||||
const file = changes?.file.currentValue as File;
|
||||
this.canSwitchToRedactedView = !file.analysisRequired && !file.excluded;
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
<ng-container *ngIf="dossier$ | async as dossier">
|
||||
<ng-container *ngIf="file$ | async as file">
|
||||
<ng-container *ngIf="fileData?.file$ | async as file">
|
||||
<section [class.fullscreen]="fullScreen">
|
||||
<div class="page-header">
|
||||
<div class="flex flex-1">
|
||||
<redaction-view-switch (switchView)="switchView($event)" [fileData]="fileData" [file]="file"></redaction-view-switch>
|
||||
<redaction-view-switch (switchView)="switchView($event)" [file]="file"></redaction-view-switch>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 actions-container">
|
||||
@ -76,7 +76,6 @@
|
||||
[canPerformActions]="canPerformAnnotationActions$ | async"
|
||||
[class.hidden]="!ready"
|
||||
[dossier]="dossier"
|
||||
[fileData]="fileData?.fileData"
|
||||
[file]="file"
|
||||
[shouldDeselectAnnotationsOnPageChange]="shouldDeselectAnnotationsOnPageChange"
|
||||
></redaction-pdf-viewer>
|
||||
@ -110,7 +109,6 @@
|
||||
[dialogRef]="dialogRef"
|
||||
[file]="file"
|
||||
[selectedAnnotations]="selectedAnnotations"
|
||||
[viewedPages]="fileData?.viewedPages"
|
||||
[viewer]="activeViewer"
|
||||
></redaction-file-workload>
|
||||
</div>
|
||||
|
||||
@ -14,13 +14,11 @@ import {
|
||||
OnDetach,
|
||||
processFilters,
|
||||
shareDistinctLast,
|
||||
shareLast,
|
||||
} from '@iqser/common-ui';
|
||||
import { MatDialogRef, MatDialogState } from '@angular/material/dialog';
|
||||
import { ManualRedactionEntryWrapper } from '@models/file/manual-redaction-entry.wrapper';
|
||||
import { AnnotationWrapper } from '@models/file/annotation.wrapper';
|
||||
import { ManualAnnotationResponse } from '@models/file/manual-annotation-response';
|
||||
import { FileDataModel } from '@models/file/file-data.model';
|
||||
import { AnnotationDrawService } from './services/annotation-draw.service';
|
||||
import { AnnotationProcessingService } from '../../services/annotation-processing.service';
|
||||
import { Dossier, File, ViewMode } from '@red/domain';
|
||||
@ -37,7 +35,7 @@ import { handleFilterDelta } from '@utils/filter-utils';
|
||||
import { FilesService } from '@services/entity-services/files.service';
|
||||
import { DossiersService } from '@services/entity-services/dossiers.service';
|
||||
import { FileManagementService } from '@services/entity-services/file-management.service';
|
||||
import { filter, map, switchMap, tap } from 'rxjs/operators';
|
||||
import { map, switchMap, tap } from 'rxjs/operators';
|
||||
import { FilesMapService } from '@services/entity-services/files-map.service';
|
||||
import { WatermarkService } from '@shared/services/watermark.service';
|
||||
import { ExcludedPagesService } from './services/excluded-pages.service';
|
||||
@ -49,6 +47,8 @@ import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
|
||||
import { CommentingService } from './services/commenting.service';
|
||||
import { SkippedService } from './services/skipped.service';
|
||||
import { AnnotationActionsService } from './services/annotation-actions.service';
|
||||
import { FilePreviewStateService } from './services/file-preview-state.service';
|
||||
import { FileDataModel } from '../../../../models/file/file-data.model';
|
||||
import Annotation = Core.Annotations.Annotation;
|
||||
import PDFNet = Core.PDFNet;
|
||||
|
||||
@ -67,6 +67,8 @@ const ALL_HOTKEY_ARRAY = ['Escape', 'F', 'f', 'ArrowUp', 'ArrowDown'];
|
||||
SkippedService,
|
||||
AnnotationDrawService,
|
||||
AnnotationActionsService,
|
||||
FilePreviewStateService,
|
||||
PdfViewerDataService,
|
||||
],
|
||||
})
|
||||
export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnInit, OnDestroy, OnAttach, OnDetach {
|
||||
@ -75,7 +77,6 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
|
||||
dialogRef: MatDialogRef<unknown>;
|
||||
fullScreen = false;
|
||||
shouldDeselectAnnotationsOnPageChange = true;
|
||||
fileData: FileDataModel;
|
||||
selectedAnnotations: AnnotationWrapper[] = [];
|
||||
displayPdfViewer = false;
|
||||
activeViewerPage: number = null;
|
||||
@ -83,7 +84,6 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
|
||||
readonly dossierId: string;
|
||||
readonly canPerformAnnotationActions$: Observable<boolean>;
|
||||
readonly dossier$: Observable<Dossier>;
|
||||
readonly file$: Observable<File>;
|
||||
readonly fileId: string;
|
||||
ready = false;
|
||||
private _instance: WebViewerInstance;
|
||||
@ -98,6 +98,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
|
||||
constructor(
|
||||
readonly permissionsService: PermissionsService,
|
||||
readonly userPreferenceService: UserPreferenceService,
|
||||
private readonly _stateService: FilePreviewStateService,
|
||||
private readonly _watermarkService: WatermarkService,
|
||||
private readonly _changeDetectorRef: ChangeDetectorRef,
|
||||
private readonly _activatedRoute: ActivatedRoute,
|
||||
@ -105,7 +106,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
|
||||
private readonly _router: Router,
|
||||
private readonly _annotationProcessingService: AnnotationProcessingService,
|
||||
private readonly _annotationDrawService: AnnotationDrawService,
|
||||
private readonly _fileDownloadService: PdfViewerDataService,
|
||||
private readonly _pdfViewerDataService: PdfViewerDataService,
|
||||
private readonly _filesService: FilesService,
|
||||
private readonly _ngZone: NgZone,
|
||||
private readonly _fileManagementService: FileManagementService,
|
||||
@ -126,12 +127,6 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
|
||||
this.dossierId = _activatedRoute.snapshot.paramMap.get('dossierId');
|
||||
this.dossier$ = _dossiersService.getEntityChanged$(this.dossierId);
|
||||
this.fileId = _activatedRoute.snapshot.paramMap.get('fileId');
|
||||
this.file$ = _filesMapService.watch$(this.dossierId, this.fileId).pipe(
|
||||
tap(async file => {
|
||||
await this._reloadFile(file);
|
||||
}),
|
||||
shareLast(),
|
||||
);
|
||||
this.canPerformAnnotationActions$ = this._canPerformAnnotationActions$;
|
||||
|
||||
document.documentElement.addEventListener('fullscreenchange', () => {
|
||||
@ -153,8 +148,16 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
|
||||
return this._instance;
|
||||
}
|
||||
|
||||
get fileData(): FileDataModel {
|
||||
return this._stateService.fileData;
|
||||
}
|
||||
|
||||
private get _canPerformAnnotationActions$() {
|
||||
return combineLatest([this.file$, this.viewModeService.viewMode$, this.viewModeService.compareMode$]).pipe(
|
||||
return combineLatest([
|
||||
this._stateService.fileData$.pipe(switchMap(fileData => fileData.file$)),
|
||||
this.viewModeService.viewMode$,
|
||||
this.viewModeService.compareMode$,
|
||||
]).pipe(
|
||||
map(([file, viewMode]) => this.permissionsService.canPerformAnnotationActions(file) && viewMode === 'STANDARD'),
|
||||
shareDistinctLast(),
|
||||
);
|
||||
@ -260,11 +263,9 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
|
||||
});
|
||||
console.log(`[REDACTION] Process time: ${new Date().getTime() - processStartTime} ms`);
|
||||
console.log(`[REDACTION] Filter rebuild time: ${new Date().getTime() - startTime}`);
|
||||
console.log();
|
||||
}
|
||||
|
||||
handleAnnotationSelected(annotationIds: string[]) {
|
||||
// TODO: use includes() here
|
||||
this.selectedAnnotations = annotationIds
|
||||
.map(id => this.visibleAnnotations.find(annotationWrapper => annotationWrapper.id === id))
|
||||
.filter(ann => ann !== undefined);
|
||||
@ -475,17 +476,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
|
||||
if (previousFile.lastOCRTime !== this.fileData?.file?.lastOCRTime) {
|
||||
return;
|
||||
}
|
||||
// excluded pages or document exclusion has changed
|
||||
const fileHasBeenExcludedOrIncluded = previousFile.excluded !== this.fileData.file.excluded;
|
||||
const excludedPagesHaveChanged =
|
||||
JSON.stringify(previousFile.excludedPages) !== JSON.stringify(this.fileData.file.excludedPages);
|
||||
if (fileHasBeenExcludedOrIncluded || excludedPagesHaveChanged) {
|
||||
await this._deleteAnnotations();
|
||||
await this._cleanupAndRedrawAnnotations();
|
||||
}
|
||||
}
|
||||
|
||||
await this._stampPDF();
|
||||
}
|
||||
|
||||
private async _stampPDF() {
|
||||
@ -543,20 +534,28 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
|
||||
}
|
||||
}
|
||||
|
||||
private async _fileUpdated(file: File): Promise<File> {
|
||||
if (!this.fileData || file.lastProcessed === this.fileData.file.lastProcessed) {
|
||||
await this._reloadFile(file);
|
||||
} else {
|
||||
// File reanalysed
|
||||
const previousAnnotations = this.visibleAnnotations;
|
||||
await this._loadFileData(file);
|
||||
await this._reloadAnnotations(previousAnnotations);
|
||||
}
|
||||
|
||||
return file;
|
||||
}
|
||||
|
||||
private _subscribeToFileUpdates(): void {
|
||||
this.addActiveScreenSubscription = this._filesMapService
|
||||
.watch$(this.dossierId, this.fileId)
|
||||
.pipe(switchMap(file => this._fileUpdated(file)))
|
||||
.subscribe();
|
||||
|
||||
this.addActiveScreenSubscription = timer(0, 5000)
|
||||
.pipe(switchMap(() => this._filesService.reload(this.dossierId, this.fileId)))
|
||||
.subscribe();
|
||||
this.addActiveScreenSubscription = this._filesMapService.fileReanalysed$
|
||||
.pipe(filter(file => file.fileId === this.fileId))
|
||||
.subscribe(async file => {
|
||||
if (file.lastProcessed !== this.fileData?.file.lastProcessed) {
|
||||
const previousAnnotations = this.visibleAnnotations;
|
||||
await this._loadFileData(file);
|
||||
await this._reloadAnnotations(previousAnnotations);
|
||||
}
|
||||
this._loadingService.stop();
|
||||
});
|
||||
|
||||
this.addActiveScreenSubscription = this._dossiersService
|
||||
.getEntityDeleted$(this.dossierId)
|
||||
@ -590,13 +589,13 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
|
||||
return this._router.navigate([this._dossiersService.find(this.dossierId).routerLink]);
|
||||
}
|
||||
|
||||
const fileData = await this._fileDownloadService.loadDataFor(file, this.fileData).toPromise();
|
||||
const fileData = await this._pdfViewerDataService.loadDataFor(file).toPromise();
|
||||
|
||||
if (file.isPending) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.fileData = fileData;
|
||||
this._stateService.fileData = fileData;
|
||||
}
|
||||
|
||||
@Debounce(0)
|
||||
@ -620,7 +619,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
|
||||
}
|
||||
|
||||
const currentPageAnnotations = this.visibleAnnotations.filter(a => a.pageNumber === page);
|
||||
this.fileData.redactionLog = await this._fileDownloadService.loadRedactionLogFor(this.dossierId, this.fileId).toPromise();
|
||||
this.fileData.redactionLog = await this._pdfViewerDataService.loadRedactionLogFor(this.dossierId, this.fileId).toPromise();
|
||||
|
||||
this._deleteAnnotations(currentPageAnnotations);
|
||||
await this._cleanupAndRedrawAnnotations(currentPageAnnotations, annotation => annotation.pageNumber === page);
|
||||
|
||||
@ -0,0 +1,21 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
import { FileDataModel } from '@models/file/file-data.model';
|
||||
|
||||
@Injectable()
|
||||
export class FilePreviewStateService {
|
||||
readonly fileData$: Observable<FileDataModel>;
|
||||
private readonly _fileData$ = new BehaviorSubject<FileDataModel>(undefined);
|
||||
|
||||
constructor() {
|
||||
this.fileData$ = this._fileData$.asObservable();
|
||||
}
|
||||
|
||||
get fileData(): FileDataModel {
|
||||
return this._fileData$.value;
|
||||
}
|
||||
|
||||
set fileData(fileDataModel: FileDataModel) {
|
||||
this._fileData$.next(fileDataModel);
|
||||
}
|
||||
}
|
||||
@ -3,13 +3,14 @@ import { forkJoin, Observable, of } from 'rxjs';
|
||||
import { catchError, map, tap } from 'rxjs/operators';
|
||||
import { FileDataModel } from '@models/file/file-data.model';
|
||||
import { PermissionsService } from '@services/permissions.service';
|
||||
import { File } from '@red/domain';
|
||||
import { File, IRedactionLog, IViewedPage } from '@red/domain';
|
||||
import { FileManagementService } from '@services/entity-services/file-management.service';
|
||||
import { RedactionLogService } from './redaction-log.service';
|
||||
import { ViewedPagesService } from '@services/entity-services/viewed-pages.service';
|
||||
import { AppStateService } from '../../../state/app-state.service';
|
||||
import { DossiersService } from '../../../services/entity-services/dossiers.service';
|
||||
import { UserPreferenceService } from '../../../services/user-preference.service';
|
||||
import { FilePreviewStateService } from '../screens/file-preview-screen/services/file-preview-state.service';
|
||||
|
||||
@Injectable()
|
||||
export class PdfViewerDataService {
|
||||
@ -21,6 +22,7 @@ export class PdfViewerDataService {
|
||||
private readonly _viewedPagesService: ViewedPagesService,
|
||||
private readonly _appStateService: AppStateService,
|
||||
private readonly _userPreferenceService: UserPreferenceService,
|
||||
private readonly _stateService: FilePreviewStateService,
|
||||
) {}
|
||||
|
||||
loadRedactionLogFor(dossierId: string, fileId: string) {
|
||||
@ -30,16 +32,17 @@ export class PdfViewerDataService {
|
||||
);
|
||||
}
|
||||
|
||||
loadDataFor(file: File, fileData?: FileDataModel): Observable<FileDataModel> {
|
||||
const file$ = fileData?.file.cacheIdentifier === file.cacheIdentifier ? of(fileData.fileData) : this.downloadOriginalFile(file);
|
||||
const reactionLog$ = this.loadRedactionLogFor(file.dossierId, file.fileId);
|
||||
loadDataFor(file: File): Observable<FileDataModel> {
|
||||
const fileData = this._stateService.fileData;
|
||||
const blob$ = fileData?.file.cacheIdentifier === file.cacheIdentifier ? of(fileData.blob$.value) : this.downloadOriginalFile(file);
|
||||
const redactionLog$ = this.loadRedactionLogFor(file.dossierId, file.fileId);
|
||||
const viewedPages$ = this.getViewedPagesFor(file);
|
||||
|
||||
const dossier = this._dossiersService.find(file.dossierId);
|
||||
|
||||
return forkJoin([file$, reactionLog$, viewedPages$]).pipe(
|
||||
return forkJoin([blob$, redactionLog$, viewedPages$]).pipe(
|
||||
map(
|
||||
data =>
|
||||
(data: [blob: Blob, redactionLog: IRedactionLog, viewedPages: IViewedPage[]]) =>
|
||||
new FileDataModel(
|
||||
file,
|
||||
...data,
|
||||
|
||||
@ -111,9 +111,9 @@ export class PdfViewerUtils {
|
||||
}
|
||||
}
|
||||
|
||||
translateQuads(page: number, quads: any) {
|
||||
translateQuad(page: number, quad: Core.Math.Quad) {
|
||||
const rotation = this._documentViewer.getCompleteRotation(page);
|
||||
return translateQuads(page, rotation, quads);
|
||||
return translateQuads(page, rotation, quad);
|
||||
}
|
||||
|
||||
deselectAllAnnotations() {
|
||||
|
||||
@ -5,7 +5,6 @@ import { filter, startWith } from 'rxjs/operators';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class FilesMapService {
|
||||
readonly fileReanalysed$ = new Subject<File>();
|
||||
private readonly _entityChanged$ = new Subject<File>();
|
||||
private readonly _entityDeleted$ = new Subject<File>();
|
||||
private readonly _map = new Map<string, BehaviorSubject<File[]>>();
|
||||
@ -38,7 +37,6 @@ export class FilesMapService {
|
||||
return entities.forEach(entity => this._entityChanged$.next(entity));
|
||||
}
|
||||
|
||||
const reanalysedEntities = [];
|
||||
const changedEntities = [];
|
||||
const deletedEntities = this.get(key).filter(oldEntity => !entities.find(newEntity => newEntity.id === oldEntity.id));
|
||||
|
||||
@ -46,10 +44,6 @@ export class FilesMapService {
|
||||
const newEntities = entities.map(newEntity => {
|
||||
const oldEntity = this.get(key, newEntity.id);
|
||||
|
||||
if (oldEntity?.lastProcessed !== newEntity.lastProcessed) {
|
||||
reanalysedEntities.push(newEntity);
|
||||
}
|
||||
|
||||
if (newEntity.isEqual(oldEntity)) {
|
||||
return oldEntity;
|
||||
}
|
||||
@ -62,10 +56,6 @@ export class FilesMapService {
|
||||
|
||||
// Emit observables only after entities have been updated
|
||||
|
||||
for (const file of reanalysedEntities) {
|
||||
this.fileReanalysed$.next(file);
|
||||
}
|
||||
|
||||
for (const file of changedEntities) {
|
||||
this._entityChanged$.next(file);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user