Pull request #345: Remove file from FileDataModel

Merge in RED/ui from refactor-fp/state to master

* commit '6c29461f18fa7d2e63b2abbc536c98b13b2289f7':
  cleanup
  fix lost async pipe
  fix excluded pages
  remove file from action requests
  remove file from fileData
  renaming and load file data flow
  fix missing fileData
  fix compile errors
  WIP
  Remove some inputs from components
This commit is contained in:
Dan Percic 2022-02-01 11:38:31 +01:00
commit d2e9fa12b3
32 changed files with 548 additions and 554 deletions

View File

@ -19,7 +19,6 @@ export class FileDataModel {
allAnnotations: AnnotationWrapper[];
readonly hasChangeLog$ = new BehaviorSubject<boolean>(false);
readonly blob$ = new BehaviorSubject<Blob>(undefined);
readonly file$ = new BehaviorSubject<File>(undefined);
constructor(
private readonly _file: File,
@ -29,19 +28,10 @@ export class FileDataModel {
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;
}

View File

@ -69,7 +69,7 @@
<div [class.required]="commentIsMandatory" class="iqser-input-group w-300">
<label translate="manual-annotation.dialog.content.comment"></label>
<textarea formControlName="comment" name="comment" redactionHasScrollbar rows="4" type="text"></textarea>
<textarea formControlName="comment" iqserHasScrollbar name="comment" rows="4" type="text"></textarea>
</div>
</div>
@ -80,5 +80,5 @@
</div>
</form>
<iqser-circle-button class="dialog-close" icon="iqser:close" (action)="close()"></iqser-circle-button>
<iqser-circle-button (action)="close()" class="dialog-close" icon="iqser:close"></iqser-circle-button>
</section>

View File

@ -4,10 +4,9 @@ import { AppStateService } from '@state/app-state.service';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { ManualRedactionEntryWrapper } from '@models/file/manual-redaction-entry.wrapper';
import { ManualAnnotationService } from '../../services/manual-annotation.service';
import { ManualAnnotationResponse } from '@models/file/manual-annotation-response';
import { PermissionsService } from '@services/permissions.service';
import { JustificationsService } from '@services/entity-services/justifications.service';
import { Dictionary, Dossier, File, IAddRedactionRequest, IManualAddResponse } from '@red/domain';
import { Dictionary, Dossier, IAddRedactionRequest } from '@red/domain';
import { DossiersService } from '@services/entity-services/dossiers.service';
import { DictionaryService } from '@shared/services/dictionary.service';
import { BaseDialogComponent } from '@iqser/common-ui';
@ -20,7 +19,6 @@ export interface LegalBasisOption {
}
@Component({
selector: 'redaction-manual-annotation-dialog',
templateUrl: './manual-annotation-dialog.component.html',
styleUrls: ['./manual-annotation-dialog.component.scss'],
})
@ -44,10 +42,10 @@ export class ManualAnnotationDialogComponent extends BaseDialogComponent impleme
private readonly _dictionaryService: DictionaryService,
protected readonly _injector: Injector,
protected readonly _dialogRef: MatDialogRef<ManualAnnotationDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: { manualRedactionEntryWrapper: ManualRedactionEntryWrapper; file: File },
@Inject(MAT_DIALOG_DATA) public data: { manualRedactionEntryWrapper: ManualRedactionEntryWrapper; dossierId: string },
) {
super(_injector, _dialogRef);
this._dossier = this._dossiersService.find(this.data.file.dossierId);
this._dossier = this._dossiersService.find(this.data.dossierId);
this.isDocumentAdmin = this._permissionsService.isApprover(this._dossier);
this.isFalsePositiveRequest = this.data.manualRedactionEntryWrapper.type === 'FALSE_POSITIVE';
@ -73,6 +71,10 @@ export class ManualAnnotationDialogComponent extends BaseDialogComponent impleme
return this.form.invalid;
}
get commentIsMandatory() {
return !this.isDocumentAdmin && !this.isDictionaryRequest;
}
async ngOnInit() {
super.ngOnInit();
this.possibleDictionaries = await this._appStateService.getDictionariesOptions(
@ -91,11 +93,7 @@ export class ManualAnnotationDialogComponent extends BaseDialogComponent impleme
save() {
this._enhanceManualRedaction(this.data.manualRedactionEntryWrapper.manualRedactionEntry);
this._manualAnnotationService.addAnnotation(this.data.manualRedactionEntryWrapper.manualRedactionEntry, this.data.file).subscribe(
(response: IManualAddResponse) =>
this._dialogRef.close(new ManualAnnotationResponse(this.data.manualRedactionEntryWrapper, response)),
() => this._dialogRef.close(),
);
this._dialogRef.close(this.data.manualRedactionEntryWrapper);
}
format(value: string) {
@ -118,10 +116,6 @@ export class ManualAnnotationDialogComponent extends BaseDialogComponent impleme
});
}
get commentIsMandatory() {
return !this.isDocumentAdmin && !this.isDictionaryRequest;
}
private _enhanceManualRedaction(addRedactionRequest: IAddRedactionRequest) {
const legalOption: LegalBasisOption = this.form.get('reason').value;
addRedactionRequest.type = this.form.get('dictionary').value;

View File

@ -32,7 +32,7 @@
></iqser-circle-button>
<iqser-circle-button
(action)="annotationActionsService.changeLegalBasis($event, annotations, file, annotationsChanged)"
(action)="annotationActionsService.changeLegalBasis($event, annotations, annotationsChanged)"
*ngIf="annotationPermissions.canChangeLegalBasis"
[tooltipPosition]="tooltipPosition"
[tooltip]="'annotation-actions.edit-reason.label' | translate"
@ -52,7 +52,7 @@
></iqser-circle-button>
<iqser-circle-button
(action)="annotationActionsService.acceptSuggestion($event, annotations, file, annotationsChanged)"
(action)="annotationActionsService.acceptSuggestion($event, annotations, annotationsChanged)"
*ngIf="annotationPermissions.canAcceptSuggestion"
[tooltipPosition]="tooltipPosition"
[tooltip]="'annotation-actions.accept-suggestion.label' | translate"
@ -61,7 +61,7 @@
></iqser-circle-button>
<iqser-circle-button
(action)="annotationActionsService.undoDirectAction($event, annotations, file, annotationsChanged)"
(action)="annotationActionsService.undoDirectAction($event, annotations, annotationsChanged)"
*ngIf="annotationPermissions.canUndo"
[tooltipPosition]="tooltipPosition"
[tooltip]="'annotation-actions.undo' | translate"
@ -70,7 +70,7 @@
></iqser-circle-button>
<iqser-circle-button
(action)="annotationActionsService.rejectSuggestion($event, annotations, file, annotationsChanged)"
(action)="annotationActionsService.rejectSuggestion($event, annotations, annotationsChanged)"
*ngIf="annotationPermissions.canRejectSuggestion"
[tooltipPosition]="tooltipPosition"
[tooltip]="'annotation-actions.reject-suggestion' | translate"
@ -79,7 +79,7 @@
></iqser-circle-button>
<iqser-circle-button
(action)="annotationActionsService.recategorizeImages($event, annotations, file, annotationsChanged)"
(action)="annotationActionsService.recategorizeImages($event, annotations, annotationsChanged)"
*ngIf="annotationPermissions.canRecategorizeImage"
[tooltipPosition]="tooltipPosition"
[tooltip]="'annotation-actions.recategorize-image' | translate"
@ -97,7 +97,7 @@
></iqser-circle-button>
<iqser-circle-button
(action)="annotationActionsService.forceAnnotation($event, annotations, file, annotationsChanged)"
(action)="annotationActionsService.forceAnnotation($event, annotations, annotationsChanged)"
*ngIf="annotationPermissions.canForceRedaction"
[tooltipPosition]="tooltipPosition"
[tooltip]="'annotation-actions.force-redaction.label' | translate"
@ -106,7 +106,7 @@
></iqser-circle-button>
<iqser-circle-button
(action)="annotationActionsService.forceAnnotation($event, annotations, file, annotationsChanged, true)"
(action)="annotationActionsService.forceAnnotation($event, annotations, annotationsChanged, true)"
*ngIf="annotationPermissions.canForceHint"
[tooltipPosition]="tooltipPosition"
[tooltip]="'annotation-actions.force-hint.label' | translate"

View File

@ -87,21 +87,15 @@ export class AnnotationActionsComponent implements OnChanges {
removeOrSuggestRemoveAnnotation($event: MouseEvent, removeFromDict: boolean) {
$event.stopPropagation();
this.annotationActionsService.removeOrSuggestRemoveAnnotation(
$event,
this.annotations,
this.file,
removeFromDict,
this.annotationsChanged,
);
this.annotationActionsService.removeOrSuggestRemoveAnnotation($event, this.annotations, removeFromDict, this.annotationsChanged);
}
markAsFalsePositive($event: MouseEvent) {
this.annotationActionsService.markAsFalsePositive($event, this.annotations, this.file, this.annotationsChanged);
this.annotationActionsService.markAsFalsePositive($event, this.annotations, this.annotationsChanged);
}
acceptRecommendation($event: MouseEvent) {
this.annotationActionsService.convertRecommendationToAnnotation($event, this.annotations, this.file, this.annotationsChanged);
this.annotationActionsService.convertRecommendationToAnnotation($event, this.annotations, this.annotationsChanged);
}
hideAnnotation($event: MouseEvent) {
@ -123,7 +117,7 @@ export class AnnotationActionsComponent implements OnChanges {
}
acceptResize($event: MouseEvent) {
this.annotationActionsService.acceptResize($event, this.viewer, this.file, this.annotations[0], this.annotationsChanged);
this.annotationActionsService.acceptResize($event, this.viewer, this.annotations[0], this.annotationsChanged);
}
cancelResize($event: MouseEvent) {

View File

@ -1,13 +1,13 @@
<div class="content-container">
<div *ngIf="references$ | async as references" class="content-container">
<div class="dialog references-dialog">
<div class="references-header flex">
<div class="small-label">
{{ annotationReferences.length }}
{{ (annotationReferences.length === 1 ? 'references.singular' : 'references.plural') | translate }}
{{ references.length }}
{{ (references.length === 1 ? 'references.singular' : 'references.plural') | translate }}
</div>
<iqser-circle-button (action)="annotationReferencesService.hide()" icon="iqser:close"></iqser-circle-button>
</div>
<div class="annotations-container flex">
<div *ngIf="annotationReferencesService.annotation$ | async as annotation" class="annotations-container flex">
<div [class.active]="isSelected(annotation.id)" class="annotation-container">
<div class="annotation-card-container flex">
<redaction-annotation-card [annotation]="annotation" [file]="file"></redaction-annotation-card>
@ -18,7 +18,7 @@
</div>
<div
(click)="referenceClicked.emit(reference)"
*ngFor="let reference of annotationReferences"
*ngFor="let reference of references"
[class.active]="isSelected(reference.id)"
class="annotation-container"
>

View File

@ -1,8 +1,10 @@
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, Output } from '@angular/core';
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
import { AnnotationWrapper } from '@models/file/annotation.wrapper';
import { AnnotationReferencesService } from '../../services/annotation-references.service';
import { File } from '@red/domain';
import { FileDataModel } from '@models/file/file-data.model';
import { FilePreviewStateService } from '../../services/file-preview-state.service';
import { combineLatest, Observable } from 'rxjs';
import { filter, map } from 'rxjs/operators';
@Component({
selector: 'redaction-annotation-references-list',
@ -10,18 +12,23 @@ import { FileDataModel } from '@models/file/file-data.model';
styleUrls: ['./annotation-references-list.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AnnotationReferencesListComponent implements OnChanges {
@Input() annotation: AnnotationWrapper;
export class AnnotationReferencesListComponent {
@Input() file: File;
@Input() fileData: FileDataModel;
@Input() selectedAnnotations: AnnotationWrapper[];
@Output() readonly referenceClicked = new EventEmitter<AnnotationWrapper>();
annotationReferences: AnnotationWrapper[];
references$ = this._annotationReferences;
constructor(readonly annotationReferencesService: AnnotationReferencesService) {}
constructor(
readonly annotationReferencesService: AnnotationReferencesService,
private readonly _filePreviewStateService: FilePreviewStateService,
) {}
ngOnChanges(): void {
this.annotationReferences = this.fileData.allAnnotations.filter(a => this.annotation.reference.includes(a.annotationId));
private get _annotationReferences(): Observable<AnnotationWrapper[]> {
const combination = combineLatest([this.annotationReferencesService.annotation$, this._filePreviewStateService.fileData$]);
return combination.pipe(
filter(([annotation]) => !!annotation),
map(([{ reference }, fileData]) => fileData.allAnnotations.filter(a => reference.includes(a.annotationId))),
);
}
isSelected(annotationId: string): boolean {

View File

@ -44,11 +44,9 @@
<redaction-annotation-details [annotation]="annotation" [isSelected]="isSelected(annotation.id)"></redaction-annotation-details>
</div>
<ng-container *ngIf="annotationReferencesService.annotation$ | async as annotation">
<ng-container *ngIf="annotationReferencesService.annotation$ | async">
<redaction-annotation-references-list
(referenceClicked)="referenceClicked($event)"
[annotation]="annotation"
[fileData]="fileData"
[file]="file"
[selectedAnnotations]="selectedAnnotations"
></redaction-annotation-references-list>

View File

@ -4,7 +4,6 @@ import { FilterService, HelpModeService, IqserEventTarget } from '@iqser/common-
import { File } from '@red/domain';
import { MultiSelectService } from '../../services/multi-select.service';
import { AnnotationReferencesService } from '../../services/annotation-references.service';
import { FileDataModel } from '../../../../../../models/file/file-data.model';
@Component({
selector: 'redaction-annotations-list',
@ -14,7 +13,6 @@ import { FileDataModel } from '../../../../../../models/file/file-data.model';
})
export class AnnotationsListComponent implements OnChanges {
@Input() file: File;
@Input() fileData: FileDataModel;
@Input() annotations: AnnotationWrapper[];
@Input() selectedAnnotations: AnnotationWrapper[];
@Input() annotationActionsTemplate: TemplateRef<unknown>;

View File

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

View File

@ -1,44 +1,40 @@
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
import { Dossier, File } from '@red/domain';
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { DossiersDialogService } from '../../../../services/dossiers-dialog.service';
import { AutoUnsubscribe } from '@iqser/common-ui';
import { DossierTemplatesService } from '@services/entity-services/dossier-templates.service';
import { DocumentInfoService } from '../../services/document-info.service';
import { Observable } from 'rxjs';
import { PermissionsService } from '@services/permissions.service';
import { combineLatest, Observable, switchMap } from 'rxjs';
import { PermissionsService } from '../../../../../../services/permissions.service';
import { FilePreviewStateService } from '../../services/file-preview-state.service';
import { map } from 'rxjs/operators';
import { File } from '@red/domain';
@Component({
selector: 'redaction-document-info [file] [dossier]',
selector: 'redaction-document-info',
templateUrl: './document-info.component.html',
styleUrls: ['./document-info.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class DocumentInfoComponent extends AutoUnsubscribe implements OnInit {
@Input() file: File;
@Input() dossier: Dossier;
fileAttributes$: Observable<{ label: string; value: string }[]>;
dossierTemplateName: string;
export class DocumentInfoComponent {
readonly fileAttributes$: Observable<{ label: string; value: string }[]>;
readonly dossierTemplateName$: Observable<string>;
constructor(
private readonly _dossierTemplatesService: DossierTemplatesService,
private readonly _dialogService: DossiersDialogService,
readonly stateService: FilePreviewStateService,
readonly permissionsService: PermissionsService,
readonly documentInfoService: DocumentInfoService,
) {
super();
}
ngOnInit(): void {
this.dossierTemplateName = this._dossierTemplatesService.find(this.dossier.dossierTemplateId).name;
this.fileAttributes$ = this.documentInfoService.fileAttributes$(
this.file.fileId,
this.file.dossierId,
this.dossier.dossierTemplateId,
this.fileAttributes$ = combineLatest([this.stateService.file$, this.stateService.dossier$]).pipe(
switchMap(([file, dossier]) => this.documentInfoService.fileAttributes$(file.fileId, dossier.id, dossier.dossierTemplateId)),
);
this.dossierTemplateName$ = this.stateService.dossier$.pipe(
switchMap(dossier => this._dossierTemplatesService.getEntityChanged$(dossier.dossierTemplateId)),
map(dossierTemplate => dossierTemplate.name),
);
}
edit() {
this._dialogService.openDialog('documentInfo', null, this.file);
edit(file: File) {
this._dialogService.openDialog('documentInfo', null, file);
}
}

View File

@ -203,7 +203,6 @@
[annotationActionsTemplate]="annotationActionsTemplate"
[annotations]="(displayedAnnotations$ | async)?.get(activeViewerPage)"
[canMultiSelect]="!isReadOnly"
[fileData]="fileData"
[file]="file"
[selectedAnnotations]="selectedAnnotations"
iqserHelpMode="workload-annotations-list"

View File

@ -33,7 +33,6 @@ import { ExcludedPagesService } from '../../services/excluded-pages.service';
import { MultiSelectService } from '../../services/multi-select.service';
import { DocumentInfoService } from '../../services/document-info.service';
import { SkippedService } from '../../services/skipped.service';
import { FileDataModel } from '../../../../../../models/file/file-data.model';
const COMMAND_KEY_ARRAY = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Escape'];
const ALL_HOTKEY_ARRAY = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'];
@ -56,7 +55,6 @@ export class FileWorkloadComponent {
@Input() file!: File;
@Input() annotationActionsTemplate: TemplateRef<unknown>;
@Input() viewer: WebViewerInstance;
@Input() fileData: FileDataModel;
@Output() readonly shouldDeselectAnnotationsOnPageChangeChange = new EventEmitter<boolean>();
@Output() readonly selectAnnotations = new EventEmitter<AnnotationWrapper[]>();
@Output() readonly deselectAnnotations = new EventEmitter<AnnotationWrapper[]>();

View File

@ -1,5 +1,5 @@
<div class="page">
<div #viewer [id]="file.fileId" class="viewer"></div>
<div #viewer [id]="(stateService.file$ | async).fileId" class="viewer"></div>
</div>
<input #compareFileInput (change)="uploadFile($event.target['files'])" class="file-upload-input" type="file" />

View File

@ -37,7 +37,7 @@ 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 { filter, switchMap, tap, withLatestFrom } from 'rxjs/operators';
import Tools = Core.Tools;
import TextTool = Tools.TextTool;
import Annotation = Core.Annotations.Annotation;
@ -64,7 +64,6 @@ const dataElements = {
styleUrls: ['./pdf-viewer.component.scss'],
})
export class PdfViewerComponent extends AutoUnsubscribe implements OnInit, OnChanges {
@Input() file: File;
@Input() dossier: Dossier;
@Input() canPerformActions = false;
@Input() annotations: AnnotationWrapper[];
@ -96,7 +95,7 @@ export class PdfViewerComponent extends AutoUnsubscribe implements OnInit, OnCha
private readonly _annotationActionsService: AnnotationActionsService,
private readonly _configService: ConfigService,
private readonly _loadingService: LoadingService,
private readonly _stateService: FilePreviewStateService,
readonly stateService: FilePreviewStateService,
readonly viewModeService: ViewModeService,
readonly multiSelectService: MultiSelectService,
) {
@ -121,24 +120,25 @@ export class PdfViewerComponent extends AutoUnsubscribe implements OnInit, OnCha
this._setReadyAndInitialState = this._setReadyAndInitialState.bind(this);
await this._loadViewer();
this.addActiveScreenSubscription = this._stateService.fileData$
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()),
withLatestFrom(this.stateService.file$),
tap(([blob, file]) => this._loadDocument(blob, file)),
)
.subscribe();
}
ngOnChanges(changes: SimpleChanges): void {
async ngOnChanges(changes: SimpleChanges): Promise<void> {
if (!this.instance) {
return;
}
if (changes.canPerformActions) {
this._handleCustomActions();
await this._handleCustomActions();
}
}
@ -158,18 +158,19 @@ export class PdfViewerComponent extends AutoUnsubscribe implements OnInit, OnCha
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._stateService.fileData.blob$.value.arrayBuffer());
const currentDocument = await pdfNet.PDFDoc.createFromBuffer(await this.stateService.fileData.blob$.value.arrayBuffer());
const loadCompareDocument = async () => {
this._loadingService.start();
this.utils.ready = false;
const mergedDocument = await pdfNet.PDFDoc.create();
const file = await this.stateService.file;
await loadCompareDocumentWrapper(
currentDocument,
compareDocument,
mergedDocument,
this.instance,
this.file,
file,
() => {
this.viewModeService.compareMode = true;
},
@ -211,10 +212,11 @@ export class PdfViewerComponent extends AutoUnsubscribe implements OnInit, OnCha
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._stateService.fileData.blob$.value.arrayBuffer());
this.instance.UI.loadDocument(currentDocument, {
filename: this.file ? this.file.filename : 'document.pdf',
});
const currentDocument = await pdfNet.PDFDoc.createFromBuffer(await this.stateService.fileData.blob$.value.arrayBuffer());
const filename = (await this.stateService.file).filename ?? 'document.pdf';
this.instance.UI.loadDocument(currentDocument, { filename });
this.instance.UI.disableElements([dataElements.CLOSE_COMPARE_BUTTON]);
this.instance.UI.enableElements([dataElements.COMPARE_BUTTON]);
this.utils.navigateToPage(1);
@ -239,7 +241,7 @@ export class PdfViewerComponent extends AutoUnsubscribe implements OnInit, OnCha
this._setSelectionMode();
this._configureElements();
this.utils.disableHotkeys();
this._configureTextPopup();
await this._configureTextPopup();
this.annotationManager.addEventListener('annotationSelected', (annotations: Annotation[], action) => {
this.annotationSelected.emit(this.annotationManager.getSelectedAnnotations().map(ann => ann.Id));
@ -266,7 +268,7 @@ export class PdfViewerComponent extends AutoUnsubscribe implements OnInit, OnCha
this.utils.deselectAllAnnotations();
}
this._ngZone.run(() => this.pageChanged.emit(pageNumber));
this._handleCustomActions();
return this._handleCustomActions();
});
this.documentViewer.addEventListener('documentLoaded', this._setReadyAndInitialState);
@ -289,11 +291,12 @@ export class PdfViewerComponent extends AutoUnsubscribe implements OnInit, OnCha
}
});
this.documentViewer.addEventListener('textSelected', (quads, selectedText) => {
this.documentViewer.addEventListener('textSelected', async (quads, selectedText) => {
this._selectedText = selectedText;
const textActions = [dataElements.ADD_DICTIONARY, dataElements.ADD_FALSE_POSITIVE];
if (selectedText.length > 2 && this.canPerformActions && !this.utils.isCurrentPageExcluded(this.file)) {
const file = await this.stateService.file;
if (selectedText.length > 2 && this.canPerformActions && !this.utils.isCurrentPageExcluded(file)) {
this.instance.UI.enableElements(textActions);
} else {
this.instance.UI.disableElements(textActions);
@ -480,9 +483,13 @@ export class PdfViewerComponent extends AutoUnsubscribe implements OnInit, OnCha
]);
}
this.instance.UI.annotationPopup.add(
this._annotationActionsService.getViewerAvailableActions(this.instance, this.file, annotationWrappers, this.annotationsChanged),
const actions = this._annotationActionsService.getViewerAvailableActions(
this.instance,
this.dossier,
annotationWrappers,
this.annotationsChanged,
);
this.instance.UI.annotationPopup.add(actions);
}
private _configureRectangleAnnotationPopup() {
@ -572,7 +579,7 @@ export class PdfViewerComponent extends AutoUnsubscribe implements OnInit, OnCha
},
]);
this._handleCustomActions();
return this._handleCustomActions();
}
private _addManualRedactionOfType(type: ManualRedactionEntryType) {
@ -582,7 +589,7 @@ export class PdfViewerComponent extends AutoUnsubscribe implements OnInit, OnCha
this.manualAnnotationRequested.emit(new ManualRedactionEntryWrapper(selectedQuads, manualRedaction, type));
}
private _handleCustomActions() {
private async _handleCustomActions() {
this.instance.UI.setToolMode('AnnotationEdit');
const { ANNOTATION_POPUP, ADD_RECTANGLE, ADD_REDACTION, SHAPE_TOOL_GROUP_BUTTON } = dataElements;
const elements = [
@ -594,7 +601,9 @@ export class PdfViewerComponent extends AutoUnsubscribe implements OnInit, OnCha
ANNOTATION_POPUP,
];
if (this.canPerformActions && !this.utils.isCurrentPageExcluded(this.file)) {
const isCurrentPageExcluded = this.utils.isCurrentPageExcluded(await this.stateService.file);
if (this.canPerformActions && !isCurrentPageExcluded) {
try {
this.instance.UI.enableTools(['AnnotationCreateRectangle']);
} catch (e) {
@ -611,7 +620,7 @@ export class PdfViewerComponent extends AutoUnsubscribe implements OnInit, OnCha
let elementsToDisable = [...elements, ADD_RECTANGLE];
if (this.utils.isCurrentPageExcluded(this.file)) {
if (isCurrentPageExcluded) {
const allowedActionsWhenPageExcluded: string[] = [ANNOTATION_POPUP, ADD_RECTANGLE, ADD_REDACTION, SHAPE_TOOL_GROUP_BUTTON];
elementsToDisable = elementsToDisable.filter(element => !allowedActionsWhenPageExcluded.includes(element));
} else {
@ -641,10 +650,8 @@ export class PdfViewerComponent extends AutoUnsubscribe implements OnInit, OnCha
return entry;
}
private _loadDocument() {
this.instance.UI.loadDocument(this._stateService.fileData.blob$.value, {
filename: this.file ? this.file.filename : 'document.pdf',
});
private _loadDocument(blob: Blob, file: File) {
this.instance.UI.loadDocument(blob, { filename: file?.filename ?? 'document.pdf' });
}
private _setReadyAndInitialState(): void {

View File

@ -1,47 +1,49 @@
<iqser-status-bar [configs]="statusBarConfigs" [small]="true"></iqser-status-bar>
<iqser-status-bar [configs]="statusBarConfig$ | async" [small]="true"></iqser-status-bar>
<div class="all-caps-label mr-16 ml-8">
{{ translations[file.workflowStatus] | translate }}
<span *ngIf="file.isUnderReview || file.isUnderApproval">{{ 'by' | translate }}:</span>
</div>
<ng-container *ngIf="stateService.file$ | async as file">
<div class="all-caps-label mr-16 ml-8">
{{ translations[file.workflowStatus] | translate }}
<span *ngIf="file.isUnderReview || file.isUnderApproval">{{ 'by' | translate }}:</span>
</div>
<redaction-initials-avatar
*ngIf="!editingReviewer"
[user]="file.assignee"
[withName]="!!file.assignee"
tooltipPosition="below"
></redaction-initials-avatar>
<div
(click)="editingReviewer = true"
*ngIf="!editingReviewer && canAssignReviewer"
class="assign-reviewer pointer"
translate="file-preview.assign-reviewer"
></div>
<redaction-assign-user-dropdown
(cancel)="editingReviewer = false"
(save)="assignReviewer(file, $event)"
*ngIf="editingReviewer"
[options]="usersOptions"
[value]="file.assignee"
></redaction-assign-user-dropdown>
<div *ngIf="!editingReviewer && canAssign" class="assign-actions-wrapper">
<iqser-circle-button
(action)="editingReviewer = true"
*ngIf="canAssignOrUnassign && !!file.assignee"
[tooltip]="assignTooltip"
icon="iqser:edit"
<redaction-initials-avatar
*ngIf="(editingReviewer$ | async) === false"
[user]="file.assignee"
[withName]="!!file.assignee"
tooltipPosition="below"
iqserHelpMode="assign-reviewer"
></iqser-circle-button>
></redaction-initials-avatar>
<iqser-circle-button
(action)="fileAssignService.assignToMe([file])"
*ngIf="canAssignToSelf"
[tooltip]="'file-preview.assign-me' | translate"
icon="red:assign-me"
tooltipPosition="below"
></iqser-circle-button>
</div>
<div
(click)="editingReviewer$.next(true)"
*ngIf="(editingReviewer$ | async) === false && (canAssignReviewer$ | async)"
class="assign-reviewer pointer"
translate="file-preview.assign-reviewer"
></div>
<redaction-assign-user-dropdown
(cancel)="editingReviewer$.next(false)"
(save)="assignReviewer(file, $event)"
*ngIf="editingReviewer$ | async"
[options]="usersOptions$ | async"
[value]="file.assignee"
></redaction-assign-user-dropdown>
<div *ngIf="(editingReviewer$ | async) === false && canAssign$ | async" class="assign-actions-wrapper">
<iqser-circle-button
(action)="editingReviewer$.next(true)"
*ngIf="(canAssignOrUnassign$ | async) && !!file.assignee"
[tooltip]="assignTooltip$ | async"
icon="iqser:edit"
iqserHelpMode="assign-reviewer"
tooltipPosition="below"
></iqser-circle-button>
<iqser-circle-button
(action)="fileAssignService.assignToMe([file])"
*ngIf="canAssignToSelf$ | async"
[tooltip]="'file-preview.assign-me' | translate"
icon="red:assign-me"
tooltipPosition="below"
></iqser-circle-button>
</div>
</ng-container>

View File

@ -1,4 +1,4 @@
import { ChangeDetectionStrategy, Component, Input, OnChanges } from '@angular/core';
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { Dossier, File, StatusBarConfigs, User } from '@red/domain';
import { List, LoadingService, Toaster } from '@iqser/common-ui';
import { PermissionsService } from '@services/permissions.service';
@ -8,7 +8,10 @@ import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { UserService } from '@services/user.service';
import { FilesService } from '@services/entity-services/files.service';
import { TranslateService } from '@ngx-translate/core';
import { firstValueFrom } from 'rxjs';
import { BehaviorSubject, combineLatest, firstValueFrom, Observable, switchMap } from 'rxjs';
import { FilePreviewStateService } from '../../services/file-preview-state.service';
import { distinctUntilChanged, map } from 'rxjs/operators';
import { DossiersService } from '@services/entity-services/dossiers.service';
@Component({
selector: 'redaction-user-management',
@ -16,22 +19,19 @@ import { firstValueFrom } from 'rxjs';
styleUrls: ['./user-management.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class UserManagementComponent implements OnChanges {
export class UserManagementComponent {
readonly translations = workflowFileStatusTranslations;
@Input() file: File;
@Input() dossier: Dossier;
editingReviewer = false;
statusBarConfigs: StatusBarConfigs;
canAssignToSelf = false;
canAssignUser = false;
canUnassignUser = false;
canAssign = false;
canAssignOrUnassign = false;
canAssignReviewer = false;
assignTooltip: string;
usersOptions: List;
readonly statusBarConfig$: Observable<StatusBarConfigs>;
readonly assignTooltip$: Observable<string>;
readonly canAssignReviewer$: Observable<boolean>;
readonly canAssignToSelf$: Observable<boolean>;
readonly editingReviewer$ = new BehaviorSubject<boolean>(false);
readonly canAssignOrUnassign$: Observable<boolean>;
readonly canAssign$: Observable<boolean>;
readonly usersOptions$: Observable<List>;
private readonly _dossier$: Observable<Dossier>;
private readonly _canAssignUser$: Observable<boolean>;
private readonly _canUnassignUser$: Observable<boolean>;
constructor(
readonly fileAssignService: FileAssignService,
@ -41,44 +41,55 @@ export class UserManagementComponent implements OnChanges {
readonly toaster: Toaster,
readonly loadingService: LoadingService,
readonly translateService: TranslateService,
) {}
readonly stateService: FilePreviewStateService,
private readonly _dossiersService: DossiersService,
) {
this._dossier$ = this.stateService.file$.pipe(switchMap(file => this._dossiersService.getEntityChanged$(file.dossierId)));
this.statusBarConfig$ = this.stateService.file$.pipe(map(file => [{ length: 1, color: file.workflowStatus }]));
this.assignTooltip$ = this.stateService.file$.pipe(
map(file =>
file.isUnderApproval
? this.translateService.instant(_('dossier-overview.assign-approver'))
: file.assignee
? this.translateService.instant(_('file-preview.change-reviewer'))
: this.translateService.instant(_('file-preview.assign-reviewer')),
),
);
private get _statusBarConfig(): StatusBarConfigs {
return [{ length: 1, color: this.file.workflowStatus }];
}
this.canAssignToSelf$ = this.stateService.file$.pipe(
map(file => this.permissionsService.canAssignToSelf(file)),
distinctUntilChanged(),
);
this._canAssignUser$ = this.stateService.file$.pipe(
map(file => this.permissionsService.canAssignUser(file)),
distinctUntilChanged(),
);
this._canUnassignUser$ = this.stateService.file$.pipe(
map(file => this.permissionsService.canUnassignUser(file)),
distinctUntilChanged(),
);
private get _assignOrChangeReviewerTooltip(): string {
return this.file.assignee
? this.translateService.instant(_('file-preview.change-reviewer'))
: this.translateService.instant(_('file-preview.assign-reviewer'));
}
this.canAssignOrUnassign$ = combineLatest([this._canAssignUser$, this._canUnassignUser$]).pipe(
map(([canAssignUser, canUnassignUser]) => canAssignUser || canUnassignUser),
distinctUntilChanged(),
);
private get _assignTooltip(): string {
return this.file.isUnderApproval
? this.translateService.instant(_('dossier-overview.assign-approver'))
: this._assignOrChangeReviewerTooltip;
}
this.canAssign$ = combineLatest([this.canAssignToSelf$, this.canAssignOrUnassign$]).pipe(
map(([canAssignToSelf, canAssignOrUnassign]) => canAssignToSelf || canAssignOrUnassign),
distinctUntilChanged(),
);
private get _canAssignReviewer(): boolean {
return !this.file.assignee && this.canAssignUser && this.dossier.hasReviewers;
}
this.canAssignReviewer$ = combineLatest([this.stateService.file$, this._canAssignUser$, this._dossier$]).pipe(
map(([file, canAssignUser, dossier]) => !file.assignee && canAssignUser && dossier.hasReviewers),
distinctUntilChanged(),
);
private get _usersOptions(): List {
const unassignUser = this.canUnassignUser ? [undefined] : [];
return this.file.isUnderApproval ? [...this.dossier.approverIds, ...unassignUser] : [...this.dossier.memberIds, ...unassignUser];
}
ngOnChanges() {
this.canAssignToSelf = this.permissionsService.canAssignToSelf(this.file);
this.canAssignUser = this.permissionsService.canAssignUser(this.file);
this.canUnassignUser = this.permissionsService.canUnassignUser(this.file);
this.canAssignOrUnassign = this.canAssignUser || this.canUnassignUser;
this.canAssign = this.canAssignToSelf || this.canAssignOrUnassign;
this.statusBarConfigs = this._statusBarConfig;
this.canAssignReviewer = this._canAssignReviewer;
this.assignTooltip = this._assignTooltip;
this.usersOptions = this._usersOptions;
this.usersOptions$ = combineLatest([this._canUnassignUser$, this.stateService.file$, this._dossier$]).pipe(
map(([canUnassignUser, file, dossier]) => {
const unassignUser = canUnassignUser ? [undefined] : [];
return file.isUnderApproval ? [...dossier.approverIds, ...unassignUser] : [...dossier.memberIds, ...unassignUser];
}),
);
}
async assignReviewer(file: File, user: User | string) {
@ -99,6 +110,6 @@ export class UserManagementComponent implements OnChanges {
this.loadingService.stop();
this.toaster.info(_('assignment.reviewer'), { params: { reviewerName, filename } });
this.editingReviewer = false;
this.editingReviewer$.next(false);
}
}

View File

@ -21,9 +21,9 @@
</button>
<button
(click)="canSwitchToRedactedView && switchView.emit('REDACTED')"
(click)="switchView.emit('REDACTED')"
[class.active]="viewModeService.isRedacted"
[disabled]="!canSwitchToRedactedView"
[disabled]="(canSwitchToRedactedView$ | async) === false"
[matTooltip]="'file-preview.redacted-tooltip' | translate"
class="red-tab"
iqserHelpMode="preview-view"

View File

@ -1,38 +1,32 @@
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core';
import { File, ViewMode } from '@red/domain';
import { ChangeDetectionStrategy, Component, EventEmitter, Output } from '@angular/core';
import { ViewMode } from '@red/domain';
import { ViewModeService } from '../../services/view-mode.service';
import { FilePreviewStateService } from '../../services/file-preview-state.service';
import { combineLatest, Observable } from 'rxjs';
import { filter, map, switchMap } from 'rxjs/operators';
@Component({
selector: 'redaction-view-switch [file]',
selector: 'redaction-view-switch',
templateUrl: './view-switch.component.html',
styleUrls: ['./view-switch.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ViewSwitchComponent implements OnChanges {
export class ViewSwitchComponent {
@Output() readonly switchView = new EventEmitter<ViewMode>();
@Input() file: File;
readonly canSwitchToDeltaView$: Observable<boolean>;
canSwitchToRedactedView = false;
readonly canSwitchToRedactedView$: Observable<boolean>;
constructor(readonly viewModeService: ViewModeService, private readonly _stateService: FilePreviewStateService) {
this.canSwitchToDeltaView$ = this._stateService.fileData$.pipe(
this.canSwitchToDeltaView$ = _stateService.fileData$.pipe(
filter(fileData => !!fileData),
switchMap(fileData =>
combineLatest([fileData.hasChangeLog$, fileData.file$]).pipe(
combineLatest([fileData.hasChangeLog$, _stateService.file$]).pipe(
map(([hasChangeLog, file]) => hasChangeLog && !file.isApproved),
),
),
);
}
ngOnChanges(changes: SimpleChanges) {
if (changes.file) {
const file = changes?.file.currentValue as File;
this.canSwitchToRedactedView = !file.analysisRequired && !file.excluded;
}
this.canSwitchToRedactedView$ = _stateService.file$.pipe(map(file => !file.analysisRequired && !file.excluded));
}
}

View File

@ -32,7 +32,7 @@
<div [class.required]="!isDocumentAdmin" class="iqser-input-group w-300">
<label translate="manual-annotation.dialog.content.comment"></label>
<textarea formControlName="comment" name="comment" redactionHasScrollbar rows="4" type="text"></textarea>
<textarea formControlName="comment" iqserHasScrollbar name="comment" rows="4" type="text"></textarea>
</div>
</div>

View File

@ -3,7 +3,7 @@ import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { AppStateService } from '@state/app-state.service';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { PermissionsService } from '@services/permissions.service';
import { Dictionary, Dossier, File } from '@red/domain';
import { Dictionary, Dossier } from '@red/domain';
import { DossiersService } from '@services/entity-services/dossiers.service';
import { BaseDialogComponent } from '@iqser/common-ui';
import { DictionaryService } from '@shared/services/dictionary.service';
@ -12,7 +12,7 @@ import { AnnotationWrapper } from '../../../../../../models/file/annotation.wrap
export interface AcceptRecommendationData {
readonly annotations: AnnotationWrapper[];
readonly file: File;
readonly dossierId: string;
}
export interface AcceptRecommendationReturnType {
@ -42,7 +42,7 @@ export class AcceptRecommendationDialogComponent extends BaseDialogComponent imp
@Inject(MAT_DIALOG_DATA) readonly data: AcceptRecommendationData,
) {
super(_injector, _dialogRef);
this._dossier = this._dossiersService.find(this.data.file.dossierId);
this._dossier = this._dossiersService.find(this.data.dossierId);
this.isDocumentAdmin = this._permissionsService.isApprover(this._dossier);
this.form = this._getForm();
this.initialFormValue = this.form.getRawValue();

View File

@ -0,0 +1,27 @@
import { ExcludedPagesService } from './services/excluded-pages.service';
import { ViewModeService } from './services/view-mode.service';
import { MultiSelectService } from './services/multi-select.service';
import { DocumentInfoService } from './services/document-info.service';
import { CommentingService } from './services/commenting.service';
import { SkippedService } from './services/skipped.service';
import { AnnotationDrawService } from './services/annotation-draw.service';
import { AnnotationActionsService } from './services/annotation-actions.service';
import { FilePreviewStateService } from './services/file-preview-state.service';
import { PdfViewerDataService } from '../../services/pdf-viewer-data.service';
import { AnnotationReferencesService } from './services/annotation-references.service';
import { FilterService } from '../../../../../../../../libs/common-ui/src';
export const filePreviewScreenProviders = [
FilterService,
ExcludedPagesService,
ViewModeService,
MultiSelectService,
DocumentInfoService,
CommentingService,
SkippedService,
AnnotationDrawService,
AnnotationActionsService,
FilePreviewStateService,
PdfViewerDataService,
AnnotationReferencesService,
];

View File

@ -1,9 +1,9 @@
<ng-container *ngIf="dossier$ | async as dossier">
<ng-container *ngIf="fileData?.file$ | async as file">
<ng-container *ngIf="stateService.dossier$ | async as dossier">
<ng-container *ngIf="stateService.file$ | async as file">
<section [class.fullscreen]="fullScreen">
<div class="page-header">
<div class="flex flex-1">
<redaction-view-switch (switchView)="switchView($event)" [file]="file"></redaction-view-switch>
<redaction-view-switch (switchView)="switchView($event)"></redaction-view-switch>
</div>
<div class="flex-1 actions-container">
@ -16,7 +16,7 @@
<mat-icon svgIcon="red:reanalyse"></mat-icon>
</div>
<redaction-user-management [dossier]="dossier" [file]="file"></redaction-user-management>
<redaction-user-management></redaction-user-management>
<ng-container *ngIf="permissionsService.isApprover(dossier) && !!file.lastReviewer">
<div class="vertical-line"></div>
@ -76,7 +76,6 @@
[canPerformActions]="canPerformAnnotationActions$ | async"
[class.hidden]="!ready"
[dossier]="dossier"
[file]="file"
[shouldDeselectAnnotationsOnPageChange]="shouldDeselectAnnotationsOnPageChange"
></redaction-pdf-viewer>
</div>
@ -89,11 +88,7 @@
icon="red:needs-work"
></iqser-empty-state>
<redaction-document-info
*ngIf="documentInfoService.shown$ | async"
[dossier]="dossier"
[file]="file"
></redaction-document-info>
<redaction-document-info *ngIf="documentInfoService.shown$ | async"></redaction-document-info>
<redaction-file-workload
#fileWorkloadComponent
@ -107,7 +102,6 @@
[annotationActionsTemplate]="annotationActionsTemplate"
[annotations]="visibleAnnotations"
[dialogRef]="dialogRef"
[fileData]="fileData"
[file]="file"
[selectedAnnotations]="selectedAnnotations"
[viewer]="activeViewer"

View File

@ -22,9 +22,9 @@ import { AnnotationWrapper } from '@models/file/annotation.wrapper';
import { ManualAnnotationResponse } from '@models/file/manual-annotation-response';
import { AnnotationDrawService } from './services/annotation-draw.service';
import { AnnotationProcessingService } from '../../services/annotation-processing.service';
import { Dossier, File, ViewMode } from '@red/domain';
import { File, ViewMode } from '@red/domain';
import { PermissionsService } from '@services/permissions.service';
import { combineLatest, Observable, timer } from 'rxjs';
import { combineLatest, firstValueFrom, Observable, of, timer } from 'rxjs';
import { UserPreferenceService } from '@services/user-preference.service';
import { PdfViewerDataService } from '../../services/pdf-viewer-data.service';
import { download } from '@utils/file-download-utils';
@ -36,7 +36,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 { map, switchMap, tap } from 'rxjs/operators';
import { catchError, filter, 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';
@ -45,12 +45,11 @@ import { MultiSelectService } from './services/multi-select.service';
import { DocumentInfoService } from './services/document-info.service';
import { ReanalysisService } from '../../../../services/reanalysis.service';
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 { AnnotationReferencesService } from './services/annotation-references.service';
import { filePreviewScreenProviders } from './file-preview-providers';
import { ManualAnnotationService } from '../../services/manual-annotation.service';
import Annotation = Core.Annotations.Annotation;
import PDFNet = Core.PDFNet;
@ -59,20 +58,7 @@ const ALL_HOTKEY_ARRAY = ['Escape', 'F', 'f', 'ArrowUp', 'ArrowDown'];
@Component({
templateUrl: './file-preview-screen.component.html',
styleUrls: ['./file-preview-screen.component.scss'],
providers: [
FilterService,
ExcludedPagesService,
ViewModeService,
MultiSelectService,
DocumentInfoService,
CommentingService,
SkippedService,
AnnotationDrawService,
AnnotationActionsService,
FilePreviewStateService,
PdfViewerDataService,
AnnotationReferencesService,
],
providers: filePreviewScreenProviders,
})
export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnInit, OnDestroy, OnAttach, OnDetach {
readonly circleButtonTypes = CircleButtonTypes;
@ -84,10 +70,9 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
displayPdfViewer = false;
activeViewerPage: number = null;
@ViewChild(PdfViewerComponent) readonly viewerComponent: PdfViewerComponent;
readonly dossierId: string;
readonly canPerformAnnotationActions$: Observable<boolean>;
readonly dossier$: Observable<Dossier>;
readonly fileId: string;
readonly fileId = this.stateService.fileId;
readonly dossierId = this.stateService.dossierId;
ready = false;
private _instance: WebViewerInstance;
private _lastPage: string;
@ -102,7 +87,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
constructor(
readonly permissionsService: PermissionsService,
readonly userPreferenceService: UserPreferenceService,
private readonly _stateService: FilePreviewStateService,
readonly stateService: FilePreviewStateService,
private readonly _watermarkService: WatermarkService,
private readonly _changeDetectorRef: ChangeDetectorRef,
private readonly _activatedRoute: ActivatedRoute,
@ -122,15 +107,13 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
private readonly _reanalysisService: ReanalysisService,
private readonly _errorService: ErrorService,
private readonly _skippedService: SkippedService,
private readonly _manualAnnotationService: ManualAnnotationService,
readonly excludedPagesService: ExcludedPagesService,
readonly viewModeService: ViewModeService,
readonly multiSelectService: MultiSelectService,
readonly documentInfoService: DocumentInfoService,
) {
super();
this.dossierId = _activatedRoute.snapshot.paramMap.get('dossierId');
this.dossier$ = _dossiersService.getEntityChanged$(this.dossierId);
this.fileId = _activatedRoute.snapshot.paramMap.get('fileId');
this.canPerformAnnotationActions$ = this._canPerformAnnotationActions$;
document.documentElement.addEventListener('fullscreenchange', () => {
@ -141,34 +124,30 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
}
get visibleAnnotations(): AnnotationWrapper[] {
return this.fileData ? this.fileData.getVisibleAnnotations(this.viewModeService.viewMode) : [];
return this._fileData ? this._fileData.getVisibleAnnotations(this.viewModeService.viewMode) : [];
}
get allAnnotations(): AnnotationWrapper[] {
return this.fileData ? this.fileData.allAnnotations : [];
return this._fileData ? this._fileData.allAnnotations : [];
}
get activeViewer(): WebViewerInstance {
return this._instance;
}
get fileData(): FileDataModel {
return this._stateService.fileData;
private get _fileData(): FileDataModel {
return this.stateService.fileData;
}
private get _canPerformAnnotationActions$() {
return combineLatest([
this._stateService.fileData$.pipe(switchMap(fileData => fileData.file$)),
this.viewModeService.viewMode$,
this.viewModeService.compareMode$,
]).pipe(
return combineLatest([this.stateService.file$, this.viewModeService.viewMode$, this.viewModeService.compareMode$]).pipe(
map(([file, viewMode]) => this.permissionsService.canPerformAnnotationActions(file) && viewMode === 'STANDARD'),
shareDistinctLast(),
);
}
async updateViewMode(): Promise<void> {
const ocrAnnotationIds = this.fileData.allAnnotations.filter(a => a.isOCR).map(a => a.id);
const ocrAnnotationIds = this._fileData.allAnnotations.filter(a => a.isOCR).map(a => a.id);
const annotations = this._getAnnotations(a => a.getCustomData('redact-manager'));
const redactions = annotations.filter(a => a.getCustomData('redaction'));
@ -211,7 +190,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
}
async ngOnAttach(previousRoute: ActivatedRouteSnapshot): Promise<boolean> {
const file = this._filesMapService.get(this.dossierId, this.fileId);
const file = await this.stateService.file;
if (!file.canBeOpened) {
return this._router.navigate([this._dossiersService.find(this.dossierId)?.routerLink]);
}
@ -228,9 +207,10 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
await this.userPreferenceService.saveLastOpenedFileForDossier(this.dossierId, this.fileId);
this._subscribeToFileUpdates();
const file = this._filesMapService.get(this.dossierId, this.fileId);
const file = await this.stateService.file;
if (file?.analysisRequired) {
await this._reanalysisService.reanalyzeFilesForDossier([this.fileId], this.dossierId, true).toPromise();
const reanalyzeFiles = this._reanalysisService.reanalyzeFilesForDossier([this.fileId], this.dossierId, true);
await firstValueFrom(reanalyzeFiles);
}
this.displayPdfViewer = true;
@ -263,7 +243,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
this._filterService.addFilterGroup({
slug: 'secondaryFilters',
filterTemplate: this._filterTemplate,
filters: processFilters(secondaryFilters, AnnotationProcessingService.secondaryAnnotationFilters(this.fileData?.viewedPages)),
filters: processFilters(secondaryFilters, AnnotationProcessingService.secondaryAnnotationFilters(this._fileData?.viewedPages)),
});
console.log(`[REDACTION] Process time: ${new Date().getTime() - processStartTime} ms`);
console.log(`[REDACTION] Filter rebuild time: ${new Date().getTime() - startTime}`);
@ -301,18 +281,22 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
openManualAnnotationDialog(manualRedactionEntryWrapper: ManualRedactionEntryWrapper) {
this._ngZone.run(() => {
const file = this._filesMapService.get(this.dossierId, this.fileId);
this.dialogRef = this._dialogService.openDialog(
'manualAnnotation',
null,
{ manualRedactionEntryWrapper, file },
async (response: ManualAnnotationResponse) => {
{ manualRedactionEntryWrapper, dossierId: this.dossierId },
async (entryWrapper: ManualRedactionEntryWrapper) => {
const addAnnotation$ = this._manualAnnotationService
.addAnnotation(entryWrapper.manualRedactionEntry, this.dossierId, this.fileId)
.pipe(catchError(() => of(undefined)));
const addAnnotationResponse = await firstValueFrom(addAnnotation$);
const response = new ManualAnnotationResponse(entryWrapper, addAnnotationResponse);
if (response?.annotationId) {
const annotation = this._instance.Core.annotationManager.getAnnotationById(
response.manualRedactionEntryWrapper.rectId,
);
this._instance.Core.annotationManager.deleteAnnotation(annotation);
// await this._filesService.reload(this.dossierId, this.fileId).toPromise();
const distinctPages = manualRedactionEntryWrapper.manualRedactionEntry.positions
.map(p => p.page)
.filter((item, pos, self) => self.indexOf(item) === pos);
@ -470,19 +454,6 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
}, 100);
}
private async _reloadFile(file: File): Promise<void> {
const previousFile = this.fileData?.file;
await this._loadFileData(file);
// file already loaded at least once
if (previousFile) {
// If it has been OCRd, we need to wait for it to load into the viewer
if (previousFile.lastOCRTime !== this.fileData?.file?.lastOCRTime) {
return;
}
}
}
private async _stampPDF() {
if (!this._instance?.Core.documentViewer.getDocument()) {
return;
@ -538,23 +509,19 @@ 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 async _fileUpdated(file: File): Promise<void> {
await this._loadFileData(file);
await this._reloadAnnotations();
await this._stampPDF();
}
private _subscribeToFileUpdates(): void {
this.addActiveScreenSubscription = this._filesMapService
.watch$(this.dossierId, this.fileId)
.pipe(switchMap(file => this._fileUpdated(file)))
.pipe(
filter(f => !!f),
switchMap(file => this._fileUpdated(file)),
)
.subscribe();
this.addActiveScreenSubscription = timer(0, 5000)
@ -593,13 +560,11 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
return this._router.navigate([this._dossiersService.find(this.dossierId).routerLink]);
}
const fileData = await this._pdfViewerDataService.loadDataFor(file).toPromise();
if (file.isPending) {
return;
}
this._stateService.fileData = fileData;
this.stateService.fileData = await firstValueFrom(this._pdfViewerDataService.loadDataFor(file));
}
@Debounce(0)
@ -608,25 +573,24 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
this._workloadComponent?.scrollAnnotations();
}
private async _reloadAnnotations(previousAnnotations?: AnnotationWrapper[]) {
private async _reloadAnnotations() {
this._deleteAnnotations();
await this._cleanupAndRedrawAnnotations(previousAnnotations);
await this._cleanupAndRedrawAnnotations();
}
private async _reloadAnnotationsForPage(page: number) {
this.fileData.file = await this._filesService.reload(this.dossierId, this.fileId).toPromise();
const file = await firstValueFrom(this._filesService.reload(this.dossierId, this.fileId));
// if this action triggered a re-processing,
// we don't want to redraw for this page since they will get redrawn as soon as processing ends;
if (this.fileData.file.isProcessing) {
if (file.isProcessing) {
return;
}
const currentPageAnnotations = this.visibleAnnotations.filter(a => a.pageNumber === page);
this.fileData.redactionLog = await this._pdfViewerDataService.loadRedactionLogFor(this.dossierId, this.fileId).toPromise();
this._fileData.redactionLog = await firstValueFrom(this._pdfViewerDataService.loadRedactionLogFor(this.dossierId, this.fileId));
this._deleteAnnotations(currentPageAnnotations);
await this._cleanupAndRedrawAnnotations(currentPageAnnotations, annotation => annotation.pageNumber === page);
await this._cleanupAndRedrawAnnotations(annotation => annotation.pageNumber === page);
}
private _deleteAnnotations(annotationsToDelete?: AnnotationWrapper[]) {
@ -645,10 +609,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
});
}
private async _cleanupAndRedrawAnnotations(
currentAnnotations?: AnnotationWrapper[],
newAnnotationsFilter?: (annotation: AnnotationWrapper) => boolean,
) {
private async _cleanupAndRedrawAnnotations(newAnnotationsFilter?: (annotation: AnnotationWrapper) => boolean) {
if (!this._instance?.Core.documentViewer.getDocument()) {
return;
}
@ -656,17 +617,18 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
const currentFilters = this._filterService.getGroup('primaryFilters')?.filters || [];
this.rebuildFilters();
if (this.viewModeService.viewMode === 'STANDARD') {
const startTime = new Date().getTime();
const newAnnotations = newAnnotationsFilter ? this.visibleAnnotations.filter(newAnnotationsFilter) : this.visibleAnnotations;
if (currentFilters) {
this._handleDeltaAnnotationFilters(currentFilters, this.visibleAnnotations);
}
await this._redrawAnnotations(newAnnotations);
console.log(
`[REDACTION] Annotations redraw time: ${new Date().getTime() - startTime} ms for ${newAnnotations.length} annotations`,
);
if (!this.viewModeService.isStandard) {
return;
}
const startTime = new Date().getTime();
const newAnnotations = newAnnotationsFilter ? this.visibleAnnotations.filter(newAnnotationsFilter) : this.visibleAnnotations;
if (currentFilters) {
this._handleDeltaAnnotationFilters(currentFilters, this.visibleAnnotations);
}
await this._redrawAnnotations(newAnnotations);
console.log(`[REDACTION] Annotations redraw time: ${new Date().getTime() - startTime} ms for ${newAnnotations.length} annotations`);
}
private _redrawAnnotations(annotations = this.allAnnotations) {

View File

@ -10,7 +10,7 @@ import { DossiersDialogService } from '../../../services/dossiers-dialog.service
import { BASE_HREF } from '../../../../../tokens';
import { UserService } from '@services/user.service';
import { Core, WebViewerInstance } from '@pdftron/webviewer';
import { Dossier, File, IAddRedactionRequest, ILegalBasisChangeRequest, IRectangle, IResizeRequest } from '@red/domain';
import { Dossier, IAddRedactionRequest, ILegalBasisChangeRequest, IRectangle, IResizeRequest } from '@red/domain';
import { toPosition } from '../../../utils/pdf-calculation.utils';
import { AnnotationDrawService } from './annotation-draw.service';
import { translateQuads } from '@utils/pdf-coordinates';
@ -23,6 +23,7 @@ import {
import { defaultDialogConfig } from '@iqser/common-ui';
import { filter } from 'rxjs/operators';
import { MatDialog } from '@angular/material/dialog';
import { FilePreviewStateService } from './file-preview-state.service';
import Annotation = Core.Annotations.Annotation;
@Injectable()
@ -38,44 +39,45 @@ export class AnnotationActionsService {
private readonly _dialog: MatDialog,
private readonly _annotationDrawService: AnnotationDrawService,
private readonly _dossiersService: DossiersService,
private readonly _screenStateService: FilePreviewStateService,
) {}
acceptSuggestion(
$event: MouseEvent,
annotations: AnnotationWrapper[],
file: File,
annotationsChanged: EventEmitter<AnnotationWrapper>,
) {
private get _dossier(): Dossier {
return this._dossiersService.find(this._screenStateService.dossierId);
}
acceptSuggestion($event: MouseEvent, annotations: AnnotationWrapper[], annotationsChanged: EventEmitter<AnnotationWrapper>) {
$event?.stopPropagation();
const { dossierId, fileId } = this._screenStateService;
annotations.forEach(annotation => {
this._processObsAndEmit(
this._manualAnnotationService.approve(annotation.id, file, annotation.isModifyDictionary),
this._manualAnnotationService.approve(annotation.id, dossierId, fileId, annotation.isModifyDictionary),
annotation,
annotationsChanged,
);
});
}
rejectSuggestion(
$event: MouseEvent,
annotations: AnnotationWrapper[],
file: File,
annotationsChanged: EventEmitter<AnnotationWrapper>,
) {
rejectSuggestion($event: MouseEvent, annotations: AnnotationWrapper[], annotationsChanged: EventEmitter<AnnotationWrapper>) {
$event?.stopPropagation();
const { dossierId, fileId } = this._screenStateService;
annotations.forEach(annotation => {
this._processObsAndEmit(this._manualAnnotationService.declineOrRemoveRequest(annotation, file), annotation, annotationsChanged);
this._processObsAndEmit(
this._manualAnnotationService.declineOrRemoveRequest(annotation, dossierId, fileId),
annotation,
annotationsChanged,
);
});
}
forceAnnotation(
$event: MouseEvent,
annotations: AnnotationWrapper[],
file: File,
annotationsChanged: EventEmitter<AnnotationWrapper>,
hint: boolean = false,
) {
const data = { dossier: this._dossier(file), hint };
const { dossierId, fileId } = this._screenStateService;
const data = { dossier: this._dossier, hint };
this._dialogService.openDialog('forceAnnotation', $event, data, (request: ILegalBasisChangeRequest) => {
annotations.forEach(annotation => {
this._processObsAndEmit(
@ -84,7 +86,8 @@ export class AnnotationActionsService {
...request,
annotationId: annotation.id,
},
file,
dossierId,
fileId,
),
annotation,
annotationsChanged,
@ -93,22 +96,19 @@ export class AnnotationActionsService {
});
}
changeLegalBasis(
$event: MouseEvent,
annotations: AnnotationWrapper[],
file: File,
annotationsChanged: EventEmitter<AnnotationWrapper>,
) {
changeLegalBasis($event: MouseEvent, annotations: AnnotationWrapper[], annotationsChanged: EventEmitter<AnnotationWrapper>) {
const { dossierId, fileId } = this._screenStateService;
this._dialogService.openDialog(
'changeLegalBasis',
$event,
{ annotations, dossier: this._dossier(file) },
{ annotations, dossier: this._dossier },
(data: { comment: string; legalBasis: string; section: string; value: string }) => {
annotations.forEach(annotation => {
this._processObsAndEmit(
this._manualAnnotationService.changeLegalBasis(
annotation.annotationId,
file,
dossierId,
fileId,
data.section,
data.value,
data.legalBasis,
@ -125,20 +125,26 @@ export class AnnotationActionsService {
removeOrSuggestRemoveAnnotation(
$event: MouseEvent,
annotations: AnnotationWrapper[],
file: File,
removeFromDictionary: boolean,
annotationsChanged: EventEmitter<AnnotationWrapper>,
) {
const data = {
annotationsToRemove: annotations,
removeFromDictionary,
dossier: this._dossier(file),
dossier: this._dossier,
hint: annotations[0].hintDictionary,
};
const { dossierId, fileId } = this._screenStateService;
this._dialogService.openDialog('removeAnnotations', $event, data, (result: { comment: string }) => {
annotations.forEach(annotation => {
this._processObsAndEmit(
this._manualAnnotationService.removeOrSuggestRemoveAnnotation(annotation, file, removeFromDictionary, result.comment),
this._manualAnnotationService.removeOrSuggestRemoveAnnotation(
annotation,
dossierId,
fileId,
removeFromDictionary,
result.comment,
),
annotation,
annotationsChanged,
);
@ -146,28 +152,19 @@ export class AnnotationActionsService {
});
}
markAsFalsePositive(
$event: MouseEvent,
annotations: AnnotationWrapper[],
file: File,
annotationsChanged: EventEmitter<AnnotationWrapper>,
) {
markAsFalsePositive($event: MouseEvent, annotations: AnnotationWrapper[], annotationsChanged: EventEmitter<AnnotationWrapper>) {
annotations.forEach(annotation => {
this._markAsFalsePositive($event, annotation, file, this._getFalsePositiveText(annotation), annotationsChanged);
this._markAsFalsePositive($event, annotation, this._getFalsePositiveText(annotation), annotationsChanged);
});
}
recategorizeImages(
$event: MouseEvent,
annotations: AnnotationWrapper[],
file: File,
annotationsChanged: EventEmitter<AnnotationWrapper>,
) {
const data = { annotations, dossier: this._dossier(file) };
recategorizeImages($event: MouseEvent, annotations: AnnotationWrapper[], annotationsChanged: EventEmitter<AnnotationWrapper>) {
const data = { annotations, dossier: this._dossier };
const { dossierId, fileId } = this._screenStateService;
this._dialogService.openDialog('recategorizeImage', $event, data, (res: { type: string; comment: string }) => {
annotations.forEach(annotation => {
this._processObsAndEmit(
this._manualAnnotationService.recategorizeImg(annotation.annotationId, file, res.type, res.comment),
this._manualAnnotationService.recategorizeImg(annotation.annotationId, dossierId, fileId, res.type, res.comment),
annotation,
annotationsChanged,
);
@ -175,37 +172,37 @@ export class AnnotationActionsService {
});
}
undoDirectAction(
$event: MouseEvent,
annotations: AnnotationWrapper[],
file: File,
annotationsChanged: EventEmitter<AnnotationWrapper>,
) {
undoDirectAction($event: MouseEvent, annotations: AnnotationWrapper[], annotationsChanged: EventEmitter<AnnotationWrapper>) {
$event?.stopPropagation();
const { dossierId, fileId } = this._screenStateService;
annotations.forEach(annotation => {
this._processObsAndEmit(this._manualAnnotationService.undoRequest(annotation, file), annotation, annotationsChanged);
this._processObsAndEmit(
this._manualAnnotationService.undoRequest(annotation, dossierId, fileId),
annotation,
annotationsChanged,
);
});
}
convertRecommendationToAnnotation(
$event: any,
recommendations: AnnotationWrapper[],
file: File,
annotationsChanged: EventEmitter<AnnotationWrapper>,
) {
$event?.stopPropagation();
const { dossierId, fileId } = this._screenStateService;
const dialogRef = this._dialog.open<AcceptRecommendationDialogComponent, AcceptRecommendationData, AcceptRecommendationReturnType>(
AcceptRecommendationDialogComponent,
{ ...defaultDialogConfig, autoFocus: true, data: { annotations: recommendations, file } },
{ ...defaultDialogConfig, autoFocus: true, data: { annotations: recommendations, dossierId } },
);
const dialogClosed = dialogRef.afterClosed().pipe(filter(value => !!value && !!value.annotations));
dialogClosed.subscribe(({ annotations, comment: commentText }) => {
const comment = commentText ? { text: commentText } : undefined;
annotations.forEach(annotation => {
this._processObsAndEmit(
this._manualAnnotationService.addRecommendation(annotation, file, comment),
this._manualAnnotationService.addRecommendation(annotation, dossierId, fileId, comment),
annotation,
annotationsChanged,
);
@ -215,13 +212,11 @@ export class AnnotationActionsService {
getViewerAvailableActions(
viewer: WebViewerInstance,
file: File,
dossier: Dossier,
annotations: AnnotationWrapper[],
annotationsChanged: EventEmitter<AnnotationWrapper>,
): Record<string, unknown>[] {
const availableActions = [];
const dossier = this._dossier(file);
const annotationPermissions = annotations.map(annotation => ({
annotation,
permissions: AnnotationPermissions.forUser(
@ -241,21 +236,19 @@ export class AnnotationActionsService {
type: 'actionButton',
img: this._convertPath('/assets/icons/general/check.svg'),
title: this._translateService.instant('annotation-actions.resize-accept.label'),
onClick: () => {
onClick: () =>
this._ngZone.run(() => {
this.acceptResize(null, viewer, file, firstAnnotation, annotationsChanged);
});
},
this.acceptResize(null, viewer, firstAnnotation, annotationsChanged);
}),
});
availableActions.push({
type: 'actionButton',
img: this._convertPath('/assets/icons/general/close.svg'),
title: this._translateService.instant('annotation-actions.resize-cancel.label'),
onClick: () => {
onClick: () =>
this._ngZone.run(() => {
this.cancelResize(null, viewer, firstAnnotation, annotationsChanged);
});
},
}),
});
return availableActions;
}
@ -264,11 +257,7 @@ export class AnnotationActionsService {
type: 'actionButton',
img: this._convertPath('/assets/icons/general/resize.svg'),
title: this._translateService.instant('annotation-actions.resize.label'),
onClick: () => {
this._ngZone.run(() => {
this.resize(null, viewer, annotations[0]);
});
},
onClick: () => this._ngZone.run(() => this.resize(null, viewer, annotations[0])),
});
}
@ -278,11 +267,10 @@ export class AnnotationActionsService {
type: 'actionButton',
img: this._convertPath('/assets/icons/general/edit.svg'),
title: this._translateService.instant('annotation-actions.edit-reason.label'),
onClick: () => {
onClick: () =>
this._ngZone.run(() => {
this.changeLegalBasis(null, annotations, file, annotationsChanged);
});
},
this.changeLegalBasis(null, annotations, annotationsChanged);
}),
});
}
@ -292,11 +280,10 @@ export class AnnotationActionsService {
type: 'actionButton',
img: this._convertPath('/assets/icons/general/thumb-down.svg'),
title: this._translateService.instant('annotation-actions.recategorize-image'),
onClick: () => {
onClick: () =>
this._ngZone.run(() => {
this.recategorizeImages(null, annotations, file, annotationsChanged);
});
},
this.recategorizeImages(null, annotations, annotationsChanged);
}),
});
}
@ -309,11 +296,10 @@ export class AnnotationActionsService {
type: 'actionButton',
img: this._convertPath('/assets/icons/general/remove-from-dict.svg'),
title: this._translateService.instant('annotation-actions.remove-annotation.remove-from-dict'),
onClick: () => {
onClick: () =>
this._ngZone.run(() => {
this.removeOrSuggestRemoveAnnotation(null, annotations, file, true, annotationsChanged);
});
},
this.removeOrSuggestRemoveAnnotation(null, annotations, true, annotationsChanged);
}),
});
}
@ -323,11 +309,10 @@ export class AnnotationActionsService {
type: 'actionButton',
img: this._convertPath('/assets/icons/general/check.svg'),
title: this._translateService.instant('annotation-actions.accept-recommendation.label'),
onClick: () => {
onClick: () =>
this._ngZone.run(() => {
this.convertRecommendationToAnnotation(null, annotations, file, annotationsChanged);
});
},
this.convertRecommendationToAnnotation(null, annotations, annotationsChanged);
}),
});
}
@ -337,11 +322,10 @@ export class AnnotationActionsService {
type: 'actionButton',
img: this._convertPath('/assets/icons/general/check.svg'),
title: this._translateService.instant('annotation-actions.accept-suggestion.label'),
onClick: () => {
onClick: () =>
this._ngZone.run(() => {
this.acceptSuggestion(null, annotations, file, annotationsChanged);
});
},
this.acceptSuggestion(null, annotations, annotationsChanged);
}),
});
}
@ -351,11 +335,10 @@ export class AnnotationActionsService {
type: 'actionButton',
img: this._convertPath('/assets/icons/general/undo.svg'),
title: this._translateService.instant('annotation-actions.undo'),
onClick: () => {
onClick: () =>
this._ngZone.run(() => {
this.undoDirectAction(null, annotations, file, annotationsChanged);
});
},
this.undoDirectAction(null, annotations, annotationsChanged);
}),
});
}
@ -365,11 +348,10 @@ export class AnnotationActionsService {
type: 'actionButton',
img: this._convertPath('/assets/icons/general/thumb-down.svg'),
title: this._translateService.instant('annotation-actions.remove-annotation.false-positive'),
onClick: () => {
onClick: () =>
this._ngZone.run(() => {
this.markAsFalsePositive(null, annotations, file, annotationsChanged);
});
},
this.markAsFalsePositive(null, annotations, annotationsChanged);
}),
});
}
@ -379,11 +361,10 @@ export class AnnotationActionsService {
type: 'actionButton',
img: this._convertPath('/assets/icons/general/thumb-up.svg'),
title: this._translateService.instant('annotation-actions.force-redaction.label'),
onClick: () => {
onClick: () =>
this._ngZone.run(() => {
this.forceAnnotation(null, annotations, file, annotationsChanged);
});
},
this.forceAnnotation(null, annotations, annotationsChanged);
}),
});
}
@ -393,11 +374,10 @@ export class AnnotationActionsService {
type: 'actionButton',
img: this._convertPath('/assets/icons/general/thumb-up.svg'),
title: this._translateService.instant('annotation-actions.force-hint.label'),
onClick: () => {
onClick: () =>
this._ngZone.run(() => {
this.forceAnnotation(null, annotations, file, annotationsChanged, true);
});
},
this.forceAnnotation(null, annotations, annotationsChanged, true);
}),
});
}
@ -407,11 +387,10 @@ export class AnnotationActionsService {
type: 'actionButton',
img: this._convertPath('/assets/icons/general/close.svg'),
title: this._translateService.instant('annotation-actions.reject-suggestion'),
onClick: () => {
onClick: () =>
this._ngZone.run(() => {
this.rejectSuggestion(null, annotations, file, annotationsChanged);
});
},
this.rejectSuggestion(null, annotations, annotationsChanged);
}),
});
}
@ -424,11 +403,10 @@ export class AnnotationActionsService {
type: 'actionButton',
img: this._convertPath('/assets/icons/general/trash.svg'),
title: this._translateService.instant('annotation-actions.remove-annotation.only-here'),
onClick: () => {
onClick: () =>
this._ngZone.run(() => {
this.removeOrSuggestRemoveAnnotation(null, annotations, file, false, annotationsChanged);
});
},
this.removeOrSuggestRemoveAnnotation(null, annotations, false, annotationsChanged);
}),
});
}
@ -460,11 +438,11 @@ export class AnnotationActionsService {
acceptResize(
$event: MouseEvent,
viewer: WebViewerInstance,
file: File,
annotationWrapper: AnnotationWrapper,
annotationsChanged?: EventEmitter<AnnotationWrapper>,
) {
const data = { dossier: this._dossier(file) };
const data = { dossier: this._dossier };
const fileId = this._screenStateService.fileId;
this._dialogService.openDialog('resizeAnnotation', $event, data, async (result: { comment: string }) => {
const textAndPositions = await this._extractTextAndPositions(viewer, annotationWrapper.id);
const text =
@ -478,7 +456,7 @@ export class AnnotationActionsService {
};
this._processObsAndEmit(
this._manualAnnotationService.resizeOrSuggestToResize(annotationWrapper, file, resizeRequest),
this._manualAnnotationService.resizeOrSuggestToResize(annotationWrapper, data.dossier.dossierId, fileId, resizeRequest),
annotationWrapper,
annotationsChanged,
);
@ -503,10 +481,6 @@ export class AnnotationActionsService {
annotationsChanged.emit(annotationWrapper);
}
private _dossier(file: File): Dossier {
return this._dossiersService.find(file.dossierId);
}
private _processObsAndEmit(
obs: Observable<unknown>,
annotation: AnnotationWrapper,
@ -541,7 +515,6 @@ export class AnnotationActionsService {
private _markAsFalsePositive(
$event: MouseEvent,
annotation: AnnotationWrapper,
file: File,
text: string,
annotationsChanged: EventEmitter<AnnotationWrapper>,
) {
@ -554,8 +527,13 @@ export class AnnotationActionsService {
falsePositiveRequest.positions = annotation.positions;
falsePositiveRequest.addToDictionary = true;
falsePositiveRequest.comment = { text: 'False Positive' };
const { dossierId, fileId } = this._screenStateService;
this._processObsAndEmit(this._manualAnnotationService.addAnnotation(falsePositiveRequest, file), annotation, annotationsChanged);
this._processObsAndEmit(
this._manualAnnotationService.addAnnotation(falsePositiveRequest, dossierId, fileId),
annotation,
annotationsChanged,
);
}
private _convertPath(path: string): string {

View File

@ -1,14 +1,31 @@
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { BehaviorSubject, firstValueFrom, Observable } from 'rxjs';
import { FileDataModel } from '@models/file/file-data.model';
import { Dossier, File } from '@red/domain';
import { DossiersService } from '../../../../../services/entity-services/dossiers.service';
import { ActivatedRoute } from '@angular/router';
import { FilesMapService } from '../../../../../services/entity-services/files-map.service';
@Injectable()
export class FilePreviewStateService {
readonly fileData$: Observable<FileDataModel>;
readonly file$: Observable<File>;
readonly dossier$: Observable<Dossier>;
readonly dossierId: string;
readonly fileId: string;
private readonly _fileData$ = new BehaviorSubject<FileDataModel>(undefined);
constructor() {
constructor(
private readonly _dossiersService: DossiersService,
private readonly _filesMapService: FilesMapService,
activatedRoute: ActivatedRoute,
) {
this.dossierId = activatedRoute.snapshot.paramMap.get('dossierId');
this.dossier$ = _dossiersService.getEntityChanged$(this.dossierId);
this.fileId = activatedRoute.snapshot.paramMap.get('fileId');
this.fileData$ = this._fileData$.asObservable();
this.file$ = _filesMapService.watch$(this.dossierId, this.fileId);
}
get fileData(): FileDataModel {
@ -18,4 +35,8 @@ export class FilePreviewStateService {
set fileData(fileDataModel: FileDataModel) {
this._fileData$.next(fileDataModel);
}
get file(): Promise<File> {
return firstValueFrom(this.file$);
}
}

View File

@ -2,7 +2,6 @@ import { Injectable, Injector } from '@angular/core';
import { AppStateService } from '@state/app-state.service';
import {
Dossier,
File,
IAddRedactionRequest,
IApproveRequest,
IImageRecategorizationRequest,
@ -57,14 +56,15 @@ export class ManualAnnotationService extends GenericService<IManualAddResponse>
_makeRequest(
mode: AnnotationActionMode,
file: File,
dossierId: string,
fileId: string,
body: any,
secondParam: any = null,
modifyDictionary = false,
): Observable<unknown> {
const obs = !secondParam
? this[this.CONFIG[mode]](body, file.dossierId, file.id)
: this[this.CONFIG[mode]](body, secondParam, file.dossierId, file.id);
? this[this.CONFIG[mode]](body, dossierId, fileId)
: this[this.CONFIG[mode]](body, secondParam, dossierId, fileId);
return obs.pipe(
tap({
@ -74,7 +74,8 @@ export class ManualAnnotationService extends GenericService<IManualAddResponse>
this._toaster.error(this._getMessage(mode, modifyDictionary, true, isConflict), {
error,
params: {
dictionaryName: this._appStateService.getDictionary(body.type, this._dossier(file).dossierTemplateId).label,
dictionaryName: this._appStateService.getDictionary(body.type, this._dossier(dossierId).dossierTemplateId)
.label,
content: body.value,
},
positionClass: 'toast-file-preview',
@ -96,7 +97,7 @@ export class ManualAnnotationService extends GenericService<IManualAddResponse>
return super.delete({}, url);
}
addRecommendation(annotation: AnnotationWrapper, file: File, comment = { text: 'Accepted Recommendation' }) {
addRecommendation(annotation: AnnotationWrapper, dossierId: string, fileId: string, comment = { text: 'Accepted Recommendation' }) {
const manualRedactionEntry: IAddRedactionRequest = {};
manualRedactionEntry.addToDictionary = true;
// set the ID as reason, so we can hide the suggestion
@ -105,76 +106,89 @@ export class ManualAnnotationService extends GenericService<IManualAddResponse>
manualRedactionEntry.positions = annotation.positions;
manualRedactionEntry.type = annotation.recommendationType;
manualRedactionEntry.comment = comment;
return this.addAnnotation(manualRedactionEntry, file);
return this.addAnnotation(manualRedactionEntry, dossierId, fileId);
}
// /manualRedaction/request/legalBasis
changeLegalBasis(annotationId: string, file: File, section: string, value: string, legalBasis: string, comment?: string) {
const mode: AnnotationActionMode = this._permissionsService.isApprover(this._dossier(file))
changeLegalBasis(
annotationId: string,
dossierId: string,
fileId: string,
section: string,
value: string,
legalBasis: string,
comment?: string,
) {
const mode: AnnotationActionMode = this._permissionsService.isApprover(this._dossier(dossierId))
? 'change-legal-basis'
: 'request-change-legal-basis';
return this._makeRequest(mode, file, { annotationId, legalBasis, comment, section, value });
return this._makeRequest(mode, dossierId, fileId, { annotationId, legalBasis, comment, section, value });
}
// this wraps
// /manualRedaction/redaction/legalBasisChange
// /manualRedaction/request/recategorize
recategorizeImg(annotationId: string, file: File, type: string, comment: string) {
const mode: AnnotationActionMode = this._permissionsService.isApprover(this._dossier(file))
recategorizeImg(annotationId: string, dossierId: string, fileId: string, type: string, comment: string) {
const mode: AnnotationActionMode = this._permissionsService.isApprover(this._dossier(dossierId))
? 'recategorize-image'
: 'request-image-recategorization';
return this._makeRequest(mode, file, { annotationId, type, comment });
return this._makeRequest(mode, dossierId, fileId, { annotationId, type, comment });
}
// this wraps
// /manualRedaction/redaction/recategorize
// /manualRedaction/request/add
addAnnotation(manualRedactionEntry: IAddRedactionRequest, file: File) {
const mode: AnnotationActionMode = this._permissionsService.isApprover(this._dossier(file)) ? 'add' : 'suggest';
return this._makeRequest(mode, file, manualRedactionEntry, null, manualRedactionEntry.addToDictionary);
addAnnotation(manualRedactionEntry: IAddRedactionRequest, dossierId: string, fileId: string) {
const mode: AnnotationActionMode = this._permissionsService.isApprover(this._dossier(dossierId)) ? 'add' : 'suggest';
return this._makeRequest(mode, dossierId, fileId, manualRedactionEntry, null, manualRedactionEntry.addToDictionary);
}
// this wraps
// /manualRedaction/redaction/add
// /manualRedaction/request/force
force(request: ILegalBasisChangeRequest, file: File) {
const mode: AnnotationActionMode = this._permissionsService.isApprover(this._dossier(file))
force(request: ILegalBasisChangeRequest, dossierId: string, fileId: string) {
const mode: AnnotationActionMode = this._permissionsService.isApprover(this._dossier(dossierId))
? 'force-redaction'
: 'request-force-redaction';
return this._makeRequest(mode, file, request);
return this._makeRequest(mode, dossierId, fileId, request);
}
// this wraps
// /manualRedaction/redaction/force
// /manualRedaction/approve
approve(annotationId: string, file: File, addToDictionary: boolean = false) {
approve(annotationId: string, dossierId: string, fileId: string, addToDictionary: boolean = false) {
// for only here - approve the request
return this._makeRequest('approve', file, { addOrRemoveFromDictionary: addToDictionary }, annotationId, addToDictionary);
return this._makeRequest(
'approve',
dossierId,
fileId,
{ addOrRemoveFromDictionary: addToDictionary },
annotationId,
addToDictionary,
);
}
// this wraps
undoRequest(annotationWrapper: AnnotationWrapper, file: File) {
return this._makeRequest('undo', file, annotationWrapper.id, null, annotationWrapper.isModifyDictionary);
}
// /manualRedaction/undo
declineOrRemoveRequest(annotationWrapper: AnnotationWrapper, file: File) {
const mode: AnnotationActionMode = this._permissionsService.isApprover(this._dossier(file)) ? 'decline' : 'undo';
return this._makeRequest(mode, file, annotationWrapper.id, null, annotationWrapper.isModifyDictionary);
undoRequest(annotationWrapper: AnnotationWrapper, dossierId: string, fileId: string) {
return this._makeRequest('undo', dossierId, fileId, annotationWrapper.id, null, annotationWrapper.isModifyDictionary);
}
// this wraps
// /manualRedaction/decline/remove
declineOrRemoveRequest(annotationWrapper: AnnotationWrapper, dossierId: string, fileId: string) {
const mode: AnnotationActionMode = this._permissionsService.isApprover(this._dossier(dossierId)) ? 'decline' : 'undo';
return this._makeRequest(mode, dossierId, fileId, annotationWrapper.id, null, annotationWrapper.isModifyDictionary);
}
// /manualRedaction/request/resize/
resizeOrSuggestToResize(annotationWrapper: AnnotationWrapper, file: File, resizeRequest: IResizeRequest) {
const mode: AnnotationActionMode = this._permissionsService.isApprover(this._dossier(file)) ? 'resize' : 'request-resize';
return this._makeRequest(mode, file, resizeRequest);
resizeOrSuggestToResize(annotationWrapper: AnnotationWrapper, dossierId: string, fileId: string, resizeRequest: IResizeRequest) {
const mode: AnnotationActionMode = this._permissionsService.isApprover(this._dossier(dossierId)) ? 'resize' : 'request-resize';
return this._makeRequest(mode, dossierId, fileId, resizeRequest);
}
// this wraps
@ -183,7 +197,8 @@ export class ManualAnnotationService extends GenericService<IManualAddResponse>
// /manualRedaction/request/remove/
removeOrSuggestRemoveAnnotation(
annotationWrapper: AnnotationWrapper,
file: File,
dossierId: string,
fileId: string,
removeFromDictionary: boolean = false,
comment: string,
) {
@ -191,7 +206,7 @@ export class ManualAnnotationService extends GenericService<IManualAddResponse>
body: any,
removeDict = false;
if (this._permissionsService.isApprover(this._dossier(file))) {
if (this._permissionsService.isApprover(this._dossier(dossierId))) {
// if it was something manual simply decline the existing request
mode = 'remove';
body = {
@ -210,7 +225,7 @@ export class ManualAnnotationService extends GenericService<IManualAddResponse>
removeDict = removeFromDictionary;
}
return this._makeRequest(mode, file, body, null, removeDict);
return this._makeRequest(mode, dossierId, fileId, body, null, removeDict);
}
// this wraps
@ -353,8 +368,8 @@ export class ManualAnnotationService extends GenericService<IManualAddResponse>
return this._post(body, url);
}
private _dossier(file: File): Dossier {
return this._dossiersService.find(file.dossierId);
private _dossier(dossierId: string): Dossier {
return this._dossiersService.find(dossierId);
}
private _getMessage(mode: AnnotationActionMode, modifyDictionary?: boolean, error = false, isConflict = false) {

View File

@ -1,6 +1,6 @@
import { Injectable } from '@angular/core';
import { forkJoin, Observable, of } from 'rxjs';
import { catchError, map, tap } from 'rxjs/operators';
import { forkJoin, Observable, of, switchMap } from 'rxjs';
import { catchError, map, take, tap } from 'rxjs/operators';
import { FileDataModel } from '@models/file/file-data.model';
import { PermissionsService } from '@services/permissions.service';
import { File, IRedactionLog, IViewedPage } from '@red/domain';
@ -32,19 +32,23 @@ export class PdfViewerDataService {
);
}
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);
loadDataFor(newFile: File): Observable<FileDataModel> {
const oldBlob$ = this._stateService.fileData?.blob$;
const blob$ = this._stateService.file$.pipe(
map(file => file.cacheIdentifier === newFile.cacheIdentifier && oldBlob$),
switchMap(isSame => (isSame ? oldBlob$ : this.downloadOriginalFile(newFile))),
take(1),
);
const redactionLog$ = this.loadRedactionLogFor(newFile.dossierId, newFile.fileId);
const viewedPages$ = this.getViewedPagesFor(newFile);
const dossier = this._dossiersService.find(file.dossierId);
const dossier = this._dossiersService.find(newFile.dossierId);
return forkJoin([blob$, redactionLog$, viewedPages$]).pipe(
map(
(data: [blob: Blob, redactionLog: IRedactionLog, viewedPages: IViewedPage[]]) =>
new FileDataModel(
file,
newFile,
...data,
this._appStateService.dictionaryData[dossier.dossierTemplateId],
this._userPreferenceService.areDevFeaturesEnabled,
@ -60,7 +64,7 @@ export class PdfViewerDataService {
return of([]);
}
downloadOriginalFile(file: File): Observable<any> {
downloadOriginalFile(file: File): Observable<Blob> {
return this._fileManagementService.downloadOriginalFile(file.dossierId, file.fileId, 'body', true, file.cacheIdentifier);
}
}

View File

@ -138,7 +138,7 @@ export class PdfViewerUtils {
this._annotationManager.deselectAnnotations(ann);
}
private _navigateToPage(pageNumber) {
private _navigateToPage(pageNumber: number) {
if (this._currentInternalPage !== pageNumber) {
this._documentViewer.displayPageLocation(pageNumber, 0, 0);
}

View File

@ -2,6 +2,7 @@ import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable, Subject } from 'rxjs';
import { File } from '@red/domain';
import { filter, startWith } from 'rxjs/operators';
import { shareLast } from '@iqser/common-ui';
@Injectable({ providedIn: 'root' })
export class FilesMapService {
@ -37,7 +38,7 @@ export class FilesMapService {
return entities.forEach(entity => this._entityChanged$.next(entity));
}
const changedEntities = [];
const changedEntities: File[] = [];
const deletedEntities = this.get(key).filter(oldEntity => !entities.find(newEntity => newEntity.id === oldEntity.id));
// Keep old object references for unchanged entities
@ -77,6 +78,7 @@ export class FilesMapService {
return this._entityChanged$.pipe(
filter(entity => entity.id === entityId),
startWith(this.get(key, entityId)),
shareLast(),
);
}

View File

@ -18,7 +18,8 @@ export class PermissionsService {
return this.isApprover(dossier);
}
canEditFileAttributes(dossier: Dossier, file: File): boolean {
canEditFileAttributes(file: File): boolean {
const dossier = this._getDossier(file);
return ((file.isUnderReview || file.isNew) && this.isDossierMember(dossier)) || (file.isUnderApproval && this.isApprover(dossier));
}

View File

@ -23,8 +23,6 @@ export function handleFilterDelta(oldFilters: INestedFilter[], newFilters: INest
}
}
console.log(newFiltersDelta);
for (const key of Object.keys(newFiltersDelta)) {
const foundFilter = allFilters.find(f => f.id === key);
if (foundFilter) {