Merge branch 'master' into VM/RED-2614

This commit is contained in:
Valentin 2022-01-23 17:17:40 +02:00
commit daf90cba6f
27 changed files with 215 additions and 163 deletions

View File

@ -2,33 +2,45 @@ import { Dictionary, File, IRedactionLog, IRedactionLogEntry, IViewedPage, ViewM
import { AnnotationWrapper } from './annotation.wrapper'; import { AnnotationWrapper } from './annotation.wrapper';
import { RedactionLogEntryWrapper } from './redaction-log-entry.wrapper'; import { RedactionLogEntryWrapper } from './redaction-log-entry.wrapper';
import * as moment from 'moment'; import * as moment from 'moment';
import { BehaviorSubject } from 'rxjs';
export class FileDataModel { export class FileDataModel {
static readonly DELTA_VIEW_TIME = 10 * 60 * 1000; // 10 minutes; static readonly DELTA_VIEW_TIME = 10 * 60 * 1000; // 10 minutes;
hasChangeLog: boolean;
allAnnotations: AnnotationWrapper[]; allAnnotations: AnnotationWrapper[];
readonly hasChangeLog$ = new BehaviorSubject<boolean>(false);
readonly blob$ = new BehaviorSubject<Blob>(undefined);
readonly file$ = new BehaviorSubject<File>(undefined);
constructor( constructor(
public file: File, private readonly _file: File,
public fileData: Blob, private readonly _blob: Blob,
private _redactionLog: IRedactionLog, private _redactionLog: IRedactionLog,
public viewedPages?: IViewedPage[], public viewedPages?: IViewedPage[],
private _dictionaryData?: { [p: string]: Dictionary }, private _dictionaryData?: { [p: string]: Dictionary },
private _areDevFeaturesEnabled?: boolean, private _areDevFeaturesEnabled?: boolean,
) { ) {
this.file$.next(_file);
this.blob$.next(_blob);
this._buildAllAnnotations(); 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) { set redactionLog(redactionLog: IRedactionLog) {
this._redactionLog = redactionLog; this._redactionLog = redactionLog;
this._buildAllAnnotations(); this._buildAllAnnotations();
} }
get redactionLog() {
return this._redactionLog;
}
getVisibleAnnotations(viewMode: ViewMode) { getVisibleAnnotations(viewMode: ViewMode) {
return this.allAnnotations.filter(annotation => { return this.allAnnotations.filter(annotation => {
if (viewMode === 'STANDARD') { if (viewMode === 'STANDARD') {
@ -47,7 +59,7 @@ export class FileDataModel {
const previousAnnotations = this.allAnnotations || []; const previousAnnotations = this.allAnnotations || [];
this.allAnnotations = entries this.allAnnotations = entries
.map(entry => AnnotationWrapper.fromData(entry)) .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) { if (!this._areDevFeaturesEnabled) {
this.allAnnotations = this.allAnnotations.filter(annotation => !annotation.isFalsePositive); this.allAnnotations = this.allAnnotations.filter(annotation => !annotation.isFalsePositive);
@ -121,8 +133,8 @@ export class FileDataModel {
} }
private _isChangeLogEntry(redactionLogEntry: IRedactionLogEntry, wrapper: RedactionLogEntryWrapper) { private _isChangeLogEntry(redactionLogEntry: IRedactionLogEntry, wrapper: RedactionLogEntryWrapper) {
if (this.file.numberOfAnalyses > 1) { if (this._file.numberOfAnalyses > 1) {
var viableChanges = redactionLogEntry.changes.filter(c => c.analysisNumber > 1); const viableChanges = redactionLogEntry.changes.filter(c => c.analysisNumber > 1);
viableChanges.sort((a, b) => moment(a.dateTime).valueOf() - moment(b.dateTime).valueOf()); viableChanges.sort((a, b) => moment(a.dateTime).valueOf() - moment(b.dateTime).valueOf());
const lastChange = viableChanges.length >= 1 ? viableChanges[viableChanges.length - 1] : undefined; 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.changeLogType = relevantChanges[relevantChanges.length - 1].type;
wrapper.isChangeLogEntry = true; wrapper.isChangeLogEntry = true;
viewedPage.showAsUnseen = moment(viewedPage.viewedTime).valueOf() < moment(lastChange.dateTime).valueOf(); viewedPage.showAsUnseen = moment(viewedPage.viewedTime).valueOf() < moment(lastChange.dateTime).valueOf();
this.hasChangeLog = true; this.hasChangeLog$.next(true);
} else { } else {
// no relevant changes - hide removed anyway // no relevant changes - hide removed anyway
wrapper.isChangeLogEntry = false; wrapper.isChangeLogEntry = false;

View File

@ -6,7 +6,7 @@ import { BaseDialogComponent, shareDistinctLast, Toaster } from '@iqser/common-u
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { AppStateService } from '@state/app-state.service'; import { AppStateService } from '@state/app-state.service';
import { toKebabCase } from '@utils/functions'; import { toSnakeCase } from '@utils/functions';
import { DictionaryService } from '@shared/services/dictionary.service'; import { DictionaryService } from '@shared/services/dictionary.service';
import { Dictionary, IDictionary } from '@red/domain'; import { Dictionary, IDictionary } from '@red/domain';
import { UserService } from '@services/user.service'; import { UserService } from '@services/user.service';
@ -105,7 +105,7 @@ export class AddEditDictionaryDialogComponent extends BaseDialogComponent {
private _toTechnicalName(value: string) { private _toTechnicalName(value: string) {
const existingTechnicalNames = Object.keys(this._appStateService.dictionaryData[this._dossierTemplateId]); const existingTechnicalNames = Object.keys(this._appStateService.dictionaryData[this._dossierTemplateId]);
const baseTechnicalName = toKebabCase(value.trim()); const baseTechnicalName = toSnakeCase(value.trim());
let technicalName = baseTechnicalName; let technicalName = baseTechnicalName;
let suffix = 1; let suffix = 1;
while (existingTechnicalNames.includes(technicalName)) { while (existingTechnicalNames.includes(technicalName)) {

View File

@ -11,7 +11,7 @@
<ng-template #bulkActions> <ng-template #bulkActions>
<iqser-circle-button <iqser-circle-button
(action)="openConfirmDeleteDialog()" (action)="openConfirmDeleteDialog()"
*ngIf="userService.currentUser.isAdmin && listingService.areSomeSelected$ | async" *ngIf="userPreferenceService.areDevFeaturesEnabled && userService.currentUser.isAdmin && listingService.areSomeSelected$ | async"
[tooltip]="'justifications-listing.bulk.delete' | translate" [tooltip]="'justifications-listing.bulk.delete' | translate"
[type]="circleButtonTypes.dark" [type]="circleButtonTypes.dark"
icon="iqser:trash" icon="iqser:trash"
@ -26,7 +26,7 @@
<div class="table-header-actions"> <div class="table-header-actions">
<iqser-icon-button <iqser-icon-button
(action)="openAddJustificationDialog()" (action)="openAddJustificationDialog()"
*ngIf="userService.currentUser.isAdmin" *ngIf="userPreferenceService.areDevFeaturesEnabled && userService.currentUser.isAdmin"
[label]="'justifications-listing.add-new' | translate" [label]="'justifications-listing.add-new' | translate"
[type]="iconButtonTypes.primary" [type]="iconButtonTypes.primary"
icon="iqser:plus" icon="iqser:plus"

View File

@ -14,6 +14,7 @@ import { JustificationsService } from '@services/entity-services/justifications.
import { DossierTemplatesService } from '@services/entity-services/dossier-templates.service'; import { DossierTemplatesService } from '@services/entity-services/dossier-templates.service';
import { JustificationsDialogService } from '../justifications-dialog.service'; import { JustificationsDialogService } from '../justifications-dialog.service';
import { UserService } from '@services/user.service'; import { UserService } from '@services/user.service';
import { UserPreferenceService } from '@services/user-preference.service';
@Component({ @Component({
selector: 'redaction-justifications-screen', selector: 'redaction-justifications-screen',
@ -43,6 +44,7 @@ export class JustificationsScreenComponent extends ListingComponent<Justificatio
private readonly _loadingService: LoadingService, private readonly _loadingService: LoadingService,
private readonly _dossierTemplatesService: DossierTemplatesService, private readonly _dossierTemplatesService: DossierTemplatesService,
private readonly _dialogService: JustificationsDialogService, private readonly _dialogService: JustificationsDialogService,
readonly userPreferenceService: UserPreferenceService,
readonly userService: UserService, readonly userService: UserService,
) { ) {
super(_injector); super(_injector);

View File

@ -24,7 +24,7 @@
<iqser-circle-button <iqser-circle-button
(action)="openConfirmDeleteDialog()" (action)="openConfirmDeleteDialog()"
*ngIf="userService.currentUser.isAdmin" *ngIf="userPreferenceService.areDevFeaturesEnabled && userService.currentUser.isAdmin"
[tooltip]="'justifications-listing.actions.delete' | translate" [tooltip]="'justifications-listing.actions.delete' | translate"
[type]="circleButtonTypes.dark" [type]="circleButtonTypes.dark"
icon="iqser:trash" icon="iqser:trash"

View File

@ -3,6 +3,7 @@ import { Justification } from '@red/domain';
import { CircleButtonTypes, ListingService, LoadingService } from '@iqser/common-ui'; import { CircleButtonTypes, ListingService, LoadingService } from '@iqser/common-ui';
import { JustificationsDialogService } from '../justifications-dialog.service'; import { JustificationsDialogService } from '../justifications-dialog.service';
import { UserService } from '@services/user.service'; import { UserService } from '@services/user.service';
import { UserPreferenceService } from '@services/user-preference.service';
@Component({ @Component({
selector: 'redaction-table-item', selector: 'redaction-table-item',
@ -18,6 +19,7 @@ export class TableItemComponent {
private readonly _dialogService: JustificationsDialogService, private readonly _dialogService: JustificationsDialogService,
private readonly _loadingService: LoadingService, private readonly _loadingService: LoadingService,
private readonly _listingService: ListingService<Justification>, private readonly _listingService: ListingService<Justification>,
readonly userPreferenceService: UserPreferenceService,
readonly userService: UserService, readonly userService: UserService,
) {} ) {}

View File

@ -10,7 +10,6 @@ import { SharedModule } from '@shared/shared.module';
import { DossiersRoutingModule } from './dossiers-routing.module'; import { DossiersRoutingModule } from './dossiers-routing.module';
import { FileUploadDownloadModule } from '@upload-download/file-upload-download.module'; import { FileUploadDownloadModule } from '@upload-download/file-upload-download.module';
import { DossiersDialogService } from './services/dossiers-dialog.service'; import { DossiersDialogService } from './services/dossiers-dialog.service';
import { PdfViewerDataService } from './services/pdf-viewer-data.service';
import { ManualAnnotationService } from './services/manual-annotation.service'; import { ManualAnnotationService } from './services/manual-annotation.service';
import { AnnotationProcessingService } from './services/annotation-processing.service'; import { AnnotationProcessingService } from './services/annotation-processing.service';
import { EditDossierDialogComponent } from './dialogs/edit-dossier-dialog/edit-dossier-dialog.component'; import { EditDossierDialogComponent } from './dialogs/edit-dossier-dialog/edit-dossier-dialog.component';
@ -54,7 +53,7 @@ const components = [
...dialogs, ...dialogs,
]; ];
const services = [DossiersDialogService, ManualAnnotationService, PdfViewerDataService, AnnotationProcessingService]; const services = [DossiersDialogService, ManualAnnotationService, AnnotationProcessingService];
@NgModule({ @NgModule({
declarations: [...components], declarations: [...components],

View File

@ -5,10 +5,15 @@
[showCloseButton]="true" [showCloseButton]="true"
[viewModeSelection]="viewModeSelection" [viewModeSelection]="viewModeSelection"
> >
<redaction-file-download-btn [files]="entitiesService.all$ | async" tooltipPosition="below"></redaction-file-download-btn> <redaction-file-download-btn
[disabled]="listingService.areSomeSelected$ | async"
[files]="entitiesService.all$ | async"
tooltipPosition="below"
></redaction-file-download-btn>
<iqser-circle-button <iqser-circle-button
(action)="exportFilesAsCSV()" (action)="exportFilesAsCSV()"
[disabled]="listingService.areSomeSelected$ | async"
[tooltip]="'dossier-overview.header-actions.download-csv' | translate" [tooltip]="'dossier-overview.header-actions.download-csv' | translate"
icon="iqser:csv" icon="iqser:csv"
tooltipPosition="below" tooltipPosition="below"
@ -17,7 +22,8 @@
<iqser-circle-button <iqser-circle-button
(action)="reanalyseDossier()" (action)="reanalyseDossier()"
*ngIf="permissionsService.displayReanalyseBtn(dossier) && analysisForced" *ngIf="permissionsService.displayReanalyseBtn(dossier) && analysisForced"
[tooltipClass]="'small ' + ((listingService.areSomeSelected$ | async) ? '' : 'warn')" [disabled]="listingService.areSomeSelected$ | async"
[tooltipClass]="'small warn'"
[tooltip]="'dossier-overview.new-rule.toast.actions.reanalyse-all' | translate" [tooltip]="'dossier-overview.new-rule.toast.actions.reanalyse-all' | translate"
[type]="circleButtonTypes.warn" [type]="circleButtonTypes.warn"
icon="iqser:refresh" icon="iqser:refresh"

View File

@ -47,7 +47,7 @@ export class ScreenHeaderComponent implements OnInit {
) {} ) {}
ngOnInit() { ngOnInit() {
this.actionConfigs = this.configService.actionConfig(this.dossier.dossierId); this.actionConfigs = this.configService.actionConfig(this.dossier.dossierId, this.listingService.areSomeSelected$);
} }
async reanalyseDossier() { async reanalyseDossier() {

View File

@ -102,7 +102,7 @@ export class ConfigService {
}; };
} }
actionConfig(dossierId: string): List<ActionConfig> { actionConfig(dossierId: string, disabled$: Observable<boolean>): List<ActionConfig> {
return [ return [
{ {
label: this._translateService.instant('dossier-overview.header-actions.edit'), label: this._translateService.instant('dossier-overview.header-actions.edit'),
@ -110,6 +110,7 @@ export class ConfigService {
icon: 'iqser:edit', icon: 'iqser:edit',
hide: !this._userService.currentUser.isManager, hide: !this._userService.currentUser.isManager,
helpModeKey: 'edit-dossier-attributes', helpModeKey: 'edit-dossier-attributes',
disabled$,
}, },
]; ];
} }

View File

@ -41,7 +41,6 @@ export class AnnotationsListComponent implements OnChanges {
if (this.canMultiSelect && ($event.ctrlKey || $event.metaKey) && this.selectedAnnotations.length > 0) { if (this.canMultiSelect && ($event.ctrlKey || $event.metaKey) && this.selectedAnnotations.length > 0) {
this.multiSelectService.activate(); this.multiSelectService.activate();
} }
console.log('emit', annotation);
this.selectAnnotations.emit([annotation]); this.selectAnnotations.emit([annotation]);
} }
} }

View File

@ -96,7 +96,6 @@
[file]="file" [file]="file"
[number]="pageNumber" [number]="pageNumber"
[showDottedIcon]="hasOnlyManualRedactionsAndIsExcluded(pageNumber)" [showDottedIcon]="hasOnlyManualRedactionsAndIsExcluded(pageNumber)"
[viewedPages]="viewedPages"
></redaction-page-indicator> ></redaction-page-indicator>
</div> </div>

View File

@ -28,7 +28,7 @@ import { PermissionsService } from '@services/permissions.service';
import { WebViewerInstance } from '@pdftron/webviewer'; import { WebViewerInstance } from '@pdftron/webviewer';
import { BehaviorSubject, combineLatest, Observable } from 'rxjs'; import { BehaviorSubject, combineLatest, Observable } from 'rxjs';
import { map, tap } from 'rxjs/operators'; 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 { ExcludedPagesService } from '../../services/excluded-pages.service';
import { MultiSelectService } from '../../services/multi-select.service'; import { MultiSelectService } from '../../services/multi-select.service';
import { DocumentInfoService } from '../../services/document-info.service'; import { DocumentInfoService } from '../../services/document-info.service';
@ -52,7 +52,6 @@ export class FileWorkloadComponent {
@Input() activeViewerPage: number; @Input() activeViewerPage: number;
@Input() shouldDeselectAnnotationsOnPageChange: boolean; @Input() shouldDeselectAnnotationsOnPageChange: boolean;
@Input() dialogRef: MatDialogRef<unknown>; @Input() dialogRef: MatDialogRef<unknown>;
@Input() viewedPages: IViewedPage[];
@Input() file!: File; @Input() file!: File;
@Input() annotationActionsTemplate: TemplateRef<unknown>; @Input() annotationActionsTemplate: TemplateRef<unknown>;
@Input() viewer: WebViewerInstance; @Input() viewer: WebViewerInstance;
@ -225,7 +224,7 @@ export class FileWorkloadComponent {
scrollAnnotationsToPage(page: number, mode: 'always' | 'if-needed' = 'if-needed'): void { scrollAnnotationsToPage(page: number, mode: 'always' | 'if-needed' = 'if-needed'): void {
if (this._annotationsElement) { 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); FileWorkloadComponent._scrollToFirstElement(elements, mode);
} }
} }
@ -235,7 +234,7 @@ export class FileWorkloadComponent {
if (!this.selectedAnnotations || this.selectedAnnotations.length === 0 || !this._annotationsElement) { if (!this.selectedAnnotations || this.selectedAnnotations.length === 0 || !this._annotationsElement) {
return; return;
} }
const elements = this._annotationsElement.nativeElement.querySelectorAll( const elements: HTMLElement[] = this._annotationsElement.nativeElement.querySelectorAll(
`div[annotation-id="${this._firstSelectedAnnotation?.id}"].active`, `div[annotation-id="${this._firstSelectedAnnotation?.id}"].active`,
); );
FileWorkloadComponent._scrollToFirstElement(elements); FileWorkloadComponent._scrollToFirstElement(elements);
@ -412,7 +411,7 @@ export class FileWorkloadComponent {
private _scrollQuickNavigationToPage(page: number) { private _scrollQuickNavigationToPage(page: number) {
if (this._quickNavigationElement) { 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); FileWorkloadComponent._scrollToFirstElement(elements);
} }
} }

View File

@ -6,6 +6,7 @@ import { ViewedPagesService } from '@services/entity-services/viewed-pages.servi
import { File, IViewedPage } from '@red/domain'; import { File, IViewedPage } from '@red/domain';
import { AutoUnsubscribe } from '@iqser/common-ui'; import { AutoUnsubscribe } from '@iqser/common-ui';
import { FilesMapService } from '@services/entity-services/files-map.service'; import { FilesMapService } from '@services/entity-services/files-map.service';
import { FilePreviewStateService } from '../../services/file-preview-state.service';
@Component({ @Component({
selector: 'redaction-page-indicator', selector: 'redaction-page-indicator',
@ -18,7 +19,6 @@ export class PageIndicatorComponent extends AutoUnsubscribe implements OnDestroy
@Input() active = false; @Input() active = false;
@Input() showDottedIcon = false; @Input() showDottedIcon = false;
@Input() number: number; @Input() number: number;
@Input() viewedPages: IViewedPage[];
@Input() activeSelection = false; @Input() activeSelection = false;
@Output() readonly pageSelected = new EventEmitter<number>(); @Output() readonly pageSelected = new EventEmitter<number>();
@ -33,25 +33,17 @@ export class PageIndicatorComponent extends AutoUnsubscribe implements OnDestroy
private readonly _configService: ConfigService, private readonly _configService: ConfigService,
private readonly _changeDetectorRef: ChangeDetectorRef, private readonly _changeDetectorRef: ChangeDetectorRef,
private readonly _permissionService: PermissionsService, private readonly _permissionService: PermissionsService,
private readonly _stateService: FilePreviewStateService,
) { ) {
super(); super();
} }
get activePage() { get activePage() {
return this.viewedPages?.find(p => p.page === this.number); return this._viewedPages.find(p => p.page === this.number);
} }
private _setReadState() { private get _viewedPages(): IViewedPage[] {
const readBefore = this.read; return this._stateService.fileData?.viewedPages || [];
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();
} }
ngOnChanges() { 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() { private async _markPageRead() {
await this._viewedPagesService.addPage({ page: this.number }, this.file.dossierId, this.file.fileId).toPromise(); await this._viewedPagesService.addPage({ page: this.number }, this.file.dossierId, this.file.fileId).toPromise();
if (this.activePage) { if (this.activePage) {
this.activePage.showAsUnseen = false; this.activePage.showAsUnseen = false;
} else { } else {
this.viewedPages?.push({ page: this.number, fileId: this.file.fileId }); this._viewedPages.push({ page: this.number, fileId: this.file.fileId });
} }
this._setReadState(); this._setReadState();
} }
private async _markPageUnread() { private async _markPageUnread() {
await this._viewedPagesService.removePage(this.file.dossierId, this.file.fileId, this.number).toPromise(); await this._viewedPagesService.removePage(this.file.dossierId, this.file.fileId, this.number).toPromise();
this.viewedPages?.splice( this._viewedPages.splice(
this.viewedPages?.findIndex(p => p.page === this.number), this._viewedPages.findIndex(p => p.page === this.number),
1, 1,
); );
this._setReadState(); this._setReadState();

View File

@ -27,7 +27,7 @@ import { AnnotationActionsService } from '../../services/annotation-actions.serv
import { UserPreferenceService } from '@services/user-preference.service'; import { UserPreferenceService } from '@services/user-preference.service';
import { BASE_HREF } from '../../../../../../tokens'; import { BASE_HREF } from '../../../../../../tokens';
import { ConfigService } from '@services/config.service'; 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 { DossiersDialogService } from '../../../../services/dossiers-dialog.service';
import { loadCompareDocumentWrapper } from '../../../../utils/compare-mode.utils'; import { loadCompareDocumentWrapper } from '../../../../utils/compare-mode.utils';
import { PdfViewerUtils } from '../../../../utils/pdf-viewer.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 { toPosition } from '../../../../utils/pdf-calculation.utils';
import { ViewModeService } from '../../services/view-mode.service'; import { ViewModeService } from '../../services/view-mode.service';
import { MultiSelectService } from '../../services/multi-select.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 Tools = Core.Tools;
import TextTool = Tools.TextTool; import TextTool = Tools.TextTool;
import Annotation = Core.Annotations.Annotation; 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 = { const dataElements = {
ADD_REDACTION: 'add-redaction', ADD_REDACTION: 'add-redaction',
ADD_DICTIONARY: 'add-dictionary', ADD_DICTIONARY: 'add-dictionary',
@ -61,8 +63,7 @@ const dataElements = {
templateUrl: './pdf-viewer.component.html', templateUrl: './pdf-viewer.component.html',
styleUrls: ['./pdf-viewer.component.scss'], styleUrls: ['./pdf-viewer.component.scss'],
}) })
export class PdfViewerComponent implements OnInit, OnChanges { export class PdfViewerComponent extends AutoUnsubscribe implements OnInit, OnChanges {
@Input() fileData: Blob;
@Input() file: File; @Input() file: File;
@Input() dossier: Dossier; @Input() dossier: Dossier;
@Input() canPerformActions = false; @Input() canPerformActions = false;
@ -95,9 +96,12 @@ export class PdfViewerComponent implements OnInit, OnChanges {
private readonly _annotationActionsService: AnnotationActionsService, private readonly _annotationActionsService: AnnotationActionsService,
private readonly _configService: ConfigService, private readonly _configService: ConfigService,
private readonly _loadingService: LoadingService, private readonly _loadingService: LoadingService,
private readonly _stateService: FilePreviewStateService,
readonly viewModeService: ViewModeService, readonly viewModeService: ViewModeService,
readonly multiSelectService: MultiSelectService, readonly multiSelectService: MultiSelectService,
) {} ) {
super();
}
private get _toggleTooltipsBtnTitle(): string { private get _toggleTooltipsBtnTitle(): string {
return this._translateService.instant(_('pdf-viewer.toggle-tooltips'), { return this._translateService.instant(_('pdf-viewer.toggle-tooltips'), {
@ -115,7 +119,17 @@ export class PdfViewerComponent implements OnInit, OnChanges {
async ngOnInit() { async ngOnInit() {
this._setReadyAndInitialState = this._setReadyAndInitialState.bind(this); 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 { ngOnChanges(changes: SimpleChanges): void {
@ -123,16 +137,12 @@ export class PdfViewerComponent implements OnInit, OnChanges {
return; return;
} }
if (changes.fileData) {
this._loadDocument();
}
if (changes.canPerformActions) { if (changes.canPerformActions) {
this._handleCustomActions(); this._handleCustomActions();
} }
} }
uploadFile(files: any) { uploadFile(files: FileList) {
const fileToCompare = files[0]; const fileToCompare = files[0];
this.compareFileInput.nativeElement.value = null; this.compareFileInput.nativeElement.value = null;
@ -148,7 +158,7 @@ export class PdfViewerComponent implements OnInit, OnChanges {
await pdfNet.initialize(environment.licenseKey ? atob(environment.licenseKey) : null); await pdfNet.initialize(environment.licenseKey ? atob(environment.licenseKey) : null);
const compareDocument = await pdfNet.PDFDoc.createFromBuffer(fileReader.result as ArrayBuffer); 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 () => { const loadCompareDocument = async () => {
this._loadingService.start(); this._loadingService.start();
@ -201,7 +211,7 @@ export class PdfViewerComponent implements OnInit, OnChanges {
this.viewModeService.compareMode = false; this.viewModeService.compareMode = false;
const pdfNet = this.instance.Core.PDFNet; const pdfNet = this.instance.Core.PDFNet;
await pdfNet.initialize(environment.licenseKey ? atob(environment.licenseKey) : null); 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, { this.instance.UI.loadDocument(currentDocument, {
filename: this.file ? this.file.filename : 'document.pdf', filename: this.file ? this.file.filename : 'document.pdf',
}); });
@ -210,7 +220,7 @@ export class PdfViewerComponent implements OnInit, OnChanges {
this.utils.navigateToPage(1); this.utils.navigateToPage(1);
} }
async loadViewer() { private async _loadViewer() {
this.instance = await WebViewer( this.instance = await WebViewer(
{ {
licenseKey: environment.licenseKey ? atob(environment.licenseKey) : null, licenseKey: environment.licenseKey ? atob(environment.licenseKey) : null,
@ -219,7 +229,7 @@ export class PdfViewerComponent implements OnInit, OnChanges {
css: this._convertPath('/assets/pdftron/stylesheet.css'), css: this._convertPath('/assets/pdftron/stylesheet.css'),
backendType: 'ems', backendType: 'ems',
}, },
this.viewer.nativeElement, this.viewer.nativeElement as HTMLElement,
); );
this.documentViewer = this.instance.Core.documentViewer; this.documentViewer = this.instance.Core.documentViewer;
@ -231,7 +241,7 @@ export class PdfViewerComponent implements OnInit, OnChanges {
this.utils.disableHotkeys(); this.utils.disableHotkeys();
this._configureTextPopup(); 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)); this.annotationSelected.emit(this.annotationManager.getSelectedAnnotations().map(ann => ann.Id));
if (action === 'deselected') { if (action === 'deselected') {
this._toggleRectangleAnnotationAction(true); 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, // when a rectangle is drawn,
// it returns one annotation with tool name 'AnnotationCreateRectangle; // it returns one annotation with tool name 'AnnotationCreateRectangle;
// this will auto select rectangle after drawing // 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) { if (this.shouldDeselectAnnotationsOnPageChange) {
this.utils.deselectAllAnnotations(); this.utils.deselectAllAnnotations();
} }
@ -261,9 +271,9 @@ export class PdfViewerComponent implements OnInit, OnChanges {
this.documentViewer.addEventListener('documentLoaded', this._setReadyAndInitialState); this.documentViewer.addEventListener('documentLoaded', this._setReadyAndInitialState);
this.documentViewer.addEventListener('keyUp', $event => { this.documentViewer.addEventListener('keyUp', ($event: KeyboardEvent) => {
// arrows and full-screen // 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') { if ($event.key.startsWith('Arrow') || $event.key === 'f') {
this._ngZone.run(() => { this._ngZone.run(() => {
this.keyUp.emit($event); 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.preventDefault();
$event.stopPropagation(); $event.stopPropagation();
} }
@ -301,8 +311,6 @@ export class PdfViewerComponent implements OnInit, OnChanges {
} }
} }
}); });
this._loadDocument();
} }
private _setInitialDisplayMode() { private _setInitialDisplayMode() {
@ -568,7 +576,7 @@ export class PdfViewerComponent implements OnInit, OnChanges {
} }
private _addManualRedactionOfType(type: ManualRedactionEntryType) { 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 text = this.documentViewer.getSelectedText();
const manualRedaction = this._getManualRedaction(selectedQuads, text, true); const manualRedaction = this._getManualRedaction(selectedQuads, text, true);
this.manualAnnotationRequested.emit(new ManualRedactionEntryWrapper(selectedQuads, manualRedaction, type)); this.manualAnnotationRequested.emit(new ManualRedactionEntryWrapper(selectedQuads, manualRedaction, type));
@ -624,7 +632,7 @@ export class PdfViewerComponent implements OnInit, OnChanges {
for (const quad of quads[key]) { for (const quad of quads[key]) {
const page = parseInt(key, 10); const page = parseInt(key, 10);
const pageHeight = this.documentViewer.getPageHeight(page); 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() { private _loadDocument() {
if (!this.fileData) { this.instance.UI.loadDocument(this._stateService.fileData.blob$.value, {
return;
}
this.instance.UI.loadDocument(this.fileData, {
filename: this.file ? this.file.filename : 'document.pdf', filename: this.file ? this.file.filename : 'document.pdf',
}); });
} }
@ -647,7 +651,7 @@ export class PdfViewerComponent implements OnInit, OnChanges {
this._ngZone.run(() => { this._ngZone.run(() => {
this.utils.ready = true; this.utils.ready = true;
this.viewerReady.emit(this.instance); 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.pageChanged.emit(routePageNumber || 1);
this._setInitialDisplayMode(); this._setInitialDisplayMode();
this._updateTooltipsVisibility(); this._updateTooltipsVisibility();

View File

@ -1,5 +1,5 @@
<ng-container *ngIf="viewModeService.viewMode$ | async as viewMode"> <ng-container *ngIf="viewModeService.viewMode$ | async as viewMode">
<div <button
(click)="switchView.emit('STANDARD')" (click)="switchView.emit('STANDARD')"
[class.active]="viewModeService.isStandard" [class.active]="viewModeService.isStandard"
[matTooltip]="'file-preview.standard-tooltip' | translate" [matTooltip]="'file-preview.standard-tooltip' | translate"
@ -7,27 +7,27 @@
iqserHelpMode="standard-view" iqserHelpMode="standard-view"
> >
{{ 'file-preview.standard' | translate }} {{ 'file-preview.standard' | translate }}
</div> </button>
<div <button
(click)="canSwitchToDeltaView && switchView.emit('DELTA')" (click)="switchView.emit('DELTA')"
[class.active]="viewModeService.isDelta" [class.active]="viewModeService.isDelta"
[class.disabled]="!canSwitchToDeltaView" [disabled]="(canSwitchToDeltaView$ | async) === false"
[matTooltip]="'file-preview.delta-tooltip' | translate" [matTooltip]="'file-preview.delta-tooltip' | translate"
class="red-tab" class="red-tab"
iqserHelpMode="delta-view" iqserHelpMode="delta-view"
> >
{{ 'file-preview.delta' | translate }} {{ 'file-preview.delta' | translate }}
</div> </button>
<div <button
(click)="canSwitchToRedactedView && switchView.emit('REDACTED')" (click)="canSwitchToRedactedView && switchView.emit('REDACTED')"
[class.active]="viewModeService.isRedacted" [class.active]="viewModeService.isRedacted"
[class.disabled]="!canSwitchToRedactedView" [disabled]="!canSwitchToRedactedView"
[matTooltip]="'file-preview.redacted-tooltip' | translate" [matTooltip]="'file-preview.redacted-tooltip' | translate"
class="red-tab" class="red-tab"
iqserHelpMode="preview-view" iqserHelpMode="preview-view"
> >
{{ 'file-preview.redacted' | translate }} {{ 'file-preview.redacted' | translate }}
</div> </button>
</ng-container> </ng-container>

View File

@ -1,10 +1,12 @@
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core'; import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core';
import { File, ViewMode } from '@red/domain'; import { File, ViewMode } from '@red/domain';
import { ViewModeService } from '../../services/view-mode.service'; 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({ @Component({
selector: 'redaction-view-switch [file] [fileData]', selector: 'redaction-view-switch [file]',
templateUrl: './view-switch.component.html', templateUrl: './view-switch.component.html',
styleUrls: ['./view-switch.component.scss'], styleUrls: ['./view-switch.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
@ -12,19 +14,18 @@ import { FileDataModel } from '@models/file/file-data.model';
export class ViewSwitchComponent implements OnChanges { export class ViewSwitchComponent implements OnChanges {
@Output() readonly switchView = new EventEmitter<ViewMode>(); @Output() readonly switchView = new EventEmitter<ViewMode>();
@Input() file: File; @Input() file: File;
@Input() fileData: FileDataModel;
canSwitchToDeltaView = false; readonly canSwitchToDeltaView$: Observable<boolean>;
canSwitchToRedactedView = false; 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) { ngOnChanges(changes: SimpleChanges) {
if (changes.fileData) {
const fileData = changes.fileData.currentValue as FileDataModel;
this.canSwitchToDeltaView = fileData?.hasChangeLog;
}
if (changes.file) { if (changes.file) {
const file = changes?.file.currentValue as File; const file = changes?.file.currentValue as File;
this.canSwitchToRedactedView = !file.analysisRequired && !file.excluded; this.canSwitchToRedactedView = !file.analysisRequired && !file.excluded;

View File

@ -1,9 +1,9 @@
<ng-container *ngIf="dossier$ | async as dossier"> <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"> <section [class.fullscreen]="fullScreen">
<div class="page-header"> <div class="page-header">
<div class="flex flex-1"> <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>
<div class="flex-1 actions-container"> <div class="flex-1 actions-container">
@ -76,7 +76,6 @@
[canPerformActions]="canPerformAnnotationActions$ | async" [canPerformActions]="canPerformAnnotationActions$ | async"
[class.hidden]="!ready" [class.hidden]="!ready"
[dossier]="dossier" [dossier]="dossier"
[fileData]="fileData?.fileData"
[file]="file" [file]="file"
[shouldDeselectAnnotationsOnPageChange]="shouldDeselectAnnotationsOnPageChange" [shouldDeselectAnnotationsOnPageChange]="shouldDeselectAnnotationsOnPageChange"
></redaction-pdf-viewer> ></redaction-pdf-viewer>
@ -110,7 +109,6 @@
[dialogRef]="dialogRef" [dialogRef]="dialogRef"
[file]="file" [file]="file"
[selectedAnnotations]="selectedAnnotations" [selectedAnnotations]="selectedAnnotations"
[viewedPages]="fileData?.viewedPages"
[viewer]="activeViewer" [viewer]="activeViewer"
></redaction-file-workload> ></redaction-file-workload>
</div> </div>

View File

@ -14,13 +14,11 @@ import {
OnDetach, OnDetach,
processFilters, processFilters,
shareDistinctLast, shareDistinctLast,
shareLast,
} from '@iqser/common-ui'; } from '@iqser/common-ui';
import { MatDialogRef, MatDialogState } from '@angular/material/dialog'; import { MatDialogRef, MatDialogState } from '@angular/material/dialog';
import { ManualRedactionEntryWrapper } from '@models/file/manual-redaction-entry.wrapper'; import { ManualRedactionEntryWrapper } from '@models/file/manual-redaction-entry.wrapper';
import { AnnotationWrapper } from '@models/file/annotation.wrapper'; import { AnnotationWrapper } from '@models/file/annotation.wrapper';
import { ManualAnnotationResponse } from '@models/file/manual-annotation-response'; import { ManualAnnotationResponse } from '@models/file/manual-annotation-response';
import { FileDataModel } from '@models/file/file-data.model';
import { AnnotationDrawService } from './services/annotation-draw.service'; import { AnnotationDrawService } from './services/annotation-draw.service';
import { AnnotationProcessingService } from '../../services/annotation-processing.service'; import { AnnotationProcessingService } from '../../services/annotation-processing.service';
import { Dossier, File, ViewMode } from '@red/domain'; 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 { FilesService } from '@services/entity-services/files.service';
import { DossiersService } from '@services/entity-services/dossiers.service'; import { DossiersService } from '@services/entity-services/dossiers.service';
import { FileManagementService } from '@services/entity-services/file-management.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 { FilesMapService } from '@services/entity-services/files-map.service';
import { WatermarkService } from '@shared/services/watermark.service'; import { WatermarkService } from '@shared/services/watermark.service';
import { ExcludedPagesService } from './services/excluded-pages.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 { CommentingService } from './services/commenting.service';
import { SkippedService } from './services/skipped.service'; import { SkippedService } from './services/skipped.service';
import { AnnotationActionsService } from './services/annotation-actions.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 Annotation = Core.Annotations.Annotation;
import PDFNet = Core.PDFNet; import PDFNet = Core.PDFNet;
@ -67,6 +67,8 @@ const ALL_HOTKEY_ARRAY = ['Escape', 'F', 'f', 'ArrowUp', 'ArrowDown'];
SkippedService, SkippedService,
AnnotationDrawService, AnnotationDrawService,
AnnotationActionsService, AnnotationActionsService,
FilePreviewStateService,
PdfViewerDataService,
], ],
}) })
export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnInit, OnDestroy, OnAttach, OnDetach { export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnInit, OnDestroy, OnAttach, OnDetach {
@ -75,7 +77,6 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
dialogRef: MatDialogRef<unknown>; dialogRef: MatDialogRef<unknown>;
fullScreen = false; fullScreen = false;
shouldDeselectAnnotationsOnPageChange = true; shouldDeselectAnnotationsOnPageChange = true;
fileData: FileDataModel;
selectedAnnotations: AnnotationWrapper[] = []; selectedAnnotations: AnnotationWrapper[] = [];
displayPdfViewer = false; displayPdfViewer = false;
activeViewerPage: number = null; activeViewerPage: number = null;
@ -83,7 +84,6 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
readonly dossierId: string; readonly dossierId: string;
readonly canPerformAnnotationActions$: Observable<boolean>; readonly canPerformAnnotationActions$: Observable<boolean>;
readonly dossier$: Observable<Dossier>; readonly dossier$: Observable<Dossier>;
readonly file$: Observable<File>;
readonly fileId: string; readonly fileId: string;
ready = false; ready = false;
private _instance: WebViewerInstance; private _instance: WebViewerInstance;
@ -98,6 +98,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
constructor( constructor(
readonly permissionsService: PermissionsService, readonly permissionsService: PermissionsService,
readonly userPreferenceService: UserPreferenceService, readonly userPreferenceService: UserPreferenceService,
private readonly _stateService: FilePreviewStateService,
private readonly _watermarkService: WatermarkService, private readonly _watermarkService: WatermarkService,
private readonly _changeDetectorRef: ChangeDetectorRef, private readonly _changeDetectorRef: ChangeDetectorRef,
private readonly _activatedRoute: ActivatedRoute, private readonly _activatedRoute: ActivatedRoute,
@ -105,7 +106,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
private readonly _router: Router, private readonly _router: Router,
private readonly _annotationProcessingService: AnnotationProcessingService, private readonly _annotationProcessingService: AnnotationProcessingService,
private readonly _annotationDrawService: AnnotationDrawService, private readonly _annotationDrawService: AnnotationDrawService,
private readonly _fileDownloadService: PdfViewerDataService, private readonly _pdfViewerDataService: PdfViewerDataService,
private readonly _filesService: FilesService, private readonly _filesService: FilesService,
private readonly _ngZone: NgZone, private readonly _ngZone: NgZone,
private readonly _fileManagementService: FileManagementService, private readonly _fileManagementService: FileManagementService,
@ -126,12 +127,6 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
this.dossierId = _activatedRoute.snapshot.paramMap.get('dossierId'); this.dossierId = _activatedRoute.snapshot.paramMap.get('dossierId');
this.dossier$ = _dossiersService.getEntityChanged$(this.dossierId); this.dossier$ = _dossiersService.getEntityChanged$(this.dossierId);
this.fileId = _activatedRoute.snapshot.paramMap.get('fileId'); 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$; this.canPerformAnnotationActions$ = this._canPerformAnnotationActions$;
document.documentElement.addEventListener('fullscreenchange', () => { document.documentElement.addEventListener('fullscreenchange', () => {
@ -153,8 +148,16 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
return this._instance; return this._instance;
} }
get fileData(): FileDataModel {
return this._stateService.fileData;
}
private get _canPerformAnnotationActions$() { 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'), map(([file, viewMode]) => this.permissionsService.canPerformAnnotationActions(file) && viewMode === 'STANDARD'),
shareDistinctLast(), 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] Process time: ${new Date().getTime() - processStartTime} ms`);
console.log(`[REDACTION] Filter rebuild time: ${new Date().getTime() - startTime}`); console.log(`[REDACTION] Filter rebuild time: ${new Date().getTime() - startTime}`);
console.log();
} }
handleAnnotationSelected(annotationIds: string[]) { handleAnnotationSelected(annotationIds: string[]) {
// TODO: use includes() here
this.selectedAnnotations = annotationIds this.selectedAnnotations = annotationIds
.map(id => this.visibleAnnotations.find(annotationWrapper => annotationWrapper.id === id)) .map(id => this.visibleAnnotations.find(annotationWrapper => annotationWrapper.id === id))
.filter(ann => ann !== undefined); .filter(ann => ann !== undefined);
@ -469,23 +470,17 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
const previousFile = this.fileData?.file; const previousFile = this.fileData?.file;
await this._loadFileData(file); await this._loadFileData(file);
const fileHasBeenExcludedOrIncluded = previousFile?.excluded !== this.fileData?.file?.excluded; // file already loaded at least once
const excludedPagesHaveChanged = JSON.stringify(previousFile?.excludedPages) !== JSON.stringify(this.fileData?.file?.excludedPages); if (previousFile) {
if (fileHasBeenExcludedOrIncluded || excludedPagesHaveChanged) { // If it has been OCRd, we need to wait for it to load into the viewer
await this._deleteAnnotations(); if (previousFile.lastOCRTime !== this.fileData?.file?.lastOCRTime) {
await this._cleanupAndRedrawAnnotations(); return;
}
} }
await this._stampPDF();
} }
private async _stampPDF() { private async _stampPDF() {
if (!this._instance) { if (!this._instance?.Core.documentViewer.getDocument()) {
return;
}
const document = this._instance.Core.documentViewer.getDocument();
if (!document) {
return; return;
} }
@ -539,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 { private _subscribeToFileUpdates(): void {
this.addActiveScreenSubscription = this._filesMapService
.watch$(this.dossierId, this.fileId)
.pipe(switchMap(file => this._fileUpdated(file)))
.subscribe();
this.addActiveScreenSubscription = timer(0, 5000) this.addActiveScreenSubscription = timer(0, 5000)
.pipe(switchMap(() => this._filesService.reload(this.dossierId, this.fileId))) .pipe(switchMap(() => this._filesService.reload(this.dossierId, this.fileId)))
.subscribe(); .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 this.addActiveScreenSubscription = this._dossiersService
.getEntityDeleted$(this.dossierId) .getEntityDeleted$(this.dossierId)
@ -586,13 +589,13 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
return this._router.navigate([this._dossiersService.find(this.dossierId).routerLink]); 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) { if (file.isPending) {
return; return;
} }
this.fileData = fileData; this._stateService.fileData = fileData;
} }
@Debounce(0) @Debounce(0)
@ -616,13 +619,17 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
} }
const currentPageAnnotations = this.visibleAnnotations.filter(a => a.pageNumber === page); 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); this._deleteAnnotations(currentPageAnnotations);
await this._cleanupAndRedrawAnnotations(currentPageAnnotations, annotation => annotation.pageNumber === page); await this._cleanupAndRedrawAnnotations(currentPageAnnotations, annotation => annotation.pageNumber === page);
} }
private _deleteAnnotations(annotationsToDelete?: AnnotationWrapper[]) { private _deleteAnnotations(annotationsToDelete?: AnnotationWrapper[]) {
if (!this._instance?.Core.documentViewer.getDocument()) {
return;
}
if (!annotationsToDelete) { if (!annotationsToDelete) {
this._instance.Core.annotationManager.deleteAnnotations(this._instance.Core.annotationManager.getAnnotationsList(), { this._instance.Core.annotationManager.deleteAnnotations(this._instance.Core.annotationManager.getAnnotationsList(), {
imported: true, imported: true,
@ -638,6 +645,10 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
currentAnnotations?: AnnotationWrapper[], currentAnnotations?: AnnotationWrapper[],
newAnnotationsFilter?: (annotation: AnnotationWrapper) => boolean, newAnnotationsFilter?: (annotation: AnnotationWrapper) => boolean,
) { ) {
if (!this._instance?.Core.documentViewer.getDocument()) {
return;
}
this.rebuildFilters(); this.rebuildFilters();
if (this.viewModeService.viewMode === 'STANDARD') { if (this.viewModeService.viewMode === 'STANDARD') {

View File

@ -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);
}
}

View File

@ -3,13 +3,14 @@ import { forkJoin, Observable, of } from 'rxjs';
import { catchError, map, tap } from 'rxjs/operators'; import { catchError, map, tap } from 'rxjs/operators';
import { FileDataModel } from '@models/file/file-data.model'; import { FileDataModel } from '@models/file/file-data.model';
import { PermissionsService } from '@services/permissions.service'; 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 { FileManagementService } from '@services/entity-services/file-management.service';
import { RedactionLogService } from './redaction-log.service'; import { RedactionLogService } from './redaction-log.service';
import { ViewedPagesService } from '@services/entity-services/viewed-pages.service'; import { ViewedPagesService } from '@services/entity-services/viewed-pages.service';
import { AppStateService } from '../../../state/app-state.service'; import { AppStateService } from '../../../state/app-state.service';
import { DossiersService } from '../../../services/entity-services/dossiers.service'; import { DossiersService } from '../../../services/entity-services/dossiers.service';
import { UserPreferenceService } from '../../../services/user-preference.service'; import { UserPreferenceService } from '../../../services/user-preference.service';
import { FilePreviewStateService } from '../screens/file-preview-screen/services/file-preview-state.service';
@Injectable() @Injectable()
export class PdfViewerDataService { export class PdfViewerDataService {
@ -21,6 +22,7 @@ export class PdfViewerDataService {
private readonly _viewedPagesService: ViewedPagesService, private readonly _viewedPagesService: ViewedPagesService,
private readonly _appStateService: AppStateService, private readonly _appStateService: AppStateService,
private readonly _userPreferenceService: UserPreferenceService, private readonly _userPreferenceService: UserPreferenceService,
private readonly _stateService: FilePreviewStateService,
) {} ) {}
loadRedactionLogFor(dossierId: string, fileId: string) { loadRedactionLogFor(dossierId: string, fileId: string) {
@ -30,16 +32,17 @@ export class PdfViewerDataService {
); );
} }
loadDataFor(file: File, fileData?: FileDataModel): Observable<FileDataModel> { loadDataFor(file: File): Observable<FileDataModel> {
const file$ = fileData?.file.cacheIdentifier === file.cacheIdentifier ? of(fileData.fileData) : this.downloadOriginalFile(file); const fileData = this._stateService.fileData;
const reactionLog$ = this.loadRedactionLogFor(file.dossierId, file.fileId); 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 viewedPages$ = this.getViewedPagesFor(file);
const dossier = this._dossiersService.find(file.dossierId); const dossier = this._dossiersService.find(file.dossierId);
return forkJoin([file$, reactionLog$, viewedPages$]).pipe( return forkJoin([blob$, redactionLog$, viewedPages$]).pipe(
map( map(
data => (data: [blob: Blob, redactionLog: IRedactionLog, viewedPages: IViewedPage[]]) =>
new FileDataModel( new FileDataModel(
file, file,
...data, ...data,

View File

@ -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); const rotation = this._documentViewer.getCompleteRotation(page);
return translateQuads(page, rotation, quads); return translateQuads(page, rotation, quad);
} }
deselectAllAnnotations() { deselectAllAnnotations() {

View File

@ -5,7 +5,6 @@ import { filter, startWith } from 'rxjs/operators';
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
export class FilesMapService { export class FilesMapService {
readonly fileReanalysed$ = new Subject<File>();
private readonly _entityChanged$ = new Subject<File>(); private readonly _entityChanged$ = new Subject<File>();
private readonly _entityDeleted$ = new Subject<File>(); private readonly _entityDeleted$ = new Subject<File>();
private readonly _map = new Map<string, BehaviorSubject<File[]>>(); private readonly _map = new Map<string, BehaviorSubject<File[]>>();
@ -38,7 +37,6 @@ export class FilesMapService {
return entities.forEach(entity => this._entityChanged$.next(entity)); return entities.forEach(entity => this._entityChanged$.next(entity));
} }
const reanalysedEntities = [];
const changedEntities = []; const changedEntities = [];
const deletedEntities = this.get(key).filter(oldEntity => !entities.find(newEntity => newEntity.id === oldEntity.id)); 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 newEntities = entities.map(newEntity => {
const oldEntity = this.get(key, newEntity.id); const oldEntity = this.get(key, newEntity.id);
if (oldEntity?.lastProcessed !== newEntity.lastProcessed) {
reanalysedEntities.push(newEntity);
}
if (newEntity.isEqual(oldEntity)) { if (newEntity.isEqual(oldEntity)) {
return oldEntity; return oldEntity;
} }
@ -62,10 +56,6 @@ export class FilesMapService {
// Emit observables only after entities have been updated // Emit observables only after entities have been updated
for (const file of reanalysedEntities) {
this.fileReanalysed$.next(file);
}
for (const file of changedEntities) { for (const file of changedEntities) {
this._entityChanged$.next(file); this._entityChanged$.next(file);
} }

View File

@ -98,9 +98,9 @@ export function removeBraces(str: any): string {
return str.replace(/[{}]/g, ''); return str.replace(/[{}]/g, '');
} }
export function toKebabCase(str: string): string { export function toSnakeCase(str: string): string {
return str return str
.replace(/([a-z])([A-Z])/g, '$1-$2') .replace(/([a-z])([A-Z])/g, '$1-$2')
.replace(/[\s_]+/g, '-') .replace(/[\s_]+/g, '_')
.toLowerCase(); .toLowerCase();
} }

@ -1 +1 @@
Subproject commit 867d7b089ee3d10abf42bf6957c02f7f48ffdb7f Subproject commit 54d460682a995debb1bde96130e1b3689b0595de

View File

@ -1,6 +1,6 @@
{ {
"name": "redaction", "name": "redaction",
"version": "3.168.0", "version": "3.173.0",
"private": true, "private": true,
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {

Binary file not shown.