Pull request #76: Annotation actions pop-up in viewer

Merge in RED/ui from RED-888 to master

* commit '6933fead2514c92462ed6c9ca8c0948165b6594a':
  Annotation actions pop-up in viewer
This commit is contained in:
Timo Bejan 2020-12-21 16:26:02 +01:00
commit e705b62c6c
6 changed files with 232 additions and 111 deletions

View File

@ -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<AnnotationWrapper>) {
if ($event) $event.stopPropagation();
this._processObsAndEmit(this._manualAnnotationService.approveRequest(annotation.id, annotation.isModifyDictionary), annotation, annotationsChanged);
}
public rejectSuggestion($event: MouseEvent, annotation: AnnotationWrapper, annotationsChanged: EventEmitter<AnnotationWrapper>) {
if ($event) $event.stopPropagation();
this._processObsAndEmit(this._manualAnnotationService.declineOrRemoveRequest(annotation), annotation, annotationsChanged);
}
public suggestRemoveAnnotation(
$event: MouseEvent,
annotation: AnnotationWrapper,
removeFromDictionary: boolean,
annotationsChanged: EventEmitter<AnnotationWrapper>
) {
this._dialogService.openRemoveFromDictionaryDialog($event, annotation, removeFromDictionary, () => {
this._processObsAndEmit(
this._manualAnnotationService.removeOrSuggestRemoveAnnotation(annotation, removeFromDictionary),
annotation,
annotationsChanged
);
});
}
public undoDirectAction($event: MouseEvent, annotation: AnnotationWrapper, annotationsChanged: EventEmitter<AnnotationWrapper>) {
if ($event) $event.stopPropagation();
this._processObsAndEmit(this._manualAnnotationService.undoRequest(annotation), annotation, annotationsChanged);
}
public convertRecommendationToAnnotation($event: any, annotation: AnnotationWrapper, annotationsChanged: EventEmitter<AnnotationWrapper>) {
if ($event) $event.stopPropagation();
this._processObsAndEmit(this._manualAnnotationService.addRecommendation(annotation), annotation, annotationsChanged);
}
private _processObsAndEmit(obs: Observable<any>, annotation: AnnotationWrapper, annotationsChanged: EventEmitter<AnnotationWrapper>) {
obs.subscribe(
(data) => {
annotationsChanged.emit(annotation);
},
() => {
annotationsChanged.emit();
}
);
}
public getViewerAvailableActions(annotation: AnnotationWrapper, annotationsChanged: EventEmitter<AnnotationWrapper>): {}[] {
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;
}
}

View File

@ -1,8 +1,8 @@
<div [class.visible]="menuOpen" *ngIf="canPerformAnnotationActions" class="annotation-actions">
<redaction-circle-button
(action)="convertRecommendationToAnnotation($event, annotation)"
(action)="annotationActionsService.convertRecommendationToAnnotation($event, annotation, annotationsChanged)"
type="dark-bg"
*ngIf="canConvertRecommendationToAnnotation"
*ngIf="annotationActionsService.canConvertRecommendationToAnnotation(annotation)"
tooltipPosition="before"
tooltip="annotation-actions.accept-recommendation.label"
icon="red:check-alt"
@ -10,9 +10,9 @@
</redaction-circle-button>
<redaction-circle-button
(action)="acceptSuggestion($event, annotation)"
(action)="annotationActionsService.acceptSuggestion($event, annotation, annotationsChanged)"
type="dark-bg"
*ngIf="canAcceptSuggestion"
*ngIf="annotationActionsService.canAcceptSuggestion(annotation)"
tooltipPosition="before"
tooltip="annotation-actions.accept-suggestion.label"
icon="red:check-alt"
@ -20,8 +20,8 @@
</redaction-circle-button>
<redaction-circle-button
(action)="undoDirectAction($event, annotation)"
*ngIf="canUndoAnnotation"
(action)="annotationActionsService.undoDirectAction($event, annotation, annotationsChanged)"
*ngIf="annotationActionsService.canUndoAnnotation(annotation)"
type="dark-bg"
icon="red:undo"
tooltipPosition="before"
@ -30,27 +30,27 @@
</redaction-circle-button>
<redaction-circle-button
(action)="rejectSuggestion($event, annotation)"
(action)="annotationActionsService.rejectSuggestion($event, annotation, annotationsChanged)"
type="dark-bg"
icon="red:close"
*ngIf="canRejectSuggestion"
*ngIf="annotationActionsService.canRejectSuggestion(annotation)"
tooltipPosition="before"
tooltip="annotation-actions.reject-suggestion"
>
</redaction-circle-button>
<redaction-circle-button
(action)="suggestRemoveAnnotation($event, annotation, true)"
(action)="annotationActionsService.suggestRemoveAnnotation($event, annotation, true, annotationsChanged)"
type="dark-bg"
icon="red:trash"
*ngIf="canDirectlySuggestToRemoveAnnotation"
*ngIf="annotationActionsService.canDirectlySuggestToRemoveAnnotation(annotation)"
tooltipPosition="before"
tooltip="annotation-actions.suggest-remove-annotation"
>
</redaction-circle-button>
<redaction-circle-button
*ngIf="requiresSuggestionRemoveMenu"
*ngIf="annotationActionsService.requiresSuggestionRemoveMenu(annotation)"
(action)="openMenu($event)"
[class.active]="menuOpen"
[matMenuTriggerFor]="menu"
@ -62,11 +62,15 @@
</redaction-circle-button>
<mat-menu #menu="matMenu" (closed)="onMenuClosed()" xPosition="before">
<div (click)="suggestRemoveAnnotation($event, annotation, true)" mat-menu-item>
<div (click)="annotationActionsService.suggestRemoveAnnotation($event, annotation, true, annotationsChanged)" mat-menu-item>
<redaction-annotation-icon [type]="'rhombus'" [label]="'S'" [color]="dictionaryColor"></redaction-annotation-icon>
<div [translate]="'annotation-actions.remove-annotation.remove-from-dict'"></div>
</div>
<div (click)="suggestRemoveAnnotation($event, annotation, false)" mat-menu-item *ngIf="!annotation.isIgnored">
<div
(click)="annotationActionsService.suggestRemoveAnnotation($event, annotation, false, annotationsChanged)"
mat-menu-item
*ngIf="!annotation.isIgnored"
>
<redaction-annotation-icon [type]="'rhombus'" [label]="'S'" [color]="suggestionColor"></redaction-annotation-icon>
<div translate="annotation-actions.remove-annotation.only-here"></div>
</div>

View File

@ -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<boolean>();
@Output() annotationsChanged = new EventEmitter<AnnotationWrapper>();
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<any>) {
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));
}
}

View File

@ -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"
></redaction-pdf-viewer>
</div>
@ -246,7 +248,7 @@
</div>
</div>
<redaction-annotation-actions
(annotationsChanged)="annotationsChangedByReviewAction(true, annotation)"
(annotationsChanged)="annotationsChangedByReviewAction($event)"
[annotation]="annotation"
[canPerformAnnotationActions]="canPerformAnnotationActions"
></redaction-annotation-actions>

View File

@ -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) {

View File

@ -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<string>();
@ -42,6 +44,7 @@ export class PdfViewerComponent implements OnInit, AfterViewInit, OnChanges {
@Output() pageChanged = new EventEmitter<number>();
@Output() keyUp = new EventEmitter<KeyboardEvent>();
@Output() viewerReady = new EventEmitter<WebViewerInstance>();
@Output() annotationsChanged = new EventEmitter<AnnotationWrapper>();
@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(<any>{
type: 'actionButton',
dataElement: 'add-rectangle',