rework of annotations

This commit is contained in:
Timo 2021-01-08 11:34:07 +02:00
parent 3f7237993d
commit 959bde335c
9 changed files with 257 additions and 207 deletions

View File

@ -26,19 +26,13 @@ export class AnnotationActionsService {
public canRejectSuggestion(annotation: AnnotationWrapper): boolean {
// i can reject whatever i may not undo
return (
!this.canUndoAnnotation(annotation) &&
((this.canAcceptSuggestion(annotation) && !annotation.isDeclinedSuggestion) || (annotation.isModifyDictionary && !annotation.isDeclinedSuggestion))
);
return !this.canUndoAnnotation(annotation) && this.canAcceptSuggestion(annotation) && !annotation.isDeclinedSuggestion;
}
public canDirectlySuggestToRemoveAnnotation(annotation: AnnotationWrapper): boolean {
return (
// annotation.isHint || // HINTS CAN NO LONGER BE REMOVED DIRECTLY ONLY VIA DICTIONARY ACTION
annotation.isManual &&
this._permissionsService.isManagerAndOwner() &&
!this.canUndoAnnotation(annotation) &&
!annotation.isRecommendation
annotation.isManualRedaction && this._permissionsService.isManagerAndOwner() && !this.canUndoAnnotation(annotation) && !annotation.isRecommendation
);
}
@ -47,7 +41,8 @@ export class AnnotationActionsService {
}
public canConvertRecommendationToAnnotation(annotation: AnnotationWrapper): boolean {
return annotation.isRecommendation;
// recommendations that have not already been turned into a suggestion
return annotation.isRecommendation && !annotation.isConvertedRecommendation;
}
public canUndoAnnotation(annotation: AnnotationWrapper): boolean {
@ -60,6 +55,7 @@ export class AnnotationActionsService {
public acceptSuggestion($event: MouseEvent, annotation: AnnotationWrapper, annotationsChanged: EventEmitter<AnnotationWrapper>) {
$event?.stopPropagation();
console.log(annotation.isModifyDictionary);
this._processObsAndEmit(this._manualAnnotationService.approveRequest(annotation.id, annotation.isModifyDictionary), annotation, annotationsChanged);
}

View File

@ -1,4 +1,4 @@
import { Component, Input } from '@angular/core';
import { Component, Input, OnInit } from '@angular/core';
import { TypeValue } from '@redaction/red-ui-http';
@Component({

View File

@ -18,13 +18,7 @@ export class TypeAnnotationIconComponent implements OnChanges {
ngOnChanges(): void {
if (this.annotation) {
if (
this.annotation.isSuggestion ||
this.annotation.isDeclinedSuggestion ||
this.annotation.isModifyDictionary ||
this.annotation.isIgnored ||
this.annotation.isReadyForAnalysis
) {
if (this.annotation.isSuperTypeBasedColor) {
this.color = this._appStateService.getDictionaryColor(this.annotation.superType);
} else {
this.color = this._appStateService.getDictionaryColor(this.annotation.dictionary);

View File

@ -150,12 +150,12 @@ export class DialogService {
const ref = this._dialog.open(ConfirmationDialogComponent, {
...dialogConfig,
data: new ConfirmationDialogInput({
title: annotation.isManual
title: annotation.isManualRedaction
? 'confirmation-dialog.remove-manual-redaction.title'
: removeFromDictionary
? 'confirmation-dialog.remove-from-dictionary.title'
: 'confirmation-dialog.remove-only-here.title',
question: annotation.isManual
question: annotation.isManualRedaction
? 'confirmation-dialog.remove-manual-redaction.question'
: removeFromDictionary
? 'confirmation-dialog.remove-from-dictionary.question'

View File

@ -206,7 +206,7 @@
</redaction-page-indicator>
</div>
<div style="overflow: hidden;">
<div style="overflow: hidden; width: 100%;">
<div attr.anotation-page-header="{{ activeViewerPage }}" class="page-separator">
<span *ngIf="!!activeViewerPage" class="all-caps-label"
><span translate="page"></span> {{ activeViewerPage }} - {{ displayedAnnotations[activeViewerPage]?.annotations?.length || 0 }}

View File

@ -1,6 +1,5 @@
import { Comment, IdRemoval, ManualRedactionEntry, Point, Rectangle, RedactionLogEntry, TypeValue } from '@redaction/red-ui-http';
import { UserWrapper } from '../../../user/user.service';
import { FileStatusWrapper } from './file-status.wrapper';
import { Comment, Point, Rectangle } from '@redaction/red-ui-http';
import { RedactionLogEntryWrapper } from './redaction-log-entry.wrapper';
export class AnnotationWrapper {
superType:
@ -12,17 +11,16 @@ export class AnnotationWrapper {
| 'suggestion-remove'
| 'ignore'
| 'redaction'
| 'manual'
| 'manual-redaction'
| 'hint'
| 'declined-suggestion';
dictionary: string;
color: string;
comments: Comment[] = [];
firstTopLeftPoint: Point;
id: string;
annotationId: string;
content: string;
value: string;
manual: boolean;
userId: string;
typeLabel: string;
pageNumber: number;
@ -30,14 +28,19 @@ export class AnnotationWrapper {
redaction: boolean;
status: string;
dictionaryOperation: boolean;
recommendation: boolean;
positions: Rectangle[];
recommendation: boolean;
recommendationType: string;
pendingRecommendationAnnotationId: string;
textAfter?: string;
textBefore?: string;
constructor() {}
get isDictionaryBased() {
return (this.isHint || this.isRedacted) && !this.isManual;
return (this.isHint || this.isRedacted) && !this.isManualRedaction;
}
get descriptor() {
@ -58,6 +61,10 @@ export class AnnotationWrapper {
return this.isRedacted && (this.hasTextAfter || this.hasTextBefore);
}
get isSuperTypeBasedColor() {
return this.isIgnored || this.isSuggestion || this.isReadyForAnalysis || this.isDeclinedSuggestion;
}
get isIgnored() {
return this.superType === 'ignore';
}
@ -66,8 +73,8 @@ export class AnnotationWrapper {
return this.dictionary === 'false_positive' && (this.superType === 'ignore' || this.superType === 'hint' || this.superType === 'redaction');
}
get isManual() {
return this.superType === 'manual';
get isManualRedaction() {
return this.superType === 'manual-redaction';
}
get isRedactedOrIgnored() {
@ -110,127 +117,18 @@ export class AnnotationWrapper {
return this.dictionaryOperation;
}
get isConvertedRecommendation() {
return this.isRecommendation && (this.superType === 'suggestion-add-dictionary' || this.superType === 'add-dictionary');
}
get isRecommendation() {
return this.recommendation;
}
static fromData(
user: UserWrapper,
dictionaryData: { [p: string]: TypeValue },
fileStatus: FileStatusWrapper,
redactionLogEntry?: RedactionLogEntry,
manualRedactionEntry?: ManualRedactionEntry,
idRemoval?: IdRemoval,
comments?: Comment[]
) {
// manual annotations that have been undone
if (redactionLogEntry?.type === 'manual' && redactionLogEntry?.manual === true && !manualRedactionEntry) {
return;
}
// for declined manual add to dict requests
if (manualRedactionEntry?.status === 'DECLINED' && manualRedactionEntry?.type !== 'manual') {
manualRedactionEntry.addToDictionary = true;
}
// there has been an undo on a redaction remove
if (redactionLogEntry?.manualRedactionType === 'REMOVE' && !idRemoval && !redactionLogEntry.hint) {
redactionLogEntry.redacted = true;
}
const annotationWrapper = new AnnotationWrapper();
annotationWrapper.recommendation = redactionLogEntry ? redactionLogEntry.recommendation : false;
if (annotationWrapper.recommendation) {
// if we have a manual redaction entry for a recommendation, hide the recommendation
if (manualRedactionEntry) {
return;
}
annotationWrapper.recommendationType = redactionLogEntry.type.substr('recommendation_'.length);
}
annotationWrapper.comments = comments ? comments : [];
annotationWrapper.userId = manualRedactionEntry?.user || idRemoval?.user;
if (redactionLogEntry) {
annotationWrapper.id = redactionLogEntry.id;
annotationWrapper.redaction = redactionLogEntry.redacted;
annotationWrapper.hint = redactionLogEntry.hint;
annotationWrapper.dictionary = redactionLogEntry.type;
annotationWrapper.value = redactionLogEntry.value;
annotationWrapper.firstTopLeftPoint = redactionLogEntry.positions[0]?.topLeft;
annotationWrapper.pageNumber = redactionLogEntry.positions[0]?.page;
annotationWrapper.positions = redactionLogEntry.positions;
annotationWrapper.content = AnnotationWrapper.createContent(redactionLogEntry);
annotationWrapper.status = redactionLogEntry.status;
annotationWrapper.textBefore = redactionLogEntry.textBefore;
annotationWrapper.textAfter = redactionLogEntry.textAfter;
} else {
// no redaction log entry - not yet processed
const dictionary = dictionaryData[manualRedactionEntry.type];
annotationWrapper.id = manualRedactionEntry.id;
annotationWrapper.redaction = !dictionary.hint;
annotationWrapper.hint = dictionary.hint;
annotationWrapper.dictionary = manualRedactionEntry.type;
annotationWrapper.firstTopLeftPoint = manualRedactionEntry.positions[0]?.topLeft;
annotationWrapper.pageNumber = manualRedactionEntry.positions[0]?.page;
annotationWrapper.positions = manualRedactionEntry.positions;
annotationWrapper.content = manualRedactionEntry.addToDictionary ? null : AnnotationWrapper.createContent(manualRedactionEntry);
annotationWrapper.manual = true;
annotationWrapper.userId = manualRedactionEntry.user;
}
AnnotationWrapper._setSuperType(annotationWrapper, redactionLogEntry, manualRedactionEntry, idRemoval);
annotationWrapper.typeLabel = 'annotation-type.' + annotationWrapper.superType;
return annotationWrapper;
get id() {
return this.isConvertedRecommendation ? this.pendingRecommendationAnnotationId : this.annotationId;
}
private static _setSuperType(
annotationWrapper: AnnotationWrapper,
redactionLogEntry?: RedactionLogEntry,
manualRedactionEntry?: ManualRedactionEntry,
idRemoval?: IdRemoval
) {
if (idRemoval) {
annotationWrapper.dictionaryOperation = idRemoval.removeFromDictionary;
if (idRemoval.status === 'DECLINED') {
annotationWrapper.superType = 'declined-suggestion';
} else {
if (idRemoval.removeFromDictionary) {
annotationWrapper.superType = idRemoval.status === 'REQUESTED' ? 'suggestion-remove-dictionary' : 'remove-dictionary';
} else {
// TODO check me
annotationWrapper.superType = idRemoval.status === 'REQUESTED' ? 'suggestion-remove' : 'ignore';
}
}
}
if (manualRedactionEntry) {
annotationWrapper.dictionaryOperation = manualRedactionEntry.addToDictionary;
if (manualRedactionEntry.status === 'DECLINED') {
annotationWrapper.superType = 'declined-suggestion';
} else {
if (manualRedactionEntry.addToDictionary) {
annotationWrapper.superType = manualRedactionEntry.status === 'REQUESTED' ? 'suggestion-add-dictionary' : 'add-dictionary';
} else {
// TODO check me
annotationWrapper.superType = manualRedactionEntry.status === 'REQUESTED' ? 'suggestion-add' : 'manual';
}
}
}
if (!annotationWrapper.superType) {
annotationWrapper.superType = annotationWrapper.redaction ? 'redaction' : annotationWrapper.hint ? 'hint' : 'ignore';
}
}
private static _setTypeLabel(annotationWrapper: AnnotationWrapper) {
annotationWrapper.typeLabel = annotationWrapper.superType;
}
constructor() {}
get x() {
return this.firstTopLeftPoint.x;
}
@ -239,6 +137,76 @@ export class AnnotationWrapper {
return this.firstTopLeftPoint.y;
}
static fromData(redactionLogEntry?: RedactionLogEntryWrapper) {
const annotationWrapper = new AnnotationWrapper();
annotationWrapper.annotationId = redactionLogEntry.id;
annotationWrapper.redaction = redactionLogEntry.redacted;
annotationWrapper.hint = redactionLogEntry.hint;
annotationWrapper.dictionary = redactionLogEntry.type;
annotationWrapper.value = redactionLogEntry.value;
annotationWrapper.firstTopLeftPoint = redactionLogEntry.positions[0]?.topLeft;
annotationWrapper.pageNumber = redactionLogEntry.positions[0]?.page;
annotationWrapper.positions = redactionLogEntry.positions;
annotationWrapper.status = redactionLogEntry.status;
annotationWrapper.textBefore = redactionLogEntry.textBefore;
annotationWrapper.textAfter = redactionLogEntry.textAfter;
annotationWrapper.dictionaryOperation = redactionLogEntry.dictionaryEntry;
annotationWrapper.userId = redactionLogEntry.userId;
annotationWrapper.content = AnnotationWrapper.createContent(redactionLogEntry);
AnnotationWrapper._setSuperType(annotationWrapper, redactionLogEntry);
AnnotationWrapper._handleRecommendations(annotationWrapper, redactionLogEntry);
annotationWrapper.content = annotationWrapper.id;
if (annotationWrapper.dictionary === 'PII') {
annotationWrapper.content = annotationWrapper.id;
console.log(annotationWrapper, redactionLogEntry, annotationWrapper.id);
}
return annotationWrapper;
}
private static _handleRecommendations(annotationWrapper: AnnotationWrapper, redactionLogEntry: RedactionLogEntryWrapper) {
annotationWrapper.recommendation = !!redactionLogEntry?.recommendation;
if (annotationWrapper.recommendation) {
annotationWrapper.recommendationType = redactionLogEntry.type.substr('recommendation_'.length);
if (annotationWrapper.isConvertedRecommendation) {
annotationWrapper.dictionary = annotationWrapper.recommendationType;
annotationWrapper.pendingRecommendationAnnotationId = redactionLogEntry.pendingRecommendationAnnotationId;
}
}
}
private static _setSuperType(annotationWrapper: AnnotationWrapper, redactionLogEntryWrapper: RedactionLogEntryWrapper) {
if (redactionLogEntryWrapper.manual && redactionLogEntryWrapper.status === 'DECLINED') {
annotationWrapper.superType = 'declined-suggestion';
return;
}
if (redactionLogEntryWrapper.type === 'manual') {
annotationWrapper.superType = redactionLogEntryWrapper.status === 'REQUESTED' ? 'suggestion-add' : 'manual-redaction';
} else {
if (redactionLogEntryWrapper.status === 'REQUESTED') {
if (redactionLogEntryWrapper.dictionaryEntry) {
annotationWrapper.superType =
redactionLogEntryWrapper.manualRedactionType === 'ADD' ? 'suggestion-add-dictionary' : 'suggestion-remove-dictionary';
} else {
annotationWrapper.superType = redactionLogEntryWrapper.manualRedactionType === 'ADD' ? 'suggestion-add' : 'suggestion-remove';
}
}
if (redactionLogEntryWrapper.status === 'APPROVED') {
annotationWrapper.superType = redactionLogEntryWrapper.manualRedactionType === 'ADD' ? 'add-dictionary' : 'remove-dictionary';
}
}
if (!annotationWrapper.superType) {
annotationWrapper.superType = annotationWrapper.redaction ? 'redaction' : annotationWrapper.hint ? 'hint' : 'ignore';
}
annotationWrapper.typeLabel = 'annotation-type.' + annotationWrapper.superType;
}
private static createContent(entry: any) {
let content = '';
if (entry.matchedRule) {

View File

@ -2,6 +2,7 @@ import { Comment, IdRemoval, ManualRedactionEntry, ManualRedactions, RedactionLo
import { FileStatusWrapper } from './file-status.wrapper';
import { UserWrapper } from '../../../user/user.service';
import { AnnotationWrapper } from './annotation.wrapper';
import { RedactionLogEntryWrapper } from './redaction-log-entry.wrapper';
export interface AnnotationPair {
redactionLogEntry?: RedactionLogEntry;
@ -26,85 +27,149 @@ export class FileDataModel {
}
getAnnotations(dictionaryData: { [p: string]: TypeValue }, currentUser: UserWrapper, areDevFeaturesEnabled: boolean): AnnotationWrapper[] {
const annotations = [];
const entries: RedactionLogEntryWrapper[] = this._convertData(dictionaryData);
const pairs: AnnotationPair[] = this._createPairs();
let annotations = entries.map((entry) => AnnotationWrapper.fromData(entry));
pairs.forEach((pair) => {
const annotation = AnnotationWrapper.fromData(
currentUser,
dictionaryData,
this.fileStatus,
pair.redactionLogEntry,
pair.manualRedactionEntry,
pair.idRemoval,
pair.comments
);
if (annotation) {
// skip annotations that were marked as false positive
if (pair.manualRedactionEntry?.type === 'false_positive' && pair.redactionLogEntry) {
return;
}
if (annotation.isIgnored && !areDevFeaturesEnabled) {
return;
}
if (annotation.isFalsePositive && !areDevFeaturesEnabled) {
return;
}
if (annotation.isReadyForAnalysis && annotation.isApproved) {
//
} else {
annotations.push(annotation);
}
// filter based on dev-mode
annotations = annotations.filter((annotation) => {
if (!areDevFeaturesEnabled) {
return !annotation.isIgnored && !annotation.isFalsePositive;
} else {
return true;
}
});
return annotations;
}
private _createPairs(): AnnotationPair[] {
const pairs: AnnotationPair[] = [];
private _convertData(dictionaryData: { [p: string]: TypeValue }): RedactionLogEntryWrapper[] {
let result: RedactionLogEntryWrapper[] = [];
this.redactionLog.redactionLogEntry.forEach((rdl) => {
pairs.push({
redactionLogEntry: rdl,
// only not declined
manualRedactionEntry: this.manualRedactions.entriesToAdd.find(
(eta) => (eta.id === rdl.id || eta.reason === rdl.id) && this._validateEntry(eta)
),
// only not declined
idRemoval: this.manualRedactions.idsToRemove.find((idr) => idr.id === rdl.id && this._dateValid(idr)),
comments: this.manualRedactions.comments[rdl.id]
});
this.redactionLog.redactionLogEntry.forEach((redactionLogEntry) => {
// copy the redactionLog Entry
const wrapper: RedactionLogEntryWrapper = {};
Object.assign(wrapper, redactionLogEntry);
wrapper.comments = this.manualRedactions.comments[wrapper.id];
result.push(wrapper);
});
this.manualRedactions.entriesToAdd.forEach((eta) => {
// only not declined
if (this._dateValid(eta)) {
const redactionLogEntry = this.redactionLog.redactionLogEntry.find((rdl) => rdl.id === eta.id);
if (!redactionLogEntry) {
pairs.push({
redactionLogEntry: null,
manualRedactionEntry: eta,
// only not declined
idRemoval: this.manualRedactions.idsToRemove.find((idr) => idr.id === eta.id),
comments: this.manualRedactions.comments[eta.id]
});
this.manualRedactions.entriesToAdd.forEach((manual) => {
if (this._hasAlreadyBeenProcessed(manual) && this._isAcceptedOrRejected(manual)) {
let wrapper = result.find((r) => r.id === manual.id);
if (wrapper) {
wrapper.userId = manual.user;
} else {
wrapper = result.find((r) => r.id === manual.reason);
// if we found it
if (wrapper) {
wrapper.userId = manual.user;
}
}
return;
}
// normal case
let wrapper = result.find((r) => r.id === manual.id);
if (!wrapper) {
// false positive and recommendation case
// if we mark Annotation N as false positive -> it's reason is the original annotations Id
// if we confirm a recommendation, it's reason is the original annotations Id
wrapper = result.find((r) => r.id === manual.reason);
// if we found it
if (wrapper) {
// it's a recommendation if it's not a false positive
wrapper.recommendation = manual.type !== 'false_positive';
if (wrapper.recommendation) {
wrapper.pendingRecommendationAnnotationId = manual.id;
}
wrapper.manual = true;
wrapper.manualRedactionType = 'ADD';
wrapper.status = manual.status;
} else {
const dictionary = dictionaryData[manual.type];
wrapper = {};
wrapper.id = manual.id;
wrapper.dictionaryEntry = manual.addToDictionary;
wrapper.legalBasis = manual.legalBasis;
wrapper.positions = manual.positions;
wrapper.reason = manual.reason;
wrapper.status = manual.status;
wrapper.type = manual.type;
wrapper.userId = manual.user;
wrapper.value = manual.value;
wrapper.redacted = !dictionary.hint;
wrapper.hint = dictionary.hint;
wrapper.manualRedactionType = 'ADD';
wrapper.manual = true;
wrapper.comments = this.manualRedactions.comments[wrapper.id];
result.push(wrapper);
}
} else {
wrapper.confirmed = true;
}
});
return pairs;
this.manualRedactions.idsToRemove.forEach((idToRemove) => {
if (this._hasAlreadyBeenProcessed(idToRemove) && this._isAcceptedOrRejected(idToRemove)) {
const wrapper = result.find((r) => r.id === idToRemove.id);
if (wrapper && wrapper.dictionaryEntry) {
wrapper.manual = false;
wrapper.manualRedactionType = null;
wrapper.status = null;
}
return;
}
const wrapper = result.find((r) => r.id === idToRemove.id);
if (wrapper) {
wrapper.manual = true;
wrapper.dictionaryEntry = idToRemove.removeFromDictionary;
wrapper.userId = idToRemove.user;
wrapper.status = idToRemove.status;
wrapper.manualRedactionType = 'REMOVE';
}
});
result.forEach((id) => {
if (id.id === '57205cfd183653d4d159dc8d07f86a4c') {
console.log(' ========= ', id);
}
});
// remove undone entriesToAdd and idsToRemove
result = result.filter((redactionLogEntry) => {
if (redactionLogEntry.manual) {
if (redactionLogEntry.manualRedactionType === 'ADD') {
const foundManualEntry = this.manualRedactions.entriesToAdd.find((me) => me.id === redactionLogEntry.id);
if (!foundManualEntry) {
return false;
}
}
if (redactionLogEntry.manualRedactionType === 'REMOVE') {
const foundManualEntry = this.manualRedactions.idsToRemove.find((me) => me.id === redactionLogEntry.id);
if (!foundManualEntry) {
redactionLogEntry.manual = false;
redactionLogEntry.manualRedactionType = null;
redactionLogEntry.status = null;
}
}
}
return true;
});
return result;
}
private _validateEntry(entry: ManualRedactionEntry): boolean {
return this._dateValid(entry) || entry.type === 'manual';
private _isAcceptedOrRejected(entry: ManualRedactionEntry | IdRemoval): boolean {
return entry.status === 'APPROVED' || entry.status === 'DECLINED';
}
private _dateValid(entry: ManualRedactionEntry | IdRemoval): boolean {
return new Date(entry.processedDate).getTime() > new Date(this.fileStatus.lastProcessed).getTime() || !entry.processedDate;
private _hasAlreadyBeenProcessed(entry: ManualRedactionEntry | IdRemoval): boolean {
return !entry.processedDate ? false : new Date(entry.processedDate).getTime() < new Date(this.fileStatus.lastProcessed).getTime();
}
}

View File

@ -0,0 +1,27 @@
import { Rectangle, Comment } from '@redaction/red-ui-http';
export interface RedactionLogEntryWrapper {
color?: Array<number>;
dictionaryEntry?: boolean;
hint?: boolean;
id?: string;
legalBasis?: string;
manual?: boolean;
manualRedactionType?: 'ADD' | 'REMOVE';
matchedRule?: number;
positions?: Array<Rectangle>;
reason?: string;
recommendation?: boolean;
pendingRecommendationAnnotationId?: string;
redacted?: boolean;
section?: string;
sectionNumber?: number;
status?: 'REQUESTED' | 'APPROVED' | 'DECLINED';
textAfter?: string;
textBefore?: string;
type?: string;
value?: string;
userId?: string;
comments?: Comment[];
confirmed?: boolean;
}

View File

@ -463,7 +463,7 @@
"ignore": "Ignore",
"hint": "Hint",
"redaction": "Redaction",
"manual": "Manual Redaction",
"manual-redaction": "Manual Redaction",
"declined-suggestion": "Declined Suggestion"
},
"manual-annotation": {