From 6933fead2514c92462ed6c9ca8c0948165b6594a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adina=20=C8=9Aeudan?= Date: Thu, 17 Dec 2020 23:21:28 +0200 Subject: [PATCH] Annotation actions pop-up in viewer --- .../service/annotation-actions.service.ts | 185 ++++++++++++++++++ .../annotation-actions.component.html | 30 +-- .../annotation-actions.component.ts | 89 +-------- .../file-preview-screen.component.html | 4 +- .../file-preview-screen.component.ts | 10 +- .../file/pdf-viewer/pdf-viewer.component.ts | 25 ++- 6 files changed, 232 insertions(+), 111 deletions(-) create mode 100644 apps/red-ui/src/app/common/service/annotation-actions.service.ts diff --git a/apps/red-ui/src/app/common/service/annotation-actions.service.ts b/apps/red-ui/src/app/common/service/annotation-actions.service.ts new file mode 100644 index 000000000..b731761c6 --- /dev/null +++ b/apps/red-ui/src/app/common/service/annotation-actions.service.ts @@ -0,0 +1,185 @@ +import { EventEmitter, Injectable } from '@angular/core'; +import { PermissionsService } from './permissions.service'; +import { ManualAnnotationService } from '../../screens/file/service/manual-annotation.service'; +import { DialogService } from '../../dialogs/dialog.service'; +import { AnnotationWrapper } from '../../screens/file/model/annotation.wrapper'; +import { Observable } from 'rxjs'; +import { TranslateService } from '@ngx-translate/core'; + +@Injectable({ + providedIn: 'root' +}) +export class AnnotationActionsService { + constructor( + public permissionsService: PermissionsService, + private readonly _manualAnnotationService: ManualAnnotationService, + private readonly _translateService: TranslateService, + private readonly _dialogService: DialogService + ) {} + + public canAcceptSuggestion(annotation: AnnotationWrapper): boolean { + return this.permissionsService.isManagerAndOwner() && (annotation.isSuggestion || annotation.isDeclinedSuggestion); + } + + public canRejectSuggestion(annotation: AnnotationWrapper): boolean { + // i can reject whatever i may not undo + return ( + !this.canUndoAnnotation && + ((this.canAcceptSuggestion && !annotation.isDeclinedSuggestion) || (annotation.isModifyDictionary && !annotation.isDeclinedSuggestion)) + ); + } + + public canDirectlySuggestToRemoveAnnotation(annotation: AnnotationWrapper): boolean { + return ( + (annotation.isHint || (annotation.isManual && this.permissionsService.isManagerAndOwner() && !this.canUndoAnnotation)) && + !annotation.isRecommendation + ); + } + + public requiresSuggestionRemoveMenu(annotation: AnnotationWrapper): boolean { + return (annotation.isRedacted || annotation.isIgnored) && !annotation.isRecommendation; + } + + public canConvertRecommendationToAnnotation(annotation: AnnotationWrapper): boolean { + return annotation.isRecommendation; + } + + public canUndoAnnotation(annotation: AnnotationWrapper): boolean { + // suggestions of current user can be undone + const isSuggestionOfCurrentUser = annotation.isSuggestion && annotation.userId === this.permissionsService.currentUserId; + // or any performed manual actions and you are the manager, provided that it is not a suggestion + const isActionOfManger = this.permissionsService.isManagerAndOwner() && annotation.userId === this.permissionsService.currentUserId; + return isSuggestionOfCurrentUser || isActionOfManger; + } + + public acceptSuggestion($event: MouseEvent, annotation: AnnotationWrapper, annotationsChanged: EventEmitter) { + if ($event) $event.stopPropagation(); + this._processObsAndEmit(this._manualAnnotationService.approveRequest(annotation.id, annotation.isModifyDictionary), annotation, annotationsChanged); + } + + public rejectSuggestion($event: MouseEvent, annotation: AnnotationWrapper, annotationsChanged: EventEmitter) { + if ($event) $event.stopPropagation(); + this._processObsAndEmit(this._manualAnnotationService.declineOrRemoveRequest(annotation), annotation, annotationsChanged); + } + + public suggestRemoveAnnotation( + $event: MouseEvent, + annotation: AnnotationWrapper, + removeFromDictionary: boolean, + annotationsChanged: EventEmitter + ) { + this._dialogService.openRemoveFromDictionaryDialog($event, annotation, removeFromDictionary, () => { + this._processObsAndEmit( + this._manualAnnotationService.removeOrSuggestRemoveAnnotation(annotation, removeFromDictionary), + annotation, + annotationsChanged + ); + }); + } + + public undoDirectAction($event: MouseEvent, annotation: AnnotationWrapper, annotationsChanged: EventEmitter) { + if ($event) $event.stopPropagation(); + this._processObsAndEmit(this._manualAnnotationService.undoRequest(annotation), annotation, annotationsChanged); + } + + public convertRecommendationToAnnotation($event: any, annotation: AnnotationWrapper, annotationsChanged: EventEmitter) { + if ($event) $event.stopPropagation(); + this._processObsAndEmit(this._manualAnnotationService.addRecommendation(annotation), annotation, annotationsChanged); + } + + private _processObsAndEmit(obs: Observable, annotation: AnnotationWrapper, annotationsChanged: EventEmitter) { + obs.subscribe( + (data) => { + annotationsChanged.emit(annotation); + }, + () => { + annotationsChanged.emit(); + } + ); + } + + public getViewerAvailableActions(annotation: AnnotationWrapper, annotationsChanged: EventEmitter): {}[] { + const availableActions = []; + + if (this.canConvertRecommendationToAnnotation(annotation)) { + availableActions.push({ + type: 'actionButton', + img: '/assets/icons/general/check-alt.svg', + title: this._translateService.instant('annotation-actions.accept-recommendation.label'), + onClick: () => { + this.undoDirectAction(null, annotation, annotationsChanged); + } + }); + } + + if (this.canAcceptSuggestion(annotation)) { + availableActions.push({ + type: 'actionButton', + img: '/assets/icons/general/check-alt.svg', + title: this._translateService.instant('annotation-actions.accept-suggestion.label'), + onClick: () => { + this.acceptSuggestion(null, annotation, annotationsChanged); + } + }); + } + + if (this.canUndoAnnotation(annotation)) { + availableActions.push({ + type: 'actionButton', + img: '/assets/icons/general/undo.svg', + title: this._translateService.instant('annotation-actions.undo'), + onClick: () => { + this.undoDirectAction(null, annotation, annotationsChanged); + } + }); + } + + if (this.canRejectSuggestion(annotation)) { + availableActions.push({ + type: 'actionButton', + img: '/assets/icons/general/close.svg', + title: this._translateService.instant('annotation-actions.reject-suggestion'), + onClick: () => { + this.rejectSuggestion(null, annotation, annotationsChanged); + } + }); + } + + if (this.canDirectlySuggestToRemoveAnnotation(annotation)) { + availableActions.push({ + type: 'actionButton', + img: '/assets/icons/general/trash.svg', + title: this._translateService.instant('annotation-actions.suggest-remove-annotation'), + onClick: () => { + this.suggestRemoveAnnotation(null, annotation, true, annotationsChanged); + } + }); + } + + // TODO: probably need icons for these? + + if (this.requiresSuggestionRemoveMenu(annotation)) { + availableActions.push({ + type: 'actionButton', + img: '/assets/icons/general/trash.svg', + title: this._translateService.instant('annotation-actions.remove-annotation.remove-from-dict'), + onClick: () => { + this.suggestRemoveAnnotation(null, annotation, true, annotationsChanged); + } + }); + + if (!annotation.isIgnored) { + availableActions.push({ + type: 'actionButton', + img: '/assets/icons/general/trash.svg', + title: this._translateService.instant('annotation-actions.remove-annotation.only-here'), + onClick: () => { + this.suggestRemoveAnnotation(null, annotation, false, annotationsChanged); + } + }); + } + } + + return availableActions; + } +} diff --git a/apps/red-ui/src/app/screens/file/annotation-actions/annotation-actions.component.html b/apps/red-ui/src/app/screens/file/annotation-actions/annotation-actions.component.html index de7e54be2..38fce3fc4 100644 --- a/apps/red-ui/src/app/screens/file/annotation-actions/annotation-actions.component.html +++ b/apps/red-ui/src/app/screens/file/annotation-actions/annotation-actions.component.html @@ -1,8 +1,8 @@
-
+
-
+
diff --git a/apps/red-ui/src/app/screens/file/annotation-actions/annotation-actions.component.ts b/apps/red-ui/src/app/screens/file/annotation-actions/annotation-actions.component.ts index d439ee6a6..59d954600 100644 --- a/apps/red-ui/src/app/screens/file/annotation-actions/annotation-actions.component.ts +++ b/apps/red-ui/src/app/screens/file/annotation-actions/annotation-actions.component.ts @@ -1,11 +1,8 @@ -import { Component, Input, OnInit, Output, EventEmitter } from '@angular/core'; +import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; import { AnnotationWrapper } from '../model/annotation.wrapper'; import { AppStateService } from '../../../state/app-state.service'; import { TypeValue } from '@redaction/red-ui-http'; -import { ManualAnnotationService } from '../service/manual-annotation.service'; -import { Observable } from 'rxjs'; -import { PermissionsService } from '../../../common/service/permissions.service'; -import { DialogService } from '../../../dialogs/dialog.service'; +import { AnnotationActionsService } from '../../../common/service/annotation-actions.service'; @Component({ selector: 'redaction-annotation-actions', @@ -16,90 +13,17 @@ export class AnnotationActionsComponent implements OnInit { @Input() annotation: AnnotationWrapper; @Input() canPerformAnnotationActions: boolean; - @Output() annotationsChanged = new EventEmitter(); + @Output() annotationsChanged = new EventEmitter(); suggestionType: TypeValue; menuOpen: boolean; - constructor( - public appStateService: AppStateService, - public permissionsService: PermissionsService, - private readonly _manualAnnotationService: ManualAnnotationService, - private readonly _dialogService: DialogService - ) {} + constructor(public appStateService: AppStateService, public annotationActionsService: AnnotationActionsService) {} ngOnInit(): void { this.suggestionType = this.appStateService.getDictionaryTypeValue('suggestion'); } - get canAcceptSuggestion() { - return this.permissionsService.isManagerAndOwner() && (this.annotation.isSuggestion || this.annotation.isDeclinedSuggestion); - } - - get canRejectSuggestion() { - // i can reject whatever i may not undo - return ( - !this.canUndoAnnotation && - ((this.canAcceptSuggestion && !this.annotation.isDeclinedSuggestion) || - (this.annotation.isModifyDictionary && !this.annotation.isDeclinedSuggestion)) - ); - } - - get canDirectlySuggestToRemoveAnnotation() { - return ( - (this.annotation.isHint || (this.annotation.isManual && this.permissionsService.isManagerAndOwner() && !this.canUndoAnnotation)) && - !this.annotation.isRecommendation - ); - } - - get requiresSuggestionRemoveMenu() { - return (this.annotation.isRedacted || this.annotation.isIgnored) && !this.annotation.isRecommendation; - } - - get canConvertRecommendationToAnnotation() { - return this.annotation.isRecommendation; - } - - get canUndoAnnotation() { - // suggestions of current user can be undone - const isSuggestionOfCurrentUser = this.annotation.isSuggestion && this.annotation.userId === this.permissionsService.currentUserId; - // or any performed manual actions and you are the manager, provided that it is not a suggestion - const isActionOfManger = this.permissionsService.isManagerAndOwner() && this.annotation.userId === this.permissionsService.currentUserId; - return isSuggestionOfCurrentUser || isActionOfManger; - } - - acceptSuggestion($event: MouseEvent, annotation: AnnotationWrapper) { - $event.stopPropagation(); - this._processObsAndEmit(this._manualAnnotationService.approveRequest(annotation.id, annotation.isModifyDictionary)); - } - - rejectSuggestion($event: MouseEvent, annotation: AnnotationWrapper) { - $event.stopPropagation(); - this._processObsAndEmit(this._manualAnnotationService.declineOrRemoveRequest(annotation)); - } - - suggestRemoveAnnotation($event: MouseEvent, annotation: AnnotationWrapper, removeFromDictionary: boolean) { - this._dialogService.openRemoveFromDictionaryDialog($event, annotation, removeFromDictionary, () => { - this._processObsAndEmit(this._manualAnnotationService.removeOrSuggestRemoveAnnotation(annotation, removeFromDictionary)); - }); - } - - undoDirectAction($event: MouseEvent, annotation: AnnotationWrapper) { - $event.stopPropagation(); - this._processObsAndEmit(this._manualAnnotationService.undoRequest(annotation)); - } - - private _processObsAndEmit(obs: Observable) { - obs.subscribe( - (data) => { - this.annotationsChanged.emit(!!data?.annotationId); - }, - () => { - this.annotationsChanged.emit(); - } - ); - } - public openMenu($event: MouseEvent) { $event.preventDefault(); this.menuOpen = true; @@ -116,9 +40,4 @@ export class AnnotationActionsComponent implements OnInit { get dictionaryColor() { return this.appStateService.getDictionaryColor('suggestion-add-dictionary'); } - - convertRecommendationToAnnotation($event: any, annotation: AnnotationWrapper) { - $event.stopPropagation(); - this._processObsAndEmit(this._manualAnnotationService.addRecommendation(annotation)); - } } diff --git a/apps/red-ui/src/app/screens/file/file-preview-screen/file-preview-screen.component.html b/apps/red-ui/src/app/screens/file/file-preview-screen/file-preview-screen.component.html index 6656327bf..679946d3a 100644 --- a/apps/red-ui/src/app/screens/file/file-preview-screen/file-preview-screen.component.html +++ b/apps/red-ui/src/app/screens/file/file-preview-screen/file-preview-screen.component.html @@ -157,11 +157,13 @@ (keyUp)="handleKeyEvent($event)" (annotationSelected)="handleAnnotationSelected($event)" (manualAnnotationRequested)="openManualRedactionDialog($event)" + (annotationsChanged)="annotationsChangedByReviewAction($event)" (pageChanged)="viewerPageChanged($event)" (viewerReady)="viewerReady($event)" [canPerformActions]="canPerformAnnotationActions" [fileData]="displayData" [fileStatus]="appStateService.activeFile" + [annotations]="annotations" >
@@ -246,7 +248,7 @@
diff --git a/apps/red-ui/src/app/screens/file/file-preview-screen/file-preview-screen.component.ts b/apps/red-ui/src/app/screens/file/file-preview-screen/file-preview-screen.component.ts index 187a40396..6de486bc9 100644 --- a/apps/red-ui/src/app/screens/file/file-preview-screen/file-preview-screen.component.ts +++ b/apps/red-ui/src/app/screens/file/file-preview-screen/file-preview-screen.component.ts @@ -475,14 +475,8 @@ export class FilePreviewScreenComponent implements OnInit, OnDestroy { }); } - async annotationsChangedByReviewAction(requiresCompletePageRedraw: boolean, annotation: AnnotationWrapper) { - if (!requiresCompletePageRedraw) { - this._findAndDeleteAnnotation(annotation.id); - this.fileData.fileStatus = await this.appStateService.reloadActiveFile(); - this._cleanupAndRedrawManualAnnotations(annotation.id); - } else { - await this._cleanupAndRedrawManualAnnotationsForEntirePage(annotation.pageNumber); - } + async annotationsChangedByReviewAction(annotation: AnnotationWrapper) { + await this._cleanupAndRedrawManualAnnotationsForEntirePage(annotation.pageNumber); } private _findAndDeleteAnnotation(id: string) { diff --git a/apps/red-ui/src/app/screens/file/pdf-viewer/pdf-viewer.component.ts b/apps/red-ui/src/app/screens/file/pdf-viewer/pdf-viewer.component.ts index 583972f0c..ca13c87ea 100644 --- a/apps/red-ui/src/app/screens/file/pdf-viewer/pdf-viewer.component.ts +++ b/apps/red-ui/src/app/screens/file/pdf-viewer/pdf-viewer.component.ts @@ -1,7 +1,7 @@ import { AfterViewInit, Component, ElementRef, EventEmitter, Input, NgZone, OnChanges, OnInit, Output, SimpleChanges, ViewChild } from '@angular/core'; import { AppConfigService } from '../../../app-config/app-config.service'; import { ManualRedactionEntry, Rectangle } from '@redaction/red-ui-http'; -import WebViewer, { WebViewerInstance } from '@pdftron/webviewer'; +import WebViewer, { Annotations, WebViewerInstance } from '@pdftron/webviewer'; import { TranslateService } from '@ngx-translate/core'; import { FileDownloadService } from '../service/file-download.service'; import { ManualRedactionEntryWrapper } from '../model/manual-redaction-entry.wrapper'; @@ -12,6 +12,7 @@ import { FileStatusWrapper } from '../model/file-status.wrapper'; import { KeycloakService } from 'keycloak-angular'; import { environment } from '../../../../environments/environment'; import { AnnotationDrawService } from '../service/annotation-draw.service'; +import { AnnotationActionsService } from '../../../common/service/annotation-actions.service'; export interface ViewerState { displayMode?: any; @@ -35,6 +36,7 @@ export class PdfViewerComponent implements OnInit, AfterViewInit, OnChanges { @Input() fileData: Blob; @Input() fileStatus: FileStatusWrapper; @Input() canPerformActions = false; + @Input() annotations: AnnotationWrapper[]; @Output() fileReady = new EventEmitter(); @Output() annotationSelected = new EventEmitter(); @@ -42,6 +44,7 @@ export class PdfViewerComponent implements OnInit, AfterViewInit, OnChanges { @Output() pageChanged = new EventEmitter(); @Output() keyUp = new EventEmitter(); @Output() viewerReady = new EventEmitter(); + @Output() annotationsChanged = new EventEmitter(); @ViewChild('viewer', { static: true }) viewer: ElementRef; instance: WebViewerInstance; @@ -56,7 +59,8 @@ export class PdfViewerComponent implements OnInit, AfterViewInit, OnChanges { private readonly _appConfigService: AppConfigService, private readonly _manualAnnotationService: ManualAnnotationService, private readonly _ngZone: NgZone, - private readonly _annotationDrawService: AnnotationDrawService + private readonly _annotationDrawService: AnnotationDrawService, + private readonly _annotationActionsService: AnnotationActionsService ) {} ngOnInit() { @@ -91,12 +95,13 @@ export class PdfViewerComponent implements OnInit, AfterViewInit, OnChanges { this._disableElements(); this._disableHotkeys(); this._configureTextPopup(); - this._configureAnnotationPopup(); + instance.annotManager.on('annotationSelected', (annotationList, action) => { if (action === 'deselected') { this.annotationSelected.emit(null); this._toggleRectangleAnnotationAction(true); } else { + this._configureAnnotationSpecificActions(annotationList[0]); this._toggleRectangleAnnotationAction(annotationList[0].ReadOnly); this.annotationSelected.emit(annotationList[0].Id); } @@ -192,7 +197,19 @@ export class PdfViewerComponent implements OnInit, AfterViewInit, OnChanges { })); } - private _configureAnnotationPopup() { + private _configureAnnotationSpecificActions(viewerAnnotation: Annotations.Annotation) { + const annotation = this.annotations.find((a) => a.id === viewerAnnotation.Id); + this.instance.annotationPopup.update([]); + + if (!annotation) { + this._configureRectangleAnnotationPopup(); + return; + } + + this.instance.annotationPopup.add(this._annotationActionsService.getViewerAvailableActions(annotation, this.annotationsChanged)); + } + + private _configureRectangleAnnotationPopup() { this.instance.annotationPopup.add({ type: 'actionButton', dataElement: 'add-rectangle',