Pull request #241: RED-1768

Merge in RED/ui from RED-1768 to master

* commit '17222b058da32644a1f4b6d163c7f62c998bffd3':
  Show hide bulk action, and bulk action availability in general for all new actions RED-1768
  finished modifications for annotation changed events
This commit is contained in:
Timo Bejan 2021-07-14 20:36:18 +02:00
commit 2a1b80664c
17 changed files with 288 additions and 448 deletions

View File

@ -1,24 +1,25 @@
import { UserWrapper } from '@services/user.service';
import { AnnotationWrapper } from './annotation.wrapper';
import { isArray } from 'rxjs/internal-compatibility';
export class AnnotationPermissions {
canUndo: boolean;
canUndo = true;
canAcceptRecommendation: boolean;
canMarkTextOnlyAsFalsePositive: boolean;
canMarkAsFalsePositive: boolean;
canAcceptRecommendation = true;
canMarkTextOnlyAsFalsePositive = true;
canMarkAsFalsePositive = true;
canRemoveOrSuggestToRemoveOnlyHere: boolean;
canRemoveOrSuggestToRemoveFromDictionary: boolean;
canRemoveOrSuggestToRemoveOnlyHere = true;
canRemoveOrSuggestToRemoveFromDictionary = true;
canAcceptSuggestion: boolean;
canRejectSuggestion: boolean;
canAcceptSuggestion = true;
canRejectSuggestion = true;
canForceRedaction: boolean;
canForceRedaction = true;
canChangeLegalBasis: boolean;
canChangeLegalBasis = true;
canRecategorizeImage: boolean;
canRecategorizeImage = true;
get canPerformMultipleRemoveActions() {
return (
@ -30,42 +31,64 @@ export class AnnotationPermissions {
);
}
static forUser(isApprover: boolean, user: UserWrapper, annotation: AnnotationWrapper) {
const permissions: AnnotationPermissions = new AnnotationPermissions();
static forUser(
isApprover: boolean,
user: UserWrapper,
annotations: AnnotationWrapper | AnnotationWrapper[]
) {
if (!isArray(annotations)) {
annotations = [annotations];
}
permissions.canUndo =
(!isApprover && annotation.isSuggestion) ||
(isApprover && annotation.isUndoableActionForApprover);
const summedPermissions: AnnotationPermissions = new AnnotationPermissions();
permissions.canForceRedaction = annotation.isSkipped && !permissions.canUndo;
for (const annotation of annotations) {
const permissions: AnnotationPermissions = new AnnotationPermissions();
permissions.canUndo =
(!isApprover && annotation.isSuggestion) ||
(isApprover && annotation.isUndoableActionForApprover);
permissions.canAcceptRecommendation = annotation.isRecommendation;
permissions.canForceRedaction = annotation.isSkipped && !permissions.canUndo;
permissions.canMarkAsFalsePositive =
annotation.canBeMarkedAsFalsePositive && !annotation.force;
permissions.canMarkTextOnlyAsFalsePositive =
annotation.canBeMarkedAsFalsePositiveWithTextOnly && !annotation.force;
permissions.canAcceptRecommendation = annotation.isRecommendation;
permissions.canRemoveOrSuggestToRemoveOnlyHere = annotation.isRedacted && !annotation.force;
permissions.canRemoveOrSuggestToRemoveFromDictionary =
annotation.isRedacted &&
!annotation.isManualRedaction &&
annotation.isModifyDictionary &&
!annotation.force;
permissions.canMarkAsFalsePositive =
annotation.canBeMarkedAsFalsePositive && !annotation.force;
permissions.canMarkTextOnlyAsFalsePositive =
annotation.canBeMarkedAsFalsePositiveWithTextOnly && !annotation.force;
permissions.canAcceptSuggestion =
isApprover && (annotation.isSuggestion || annotation.isDeclinedSuggestion);
permissions.canRejectSuggestion =
isApprover &&
(annotation.isSuggestion ||
(annotation.isReadyForAnalysis &&
!permissions.canUndo &&
annotation.superType !== 'pending-analysis'));
permissions.canRemoveOrSuggestToRemoveOnlyHere =
annotation.isRedacted && !annotation.force;
permissions.canRemoveOrSuggestToRemoveFromDictionary =
annotation.isRedacted &&
!annotation.isManualRedaction &&
annotation.isModifyDictionary &&
!annotation.force;
permissions.canChangeLegalBasis = !annotation.isManualRedaction && annotation.isRedacted;
permissions.canAcceptSuggestion =
isApprover && (annotation.isSuggestion || annotation.isDeclinedSuggestion);
permissions.canRejectSuggestion =
isApprover &&
(annotation.isSuggestion ||
(annotation.isReadyForAnalysis &&
!permissions.canUndo &&
annotation.superType !== 'pending-analysis'));
permissions.canRecategorizeImage = annotation.isImage;
permissions.canChangeLegalBasis =
!annotation.isManualRedaction && annotation.isRedacted;
return permissions;
permissions.canRecategorizeImage = annotation.isImage;
summedPermissions._merge(permissions);
}
return summedPermissions;
}
private _merge(permissions: AnnotationPermissions) {
for (const key of Object.keys(this)) {
if (typeof this[key] === 'boolean') {
this[key] = this[key] && permissions[key];
}
}
}
}

View File

@ -76,28 +76,15 @@ export class AnnotationWrapper {
}
get canBeMarkedAsFalsePositive() {
return (
(this.isRecommendation || this.superType === 'redaction') &&
(this.hasTextAfter || this.hasTextBefore) &&
!this.isImage
);
return (this.isRecommendation || this.superType === 'redaction') && (this.hasTextAfter || this.hasTextBefore) && !this.isImage;
}
get canBeMarkedAsFalsePositiveWithTextOnly() {
return (
!this.canBeMarkedAsFalsePositive &&
(this.isRecommendation || this.superType === 'redaction') &&
!this.isImage
);
return !this.canBeMarkedAsFalsePositive && (this.isRecommendation || this.superType === 'redaction') && !this.isImage;
}
get isSuperTypeBasedColor() {
return (
this.isSkipped ||
this.isSuggestion ||
this.isReadyForAnalysis ||
this.isDeclinedSuggestion
);
return this.isSkipped || this.isSuggestion || this.isReadyForAnalysis || this.isDeclinedSuggestion;
}
get isSkipped() {
@ -115,9 +102,7 @@ export class AnnotationWrapper {
get isFalsePositive() {
return (
this.dictionary?.toLowerCase() === 'false_positive' &&
(this.superType === 'skipped' ||
this.superType === 'hint' ||
this.superType === 'redaction')
(this.superType === 'skipped' || this.superType === 'hint' || this.superType === 'redaction')
);
}
@ -168,10 +153,7 @@ export class AnnotationWrapper {
}
get isSuggestionRemove() {
return (
this.superType === 'suggestion-remove' ||
this.superType === 'suggestion-remove-dictionary'
);
return this.superType === 'suggestion-remove' || this.superType === 'suggestion-remove-dictionary';
}
get isModifyDictionary() {
@ -179,10 +161,7 @@ export class AnnotationWrapper {
}
get isConvertedRecommendation() {
return (
this.isRecommendation &&
(this.superType === 'suggestion-add-dictionary' || this.superType === 'add-dictionary')
);
return this.isRecommendation && (this.superType === 'suggestion-add-dictionary' || this.superType === 'add-dictionary');
}
get isRecommendation() {
@ -234,21 +213,13 @@ export class AnnotationWrapper {
return annotationWrapper;
}
private static _handleRecommendations(
annotationWrapper: AnnotationWrapper,
redactionLogEntry: RedactionLogEntryWrapper
) {
private static _handleRecommendations(annotationWrapper: AnnotationWrapper, redactionLogEntry: RedactionLogEntryWrapper) {
if (annotationWrapper.superType === 'recommendation') {
annotationWrapper.recommendationType = redactionLogEntry.type.substr(
'recommendation_'.length
);
annotationWrapper.recommendationType = redactionLogEntry.type.substr('recommendation_'.length);
}
}
private static _setSuperType(
annotationWrapper: AnnotationWrapper,
redactionLogEntryWrapper: RedactionLogEntryWrapper
) {
private static _setSuperType(annotationWrapper: AnnotationWrapper, redactionLogEntryWrapper: RedactionLogEntryWrapper) {
if (redactionLogEntryWrapper.recommendation) {
if (redactionLogEntryWrapper.redacted) {
annotationWrapper.superType = 'recommendation';
@ -288,9 +259,14 @@ export class AnnotationWrapper {
if (annotationWrapper.dictionary?.toLowerCase() === 'false_positive') {
if (redactionLogEntryWrapper.status === 'REQUESTED') {
annotationWrapper.superType = 'suggestion-add-dictionary';
return;
}
if (redactionLogEntryWrapper.status === 'APPROVED') {
annotationWrapper.superType = 'add-dictionary';
return;
}
if (!redactionLogEntryWrapper.manual) {
annotationWrapper.superType = 'skipped';
}
return;
}
@ -299,16 +275,20 @@ export class AnnotationWrapper {
if (redactionLogEntryWrapper.dictionaryEntry) {
if (redactionLogEntryWrapper.status === 'REQUESTED') {
annotationWrapper.superType = 'suggestion-add-dictionary';
return;
}
if (redactionLogEntryWrapper.status === 'APPROVED') {
annotationWrapper.superType = 'add-dictionary';
return;
}
} else {
if (redactionLogEntryWrapper.status === 'REQUESTED') {
annotationWrapper.superType = 'suggestion-add';
return;
}
if (redactionLogEntryWrapper.status === 'APPROVED') {
annotationWrapper.superType = 'manual-redaction';
return;
}
}
}
@ -322,40 +302,29 @@ export class AnnotationWrapper {
: 'suggestion-remove-dictionary';
} else {
annotationWrapper.superType =
redactionLogEntryWrapper.manualRedactionType === 'ADD'
? 'suggestion-add'
: 'suggestion-remove';
redactionLogEntryWrapper.manualRedactionType === 'ADD' ? 'suggestion-add' : 'suggestion-remove';
}
return;
}
if (redactionLogEntryWrapper.status === 'APPROVED') {
if (redactionLogEntryWrapper.dictionaryEntry) {
annotationWrapper.superType =
redactionLogEntryWrapper.manualRedactionType === 'ADD'
? 'add-dictionary'
: 'remove-dictionary';
redactionLogEntryWrapper.manualRedactionType === 'ADD' ? 'add-dictionary' : 'remove-dictionary';
} else {
annotationWrapper.superType =
redactionLogEntryWrapper.manualRedactionType === 'ADD'
? 'manual-redaction'
: 'remove-only-here';
redactionLogEntryWrapper.manualRedactionType === 'ADD' ? 'manual-redaction' : 'remove-only-here';
}
return;
}
}
if (!annotationWrapper.superType) {
annotationWrapper.superType = annotationWrapper.redaction
? 'redaction'
: annotationWrapper.hint
? 'hint'
: 'skipped';
annotationWrapper.superType = annotationWrapper.redaction ? 'redaction' : annotationWrapper.hint ? 'hint' : 'skipped';
}
}
private static _createContent(
annotationWrapper: AnnotationWrapper,
entry: RedactionLogEntryWrapper
) {
private static _createContent(annotationWrapper: AnnotationWrapper, entry: RedactionLogEntryWrapper) {
let content = '';
if (entry.matchedRule) {
content += 'Rule ' + entry.matchedRule + ' matched \n\n';

View File

@ -1,115 +1,131 @@
<div *ngIf="canPerformAnnotationActions" class="annotation-actions">
<div *ngIf="canPerformAnnotationActions" class="annotation-actions" [class.always-visible]="alwaysVisible">
<redaction-circle-button
(action)="annotationActionsService.changeLegalBasis($event, annotation, annotationsChanged)"
(action)="annotationActionsService.changeLegalBasis($event, annotations, annotationsChanged)"
*ngIf="annotationPermissions.canChangeLegalBasis"
icon="red:edit"
tooltip="annotation-actions.edit-reason.label"
type="dark-bg"
[tooltipPosition]="tooltipPosition"
[type]="btnType"
>
</redaction-circle-button>
<redaction-circle-button
(action)="
annotationActionsService.convertRecommendationToAnnotation(
$event,
[annotation],
annotationsChanged
)
"
(action)="annotationActionsService.convertRecommendationToAnnotation($event, annotations, annotationsChanged)"
*ngIf="annotationPermissions.canAcceptRecommendation"
icon="red:check"
tooltip="annotation-actions.accept-recommendation.label"
type="dark-bg"
[tooltipPosition]="tooltipPosition"
[type]="btnType"
>
</redaction-circle-button>
<redaction-circle-button
(action)="
annotationActionsService.acceptSuggestion($event, [annotation], annotationsChanged)
"
(action)="annotationActionsService.acceptSuggestion($event, annotations, annotationsChanged)"
*ngIf="annotationPermissions.canAcceptSuggestion"
icon="red:check"
tooltip="annotation-actions.accept-suggestion.label"
type="dark-bg"
[tooltipPosition]="tooltipPosition"
[type]="btnType"
>
</redaction-circle-button>
<redaction-circle-button
(action)="
annotationActionsService.undoDirectAction($event, [annotation], annotationsChanged)
"
(action)="annotationActionsService.undoDirectAction($event, annotations, annotationsChanged)"
*ngIf="annotationPermissions.canUndo"
icon="red:undo"
tooltip="annotation-actions.undo"
type="dark-bg"
[tooltipPosition]="tooltipPosition"
[type]="btnType"
>
</redaction-circle-button>
<redaction-circle-button
(action)="
annotationActionsService.rejectSuggestion($event, [annotation], annotationsChanged)
"
(action)="annotationActionsService.rejectSuggestion($event, annotations, annotationsChanged)"
*ngIf="annotationPermissions.canRejectSuggestion"
icon="red:trash"
tooltip="annotation-actions.reject-suggestion"
type="dark-bg"
[tooltipPosition]="tooltipPosition"
[type]="btnType"
>
</redaction-circle-button>
<redaction-circle-button
(action)="
annotationActionsService.recategorizeImage($event, annotation, annotationsChanged)
"
(action)="annotationActionsService.recategorizeImage($event, annotations, annotationsChanged)"
*ngIf="annotationPermissions.canRecategorizeImage"
icon="red:thumb-down"
tooltip="annotation-actions.recategorize-image"
type="dark-bg"
[tooltipPosition]="tooltipPosition"
[type]="btnType"
>
</redaction-circle-button>
<redaction-circle-button
(action)="
annotationActionsService.markAsFalsePositive($event, [annotation], annotationsChanged)
"
*ngIf="
annotationPermissions.canMarkTextOnlyAsFalsePositive &&
!annotationPermissions.canPerformMultipleRemoveActions
"
(action)="annotationActionsService.markAsFalsePositive($event, annotations, annotationsChanged)"
*ngIf="annotationPermissions.canMarkTextOnlyAsFalsePositive && !annotationPermissions.canPerformMultipleRemoveActions"
icon="red:thumb-down"
tooltip="annotation-actions.remove-annotation.false-positive"
type="dark-bg"
[tooltipPosition]="tooltipPosition"
[type]="btnType"
>
</redaction-circle-button>
<redaction-circle-button
(action)="annotationActionsService.forceRedaction($event, [annotation], annotationsChanged)"
(action)="annotationActionsService.forceRedaction($event, annotations, annotationsChanged)"
*ngIf="annotationPermissions.canForceRedaction"
icon="red:thumb-up"
tooltip="annotation-actions.force-redaction.label"
type="dark-bg"
[tooltipPosition]="tooltipPosition"
[type]="btnType"
>
</redaction-circle-button>
<redaction-circle-button
(action)="hideAnnotation($event)"
*ngIf="annotation.isImage && viewerAnnotation?.isVisible()"
*ngIf="isImage && isVisible"
icon="red:visibility-off"
tooltip="annotation-actions.hide"
type="dark-bg"
[tooltipPosition]="tooltipPosition"
[type]="btnType"
>
</redaction-circle-button>
<redaction-circle-button
(action)="showAnnotation($event)"
*ngIf="annotation.isImage && !viewerAnnotation?.isVisible()"
*ngIf="isImage && !isVisible"
icon="red:visibility"
tooltip="annotation-actions.show"
type="dark-bg"
[tooltipPosition]="tooltipPosition"
[type]="btnType"
>
</redaction-circle-button>
<redaction-annotation-remove-actions
[annotationsChanged]="annotationsChanged"
[annotations]="[annotation]"
></redaction-annotation-remove-actions>
<redaction-circle-button
(action)="suggestRemoveAnnotations($event, true)"
*ngIf="annotationPermissions.canRemoveOrSuggestToRemoveFromDictionary"
[tooltipPosition]="tooltipPosition"
[type]="btnType"
icon="red:remove-from-dict"
tooltip="annotation-actions.remove-annotation.remove-from-dict"
>
</redaction-circle-button>
<redaction-circle-button
(action)="markAsFalsePositive($event)"
*ngIf="annotationPermissions.canMarkAsFalsePositive"
[tooltipPosition]="tooltipPosition"
[type]="btnType"
icon="red:thumb-down"
tooltip="annotation-actions.remove-annotation.false-positive"
>
</redaction-circle-button>
<redaction-circle-button
(action)="suggestRemoveAnnotations($event, false)"
*ngIf="annotationPermissions.canRemoveOrSuggestToRemoveOnlyHere"
[tooltipPosition]="tooltipPosition"
[type]="btnType"
icon="red:trash"
tooltip="annotation-actions.remove-annotation.only-here"
>
</redaction-circle-button>
</div>

View File

@ -14,4 +14,8 @@
> *:not(:last-child) {
margin-right: 2px;
}
&.always-visible {
display: flex;
}
}

View File

@ -4,7 +4,7 @@ import { AppStateService } from '@state/app-state.service';
import { PermissionsService } from '@services/permissions.service';
import { AnnotationPermissions } from '@models/file/annotation.permissions';
import { AnnotationActionsService } from '../../services/annotation-actions.service';
import { Annotations, WebViewerInstance } from '@pdftron/webviewer';
import { WebViewerInstance } from '@pdftron/webviewer';
@Component({
selector: 'redaction-annotation-actions',
@ -12,9 +12,13 @@ import { Annotations, WebViewerInstance } from '@pdftron/webviewer';
styleUrls: ['./annotation-actions.component.scss']
})
export class AnnotationActionsComponent implements OnInit {
@Input() annotation: AnnotationWrapper;
@Input() btnType: 'dark-bg' | 'primary' = 'dark-bg';
@Input() tooltipPosition: 'before' | 'above' = 'before';
@Input() _annotations: AnnotationWrapper[];
@Input() canPerformAnnotationActions: boolean;
@Input() viewer: WebViewerInstance;
@Input() alwaysVisible: boolean;
@Output() annotationsChanged = new EventEmitter<AnnotationWrapper>();
@ -26,27 +30,62 @@ export class AnnotationActionsComponent implements OnInit {
private _permissionsService: PermissionsService
) {}
get viewerAnnotation(): Annotations.Annotation {
return this.viewer.annotManager.getAnnotationById(this.annotation.id);
get annotations(): AnnotationWrapper[] {
return this._annotations;
}
@Input()
set annotations(value: AnnotationWrapper[]) {
this._annotations = value.filter(a => a !== undefined);
this._setPermissions();
}
get viewerAnnotations() {
if (this.viewer?.annotManager) {
return this._annotations.map(a => this.viewer?.annotManager?.getAnnotationById(a.id));
} else {
return [];
}
}
get isVisible() {
return this.viewerAnnotations?.reduce((accumulator, annotation) => annotation?.isVisible() && accumulator, true);
}
get isImage() {
return this.annotations?.reduce((accumulator, annotation) => annotation.isImage && accumulator, true);
}
ngOnInit(): void {
this.annotationPermissions = AnnotationPermissions.forUser(
this._permissionsService.isApprover(),
this._permissionsService.currentUser,
this.annotation
);
this._setPermissions();
}
suggestRemoveAnnotations($event, removeFromDict: boolean) {
$event.stopPropagation();
this.annotationActionsService.suggestRemoveAnnotation($event, this.annotations, removeFromDict, this.annotationsChanged);
}
markAsFalsePositive($event) {
this.annotationActionsService.markAsFalsePositive($event, this.annotations, this.annotationsChanged);
}
hideAnnotation($event: MouseEvent) {
$event.stopPropagation();
this.viewer.annotManager.hideAnnotations([this.viewerAnnotation]);
this.viewer.annotManager.hideAnnotations(this.viewerAnnotations);
this.viewer.annotManager.deselectAllAnnotations();
}
showAnnotation($event: MouseEvent) {
$event.stopPropagation();
this.viewer.annotManager.showAnnotations([this.viewerAnnotation]);
this.viewer.annotManager.showAnnotations(this.viewerAnnotations);
this.viewer.annotManager.deselectAllAnnotations();
}
private _setPermissions() {
this.annotationPermissions = AnnotationPermissions.forUser(
this._permissionsService.isApprover(),
this._permissionsService.currentUser,
this.annotations
);
}
}

View File

@ -1,29 +0,0 @@
<redaction-circle-button
(action)="suggestRemoveAnnotations($event, true)"
*ngIf="permissions.canRemoveOrSuggestToRemoveFromDictionary"
[tooltipPosition]="tooltipPosition"
[type]="btnType"
icon="red:remove-from-dict"
tooltip="annotation-actions.remove-annotation.remove-from-dict"
>
</redaction-circle-button>
<redaction-circle-button
(action)="markAsFalsePositive($event)"
*ngIf="permissions.canMarkAsFalsePositive"
[tooltipPosition]="tooltipPosition"
[type]="btnType"
icon="red:thumb-down"
tooltip="annotation-actions.remove-annotation.false-positive"
>
</redaction-circle-button>
<redaction-circle-button
(action)="suggestRemoveAnnotations($event, false)"
*ngIf="permissions.canRemoveOrSuggestToRemoveOnlyHere"
[tooltipPosition]="tooltipPosition"
[type]="btnType"
icon="red:trash"
tooltip="annotation-actions.remove-annotation.only-here"
>
</redaction-circle-button>

View File

@ -1,7 +0,0 @@
:host {
display: flex;
> *:not(:last-child) {
margin-right: 2px;
}
}

View File

@ -1,93 +0,0 @@
import { Component, EventEmitter, Input, Output, ViewChild } from '@angular/core';
import { AppStateService } from '@state/app-state.service';
import { AnnotationWrapper } from '@models/file/annotation.wrapper';
import { AnnotationActionsService } from '../../services/annotation-actions.service';
import { AnnotationPermissions } from '@models/file/annotation.permissions';
import { PermissionsService } from '@services/permissions.service';
import { MatMenuTrigger } from '@angular/material/menu';
@Component({
selector: 'redaction-annotation-remove-actions',
templateUrl: './annotation-remove-actions.component.html',
styleUrls: ['./annotation-remove-actions.component.scss']
})
export class AnnotationRemoveActionsComponent {
@Output() menuOpenChange = new EventEmitter<boolean>();
@Input() annotationsChanged: EventEmitter<AnnotationWrapper>;
@Input() menuOpen: boolean;
@Input() btnType: 'dark-bg' | 'primary' = 'dark-bg';
@Input() tooltipPosition: 'before' | 'above' = 'before';
@ViewChild(MatMenuTrigger) matMenuTrigger: MatMenuTrigger;
permissions: {
canRemoveOrSuggestToRemoveOnlyHere: boolean;
canRemoveOrSuggestToRemoveFromDictionary: boolean;
canMarkAsFalsePositive: boolean;
};
constructor(
readonly appStateService: AppStateService,
private readonly _annotationActionsService: AnnotationActionsService,
private readonly _permissionsService: PermissionsService
) {}
private _annotations: AnnotationWrapper[];
get annotations(): AnnotationWrapper[] {
return this._annotations;
}
@Input()
set annotations(value: AnnotationWrapper[]) {
this._annotations = value.filter(a => a !== undefined);
this._setPermissions();
}
suggestRemoveAnnotations($event, removeFromDict: boolean) {
$event.stopPropagation();
this._annotationActionsService.suggestRemoveAnnotation(
$event,
this.annotations,
removeFromDict,
this.annotationsChanged
);
}
markAsFalsePositive($event) {
this._annotationActionsService.markAsFalsePositive(
$event,
this.annotations,
this.annotationsChanged
);
}
private _setPermissions() {
this.permissions = {
canRemoveOrSuggestToRemoveOnlyHere: this._annotationsPermissions([
'canRemoveOrSuggestToRemoveOnlyHere'
]),
canRemoveOrSuggestToRemoveFromDictionary: this._annotationsPermissions([
'canRemoveOrSuggestToRemoveFromDictionary'
]),
canMarkAsFalsePositive: this._annotationsPermissions([
'canMarkAsFalsePositive',
'canMarkTextOnlyAsFalsePositive'
])
};
}
private _annotationsPermissions(keys: string[]): boolean {
return this.annotations.reduce((prevValue, annotation) => {
const annotationPermissions = AnnotationPermissions.forUser(
this._permissionsService.isApprover(),
this._permissionsService.currentUser,
annotation
);
const hasAtLeastOnePermission = keys.reduce(
(acc, key) => acc || annotationPermissions[key],
false
);
return prevValue && hasAtLeastOnePermission;
}, true);
}
}

View File

@ -1,8 +1,4 @@
<div
*ngIf="!excludePages"
class="right-title heading"
translate="file-preview.tabs.annotations.label"
>
<div *ngIf="!excludePages" class="right-title heading" translate="file-preview.tabs.annotations.label">
<div>
<div
(click)="multiSelectActive = true"
@ -20,11 +16,7 @@
></redaction-popup-filter>
</div>
</div>
<div
*ngIf="excludePages"
class="right-title heading"
translate="file-preview.tabs.exclude-pages.label"
>
<div *ngIf="excludePages" class="right-title heading" translate="file-preview.tabs.exclude-pages.label">
<div>
<redaction-circle-button
(action)="actionPerformed.emit('view-exclude-pages')"
@ -53,19 +45,18 @@
type="red-bg"
></redaction-round-checkbox>
<span class="all-caps-label">{{ selectedAnnotations?.length || 0 }} selected </span>
<redaction-annotation-remove-actions
<redaction-annotation-actions
*ngIf="selectedAnnotations?.length > 0"
[annotationsChanged]="annotationsChanged"
(annotationsChanged)="annotationsChanged.emit($event)"
[canPerformAnnotationActions]="!isReadOnly"
[annotations]="selectedAnnotations"
[viewer]="viewer"
[alwaysVisible]="true"
btnType="primary"
tooltipPosition="above"
></redaction-annotation-remove-actions>
></redaction-annotation-actions>
</div>
<redaction-circle-button
(action)="multiSelectActive = false"
icon="red:close"
type="primary"
></redaction-circle-button>
<redaction-circle-button (action)="multiSelectActive = false" icon="red:close" type="primary"></redaction-circle-button>
</div>
<div [class.lower-height]="multiSelectActive || isReadOnly" class="annotations-wrapper">
<div
@ -112,11 +103,7 @@
<span *ngIf="!!activeViewerPage" class="all-caps-label">
<span translate="page"></span> {{ activeViewerPage }} -
{{ activeAnnotationsLength || 0 }}
<span
[translate]="
activeAnnotationsLength === 1 ? 'annotation' : 'annotations'
"
></span>
<span [translate]="activeAnnotationsLength === 1 ? 'annotation' : 'annotations'"></span>
</span>
<div *ngIf="multiSelectActive">
@ -142,25 +129,11 @@
redactionHasScrollbar
tabindex="1"
>
<ng-container
*ngIf="activeViewerPage && !displayedAnnotations[activeViewerPage]"
>
<redaction-empty-state
[horizontalPadding]="24"
[verticalPadding]="40"
icon="red:document"
screen="file-preview"
>
<ng-container
*ngIf="
fileData?.fileStatus?.excludedPages?.includes(activeViewerPage)
"
>
<ng-container *ngIf="activeViewerPage && !displayedAnnotations[activeViewerPage]">
<redaction-empty-state [horizontalPadding]="24" [verticalPadding]="40" icon="red:document" screen="file-preview">
<ng-container *ngIf="fileData?.fileStatus?.excludedPages?.includes(activeViewerPage)">
{{ 'file-preview.tabs.annotations.page-is' | translate }}
<a
(click)="actionPerformed.emit('view-exclude-pages')"
translate="file-preview.excluded-from-redaction"
>
<a (click)="actionPerformed.emit('view-exclude-pages')" translate="file-preview.excluded-from-redaction">
</a
>.
</ng-container>
@ -175,9 +148,7 @@
></redaction-icon-button>
<redaction-icon-button
(action)="jumpToNextWithAnnotations()"
[disabled]="
activeViewerPage >= displayedPages[displayedPages.length - 1]
"
[disabled]="activeViewerPage >= displayedPages[displayedPages.length - 1]"
class="mt-8"
icon="red:nav-next"
text="file-preview.tabs.annotations.jump-to-next"
@ -188,9 +159,7 @@
<div
(click)="annotationClicked(annotation, $event)"
*ngFor="
let annotation of displayedAnnotations[activeViewerPage]?.annotations
"
*ngFor="let annotation of displayedAnnotations[activeViewerPage]?.annotations"
[class.active]="isSelected(annotation)"
[class.multi-select-active]="multiSelectActive"
attr.annotation-id="{{ annotation.id }}"
@ -199,18 +168,9 @@
>
<div class="active-bar-marker"></div>
<div [class.removed]="annotation.isChangeLogRemoved" class="annotation">
<redaction-hidden-action
(action)="logAnnotation(annotation)"
[requiredClicks]="2"
>
<div
[matTooltip]="annotation.content"
class="details"
matTooltipPosition="above"
>
<redaction-type-annotation-icon
[annotation]="annotation"
></redaction-type-annotation-icon>
<redaction-hidden-action (action)="logAnnotation(annotation)" [requiredClicks]="2">
<div [matTooltip]="annotation.content" class="details" matTooltipPosition="above">
<redaction-type-annotation-icon [annotation]="annotation"></redaction-type-annotation-icon>
<div class="flex-1">
<div>
<strong>{{ annotation.typeLabel | translate }}</strong>
@ -222,8 +182,7 @@
>{{ annotation.dictionary | humanize: false }}
</div>
<div *ngIf="annotation.shortContent && !annotation.isHint">
<strong><span translate="content"></span>: </strong
>{{ annotation.shortContent }}
<strong><span translate="content"></span>: </strong>{{ annotation.shortContent }}
</div>
</div>
@ -269,14 +228,9 @@
</div>
<ng-template #annotationFilterTemplate let-filter="filter">
<redaction-type-filter
*ngIf="_(filter).topLevelFilter"
[filter]="filter"
></redaction-type-filter>
<redaction-type-filter *ngIf="_(filter).topLevelFilter" [filter]="filter"></redaction-type-filter>
<ng-container *ngIf="!_(filter).topLevelFilter">
<redaction-dictionary-annotation-icon
[dictionaryKey]="filter.key"
></redaction-dictionary-annotation-icon>
<redaction-dictionary-annotation-icon [dictionaryKey]="filter.key"></redaction-dictionary-annotation-icon>
{{ filter.key | humanize: false }}
</ng-container>
</ng-template>

View File

@ -21,6 +21,7 @@ import { FilterModel } from '@shared/components/filters/popup-filter/model/filte
import { CommentsComponent } from '../comments/comments.component';
import { PermissionsService } from '../../../../services/permissions.service';
import { TranslateService } from '@ngx-translate/core';
import { WebViewerInstance } from '@pdftron/webviewer';
const COMMAND_KEY_ARRAY = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Escape'];
const ALL_HOTKEY_ARRAY = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'];
@ -42,10 +43,9 @@ export class FileWorkloadComponent {
@Input() hideSkipped: boolean;
@Input() excludePages: boolean;
@Input() annotationActionsTemplate: TemplateRef<any>;
@Input() viewer: WebViewerInstance;
@Output() shouldDeselectAnnotationsOnPageChangeChange = new EventEmitter<boolean>();
@Output() selectAnnotations = new EventEmitter<
AnnotationWrapper[] | { annotations: AnnotationWrapper[]; multiSelect: boolean }
>();
@Output() selectAnnotations = new EventEmitter<AnnotationWrapper[] | { annotations: AnnotationWrapper[]; multiSelect: boolean }>();
@Output() deselectAnnotations = new EventEmitter<AnnotationWrapper[]>();
@Output() selectPage = new EventEmitter<number>();
@Output() toggleSkipped = new EventEmitter<any>();
@ -103,10 +103,7 @@ export class FileWorkloadComponent {
return this.selectedAnnotations?.length ? this.selectedAnnotations[0] : null;
}
private static _scrollToFirstElement(
elements: HTMLElement[],
mode: 'always' | 'if-needed' = 'if-needed'
) {
private static _scrollToFirstElement(elements: HTMLElement[], mode: 'always' | 'if-needed' = 'if-needed') {
if (elements.length > 0) {
scrollIntoView(elements[0], {
behavior: 'smooth',
@ -128,9 +125,7 @@ export class FileWorkloadComponent {
toggleExpandComments(annotation: AnnotationWrapper, $event: MouseEvent) {
$event.stopPropagation();
this.annotationCommentsComponents
.find(c => c.annotation === annotation)
.toggleExpandComments();
this.annotationCommentsComponents.find(c => c.annotation === annotation).toggleExpandComments();
}
logAnnotation(annotation: AnnotationWrapper) {
@ -138,9 +133,7 @@ export class FileWorkloadComponent {
}
pageHasSelection(page: number) {
return (
this.multiSelectActive && !!this.selectedAnnotations?.find(a => a.pageNumber === page)
);
return this.multiSelectActive && !!this.selectedAnnotations?.find(a => a.pageNumber === page);
}
selectAllOnActivePage() {
@ -227,20 +220,14 @@ export class FileWorkloadComponent {
scrollAnnotationsToPage(page: number, mode: 'always' | 'if-needed' = 'if-needed') {
if (this._annotationsElement) {
const elements: any[] = this._annotationsElement.nativeElement.querySelectorAll(
`div[anotation-page-header="${page}"]`
);
const elements: any[] = this._annotationsElement.nativeElement.querySelectorAll(`div[anotation-page-header="${page}"]`);
FileWorkloadComponent._scrollToFirstElement(elements, mode);
}
}
@debounce()
scrollToSelectedAnnotation() {
if (
!this.selectedAnnotations ||
this.selectedAnnotations.length === 0 ||
!this._annotationsElement
) {
if (!this.selectedAnnotations || this.selectedAnnotations.length === 0 || !this._annotationsElement) {
return;
}
const elements: any[] = this._annotationsElement.nativeElement.querySelectorAll(
@ -251,10 +238,7 @@ export class FileWorkloadComponent {
scrollQuickNavigation() {
let quickNavPageIndex = this.displayedPages.findIndex(p => p >= this.activeViewerPage);
if (
quickNavPageIndex === -1 ||
this.displayedPages[quickNavPageIndex] !== this.activeViewerPage
) {
if (quickNavPageIndex === -1 || this.displayedPages[quickNavPageIndex] !== this.activeViewerPage) {
quickNavPageIndex = Math.max(0, quickNavPageIndex - 1);
}
this._scrollQuickNavigationToPage(this.displayedPages[quickNavPageIndex]);
@ -274,10 +258,7 @@ export class FileWorkloadComponent {
}
preventKeyDefault($event: KeyboardEvent) {
if (
COMMAND_KEY_ARRAY.includes($event.key) &&
!(($event.target as any).localName === 'input')
) {
if (COMMAND_KEY_ARRAY.includes($event.key) && !(($event.target as any).localName === 'input')) {
$event.preventDefault();
}
}
@ -296,26 +277,18 @@ export class FileWorkloadComponent {
private _selectFirstAnnotationOnCurrentPageIfNecessary() {
if (
(!this._firstSelectedAnnotation ||
this.activeViewerPage !== this._firstSelectedAnnotation.pageNumber) &&
(!this._firstSelectedAnnotation || this.activeViewerPage !== this._firstSelectedAnnotation.pageNumber) &&
this.displayedPages.indexOf(this.activeViewerPage) >= 0
) {
this.selectAnnotations.emit([
this.displayedAnnotations[this.activeViewerPage].annotations[0]
]);
this.selectAnnotations.emit([this.displayedAnnotations[this.activeViewerPage].annotations[0]]);
}
}
private _navigateAnnotations($event: KeyboardEvent) {
if (
!this._firstSelectedAnnotation ||
this.activeViewerPage !== this._firstSelectedAnnotation.pageNumber
) {
if (!this._firstSelectedAnnotation || this.activeViewerPage !== this._firstSelectedAnnotation.pageNumber) {
if (this.displayedPages.indexOf(this.activeViewerPage) !== -1) {
// Displayed page has annotations
return this.selectAnnotations.emit([
this.displayedAnnotations[this.activeViewerPage].annotations[0]
]);
return this.selectAnnotations.emit([this.displayedAnnotations[this.activeViewerPage].annotations[0]]);
}
// Displayed page doesn't have annotations
if ($event.key === 'ArrowDown') {
@ -345,8 +318,7 @@ export class FileWorkloadComponent {
this.selectAnnotations.emit([annotationsOnPage[idx + 1]]);
} else if (pageIdx + 1 < this.displayedPages.length) {
// If not last page
const nextPageAnnotations =
this.displayedAnnotations[this.displayedPages[pageIdx + 1]].annotations;
const nextPageAnnotations = this.displayedAnnotations[this.displayedPages[pageIdx + 1]].annotations;
this.shouldDeselectAnnotationsOnPageChange = false;
this.shouldDeselectAnnotationsOnPageChangeChange.emit(false);
this.selectAnnotations.emit([nextPageAnnotations[0]]);
@ -356,8 +328,7 @@ export class FileWorkloadComponent {
this.selectAnnotations.emit([annotationsOnPage[idx - 1]]);
} else if (pageIdx) {
// If not first page
const prevPageAnnotations =
this.displayedAnnotations[this.displayedPages[pageIdx - 1]].annotations;
const prevPageAnnotations = this.displayedAnnotations[this.displayedPages[pageIdx - 1]].annotations;
this.shouldDeselectAnnotationsOnPageChange = false;
this.shouldDeselectAnnotationsOnPageChangeChange.emit(false);
this.selectAnnotations.emit([prevPageAnnotations[prevPageAnnotations.length - 1]]);
@ -421,9 +392,7 @@ export class FileWorkloadComponent {
private _scrollQuickNavigationToPage(page: number) {
if (this._quickNavigationElement) {
const elements: any[] = this._quickNavigationElement.nativeElement.querySelectorAll(
`#quick-nav-page-${page}`
);
const elements: any[] = this._quickNavigationElement.nativeElement.querySelectorAll(`#quick-nav-page-${page}`);
FileWorkloadComponent._scrollToFirstElement(elements);
}
}

View File

@ -30,11 +30,13 @@ export class ChangeLegalBasisDialogComponent implements OnInit {
private readonly _permissionsService: PermissionsService,
private readonly _formBuilder: FormBuilder,
public dialogRef: MatDialogRef<ChangeLegalBasisDialogComponent>,
@Inject(MAT_DIALOG_DATA) public annotation: AnnotationWrapper
@Inject(MAT_DIALOG_DATA) public annotations: AnnotationWrapper[]
) {}
get changed(): boolean {
return this.legalBasisForm.get('reason').value.legalBasis !== this.annotation.legalBasis;
return (
this.legalBasisForm.get('reason').value.legalBasis !== this.annotations[0].legalBasis
);
}
async ngOnInit() {
@ -58,7 +60,7 @@ export class ChangeLegalBasisDialogComponent implements OnInit {
this.legalBasisForm.patchValue({
reason: this.legalOptions.find(
option => option.legalBasis === this.annotation.legalBasis
option => option.legalBasis === this.annotations[0].legalBasis
)
});
}

View File

@ -18,18 +18,18 @@ export class RecategorizeImageDialogComponent implements OnInit {
private readonly _permissionsService: PermissionsService,
private readonly _formBuilder: FormBuilder,
public dialogRef: MatDialogRef<RecategorizeImageDialogComponent>,
@Inject(MAT_DIALOG_DATA) public annotation: AnnotationWrapper
@Inject(MAT_DIALOG_DATA) public annotations: AnnotationWrapper[]
) {}
get changed(): boolean {
return this.recategorizeImageForm.get('type').value !== this.annotation.dictionary;
return this.recategorizeImageForm.get('type').value !== this.annotations[0].dictionary;
}
async ngOnInit() {
this.isDocumentAdmin = this._permissionsService.isApprover();
this.recategorizeImageForm = this._formBuilder.group({
type: [this.annotation.dictionary, Validators.required],
type: [this.annotations[0].dictionary, Validators.required],
comment: this.isDocumentAdmin ? [null] : [null, Validators.required]
});
}

View File

@ -34,7 +34,6 @@ import { PdfViewerDataService } from './services/pdf-viewer-data.service';
import { ManualAnnotationService } from './services/manual-annotation.service';
import { AnnotationDrawService } from './services/annotation-draw.service';
import { AnnotationProcessingService } from './services/annotation-processing.service';
import { AnnotationRemoveActionsComponent } from './components/annotation-remove-actions/annotation-remove-actions.component';
import { DossierDictionaryDialogComponent } from './dialogs/dossier-dictionary-dialog/dossier-dictionary-dialog.component';
import { EditDossierDialogComponent } from './dialogs/edit-dossier-dialog/edit-dossier-dialog.component';
import { EditDossierGeneralInfoComponent } from './dialogs/edit-dossier-dialog/general-info/edit-dossier-general-info.component';
@ -84,7 +83,6 @@ const components = [
DossierListingActionsComponent,
DocumentInfoComponent,
FileWorkloadComponent,
AnnotationRemoveActionsComponent,
EditDossierGeneralInfoComponent,
EditDossierDownloadPackageComponent,
EditDossierDictionaryComponent,

View File

@ -32,8 +32,7 @@
<div *ngIf="viewReady" class="flex-1 actions-container">
<ng-container *ngIf="!appStateService.activeFile.isExcluded">
<ng-container *ngIf="!appStateService.activeFile.isProcessing">
<redaction-status-bar [config]="statusBarConfig" [small]="true">
</redaction-status-bar>
<redaction-status-bar [config]="statusBarConfig" [small]="true"> </redaction-status-bar>
<div class="all-caps-label mr-16 ml-8">
{{ status | translate }}
@ -84,10 +83,7 @@
<ng-container *ngIf="permissionsService.isApprover() && !!lastReviewer">
<div class="vertical-line"></div>
<div class="all-caps-label mr-16 ml-8" translate="file-preview.last-reviewer"></div>
<redaction-initials-avatar
[userId]="lastReviewer"
[withName]="true"
></redaction-initials-avatar>
<redaction-initials-avatar [userId]="lastReviewer" [withName]="true"></redaction-initials-avatar>
</ng-container>
<div class="vertical-line"></div>
@ -153,12 +149,7 @@
<div class="right-container">
<redaction-empty-state
*ngIf="
viewReady &&
appStateService.activeFile.isExcluded &&
!viewDocumentInfo &&
!excludePages
"
*ngIf="viewReady && appStateService.activeFile.isExcluded && !viewDocumentInfo && !excludePages"
[horizontalPadding]="40"
icon="red:needs-work"
text="file-preview.tabs.is-excluded"
@ -186,6 +177,7 @@
[dialogRef]="dialogRef"
[excludePages]="excludePages"
[fileData]="fileData"
[viewer]="activeViewer"
[hideSkipped]="hideSkipped"
[primaryFilters]="primaryFilters"
[secondaryFilters]="secondaryFilters"
@ -195,13 +187,12 @@
</div>
</section>
<redaction-full-page-loading-indicator [displayed]="!viewReady">
</redaction-full-page-loading-indicator>
<redaction-full-page-loading-indicator [displayed]="!viewReady"> </redaction-full-page-loading-indicator>
<ng-template #annotationActionsTemplate let-annotation="annotation">
<redaction-annotation-actions
(annotationsChanged)="annotationsChangedByReviewAction($event)"
[annotation]="annotation"
[annotations]="[annotation]"
[canPerformAnnotationActions]="canPerformAnnotationActions"
[viewer]="activeViewer"
></redaction-annotation-actions>

View File

@ -76,22 +76,24 @@ export class AnnotationActionsService {
changeLegalBasis(
$event: MouseEvent,
annotation: AnnotationWrapper,
annotations: AnnotationWrapper[],
annotationsChanged: EventEmitter<AnnotationWrapper>
) {
this._dialogService.openChangeLegalBasisDialog(
$event,
annotation,
annotations,
(data: { comment: string; legalBasis: string }) => {
this._processObsAndEmit(
this._manualAnnotationService.changeLegalBasis(
annotation.annotationId,
data.legalBasis,
data.comment
),
annotation,
annotationsChanged
);
annotations.forEach(annotation => {
this._processObsAndEmit(
this._manualAnnotationService.changeLegalBasis(
annotation.annotationId,
data.legalBasis,
data.comment
),
annotation,
annotationsChanged
);
});
}
);
}
@ -142,22 +144,24 @@ export class AnnotationActionsService {
recategorizeImage(
$event: MouseEvent,
annotation: AnnotationWrapper,
annotations: AnnotationWrapper[],
annotationsChanged: EventEmitter<AnnotationWrapper>
) {
this._dialogService.openRecategorizeImageDialog(
$event,
annotation,
annotations,
(data: { type: string; comment: string }) => {
this._processObsAndEmit(
this._manualAnnotationService.recategorizeImage(
annotation.annotationId,
data.type,
data.comment
),
annotation,
annotationsChanged
);
annotations.forEach(annotation => {
this._processObsAndEmit(
this._manualAnnotationService.recategorizeImage(
annotation.annotationId,
data.type,
data.comment
),
annotation,
annotationsChanged
);
});
}
);
}
@ -218,7 +222,7 @@ export class AnnotationActionsService {
title: this._translateService.instant('annotation-actions.recategorize-image'),
onClick: () => {
this._ngZone.run(() => {
this.recategorizeImage(null, annotations[0], annotationsChanged);
this.recategorizeImage(null, annotations, annotationsChanged);
});
}
});

View File

@ -150,13 +150,13 @@ export class DossiersDialogService extends DialogService<DialogType> {
openChangeLegalBasisDialog(
$event: MouseEvent,
annotation: AnnotationWrapper,
annotations: AnnotationWrapper[],
cb?: Function
): MatDialogRef<ChangeLegalBasisDialogComponent> {
$event?.stopPropagation();
const ref = this._dialog.open(ChangeLegalBasisDialogComponent, {
...dialogConfig,
data: annotation
data: annotations
});
ref.afterClosed().subscribe(async result => {
if (result && cb) {
@ -168,13 +168,13 @@ export class DossiersDialogService extends DialogService<DialogType> {
openRecategorizeImageDialog(
$event: MouseEvent,
annotation: AnnotationWrapper,
annotations: AnnotationWrapper[],
cb?: Function
): MatDialogRef<RecategorizeImageDialogComponent> {
$event?.stopPropagation();
const ref = this._dialog.open(RecategorizeImageDialogComponent, {
...dialogConfig,
data: annotation
data: annotations
});
ref.afterClosed().subscribe(async result => {
if (result && cb) {

View File

@ -1,6 +1,6 @@
{
"OAUTH_URL": "https://red-staging.iqser.cloud/auth/realms/redaction",
"API_URL": "https://red-staging.iqser.cloud/redaction-gateway-v1",
"OAUTH_URL": "https://dev-06.iqser.cloud/auth/realms/redaction",
"API_URL": "https://dev-06.iqser.cloud/redaction-gateway-v1",
"OAUTH_CLIENT_ID": "redaction",
"BACKEND_APP_VERSION": "4.4.40",
"FRONTEND_APP_VERSION": "1.1",