From 92d3b866cef821eea0e7a02ae5569e87a8dbeb8f Mon Sep 17 00:00:00 2001 From: Timo Date: Fri, 8 Jan 2021 18:15:21 +0200 Subject: [PATCH] further fixes for state handling of annotations --- .../service/annotation-actions.service.ts | 89 +++------ .../annotation-actions.component.html | 24 +-- .../annotation-actions.component.ts | 13 +- .../file/model/annotation.permissions.ts | 36 ++++ .../screens/file/model/annotation.wrapper.ts | 92 ++++++---- .../app/screens/file/model/file-data.model.ts | 170 ++++++++---------- .../file/model/redaction-log-entry.wrapper.ts | 11 +- .../red-ui/src/app/state/app-state.service.ts | 5 + apps/red-ui/src/assets/i18n/en.json | 2 + 9 files changed, 227 insertions(+), 215 deletions(-) create mode 100644 apps/red-ui/src/app/screens/file/model/annotation.permissions.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 index d9bc61ec6..0dd0dd77b 100644 --- a/apps/red-ui/src/app/common/service/annotation-actions.service.ts +++ b/apps/red-ui/src/app/common/service/annotation-actions.service.ts @@ -7,6 +7,7 @@ import { Observable } from 'rxjs'; import { TranslateService } from '@ngx-translate/core'; import { AddRedactionRequest } from '@redaction/red-ui-http'; import { getFirstRelevantTextPart } from '../../utils/functions'; +import { AnnotationPermissions } from '../../screens/file/model/annotation.permissions'; @Injectable({ providedIn: 'root' @@ -20,39 +21,6 @@ export class AnnotationActionsService { 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(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.isManualRedaction && this._permissionsService.isManagerAndOwner() && !this.canUndoAnnotation(annotation) && !annotation.isRecommendation - ); - } - - public requiresSuggestionRemoveMenu(annotation: AnnotationWrapper): boolean { - return (annotation.isRedacted || annotation.isIgnored) && !annotation.isRecommendation; - } - - public canConvertRecommendationToAnnotation(annotation: AnnotationWrapper): boolean { - // recommendations that have not already been turned into a suggestion - return annotation.isRecommendation && !annotation.isConvertedRecommendation; - } - - 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) { $event?.stopPropagation(); console.log(annotation.isModifyDictionary); @@ -117,7 +85,9 @@ export class AnnotationActionsService { public getViewerAvailableActions(annotation: AnnotationWrapper, annotationsChanged: EventEmitter): {}[] { const availableActions = []; - if (this.canConvertRecommendationToAnnotation(annotation)) { + const annotationPermissions = AnnotationPermissions.forUser(this._permissionsService.currentUser, annotation); + + if (annotationPermissions.canAcceptRecommendation) { availableActions.push({ type: 'actionButton', img: '/assets/icons/general/check-alt.svg', @@ -130,7 +100,7 @@ export class AnnotationActionsService { }); } - if (this.canAcceptSuggestion(annotation)) { + if (annotationPermissions.canAcceptSuggestion) { availableActions.push({ type: 'actionButton', img: '/assets/icons/general/check-alt.svg', @@ -143,7 +113,7 @@ export class AnnotationActionsService { }); } - if (this.canUndoAnnotation(annotation)) { + if (annotationPermissions.canUndo) { availableActions.push({ type: 'actionButton', img: '/assets/icons/general/undo.svg', @@ -156,7 +126,7 @@ export class AnnotationActionsService { }); } - if (this.canRejectSuggestion(annotation)) { + if (annotationPermissions.canRejectSuggestion) { availableActions.push({ type: 'actionButton', img: '/assets/icons/general/close.svg', @@ -169,33 +139,20 @@ export class AnnotationActionsService { }); } - if (this.canDirectlySuggestToRemoveAnnotation(annotation)) { + if (annotationPermissions.canRemoveOrSuggestToRemoveOnlyHere) { availableActions.push({ type: 'actionButton', - img: '/assets/icons/general/trash.svg', + img: '/assets/icons/general/close.svg', title: this._translateService.instant('annotation-actions.suggest-remove-annotation'), onClick: () => { this._ngZone.run(() => { - this.suggestRemoveAnnotation(null, annotation, true, annotationsChanged); + this.suggestRemoveAnnotation(null, annotation, false, annotationsChanged); }); } }); } - if (this.requiresSuggestionRemoveMenu(annotation)) { - if (!annotation.isIgnored) { - availableActions.push({ - type: 'actionButton', - img: '/assets/icons/general/close.svg', - title: this._translateService.instant('annotation-actions.remove-annotation.only-here'), - onClick: () => { - this._ngZone.run(() => { - this.suggestRemoveAnnotation(null, annotation, false, annotationsChanged); - }); - } - }); - } - + if (annotationPermissions.canRemoveOrSuggestToRemoveFromDictionary) { availableActions.push({ type: 'actionButton', img: '/assets/icons/general/trash.svg', @@ -206,19 +163,19 @@ export class AnnotationActionsService { }); } }); + } - if (annotation.canBeMarkedAsFalsePositive) { - availableActions.push({ - type: 'actionButton', - img: '/assets/icons/general/thumb-down.svg', - title: this._translateService.instant('annotation-actions.remove-annotation.false-positive'), - onClick: () => { - this._ngZone.run(() => { - this.markAsFalsePositive(null, annotation, annotationsChanged); - }); - } - }); - } + if (annotationPermissions.canMarkAsFalsePositive) { + availableActions.push({ + type: 'actionButton', + img: '/assets/icons/general/thumb-down.svg', + title: this._translateService.instant('annotation-actions.remove-annotation.false-positive'), + onClick: () => { + this._ngZone.run(() => { + this.markAsFalsePositive(null, annotation, 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 ca16d9455..e0302119e 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 @@ -2,7 +2,7 @@ -
+
@@ -78,7 +82,7 @@
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 59d954600..8fe443b91 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,7 +1,8 @@ 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 { PermissionsService } from '../../../common/service/permissions.service'; +import { AnnotationPermissions } from '../model/annotation.permissions'; import { AnnotationActionsService } from '../../../common/service/annotation-actions.service'; @Component({ @@ -15,13 +16,17 @@ export class AnnotationActionsComponent implements OnInit { @Output() annotationsChanged = new EventEmitter(); - suggestionType: TypeValue; menuOpen: boolean; + annotationPermissions: AnnotationPermissions; - constructor(public appStateService: AppStateService, public annotationActionsService: AnnotationActionsService) {} + constructor( + public appStateService: AppStateService, + private _permissionsService: PermissionsService, + public annotationActionsService: AnnotationActionsService + ) {} ngOnInit(): void { - this.suggestionType = this.appStateService.getDictionaryTypeValue('suggestion'); + this.annotationPermissions = AnnotationPermissions.forUser(this._permissionsService.currentUser, this.annotation); } public openMenu($event: MouseEvent) { diff --git a/apps/red-ui/src/app/screens/file/model/annotation.permissions.ts b/apps/red-ui/src/app/screens/file/model/annotation.permissions.ts new file mode 100644 index 000000000..ca29bb35a --- /dev/null +++ b/apps/red-ui/src/app/screens/file/model/annotation.permissions.ts @@ -0,0 +1,36 @@ +import { UserWrapper } from '../../../user/user.service'; +import { AnnotationWrapper } from './annotation.wrapper'; + +export class AnnotationPermissions { + canUndo: boolean; + + canAcceptRecommendation: boolean; + canMarkAsFalsePositive: boolean; + + canRemoveOrSuggestToRemoveOnlyHere: boolean; + canRemoveOrSuggestToRemoveFromDictionary: boolean; + + canAcceptSuggestion: boolean; + canRejectSuggestion: boolean; + + public static forUser(user: UserWrapper, annotation: AnnotationWrapper) { + const permissions: AnnotationPermissions = new AnnotationPermissions(); + + permissions.canUndo = annotation.userId === user.id && annotation.isUndoableSuperType; + + permissions.canAcceptRecommendation = annotation.isRecommendation; + permissions.canMarkAsFalsePositive = annotation.canBeMarkedAsFalsePositive; + + permissions.canRemoveOrSuggestToRemoveOnlyHere = annotation.isRedacted; + permissions.canRemoveOrSuggestToRemoveFromDictionary = annotation.isRedacted && !annotation.isManualRedaction; + + permissions.canAcceptSuggestion = user.isManager && (annotation.isSuggestion || annotation.isDeclinedSuggestion); + permissions.canRejectSuggestion = user.isManager && (annotation.isSuggestion || (annotation.isReadyForAnalysis && !permissions.canUndo)); + + return permissions; + } + + public get canPerformMultipleRemoveActions() { + return this.canMarkAsFalsePositive + this.canRemoveOrSuggestToRemoveFromDictionary + this.canRemoveOrSuggestToRemoveOnlyHere >= 2; + } +} diff --git a/apps/red-ui/src/app/screens/file/model/annotation.wrapper.ts b/apps/red-ui/src/app/screens/file/model/annotation.wrapper.ts index 7901cfe63..e04eb00ea 100644 --- a/apps/red-ui/src/app/screens/file/model/annotation.wrapper.ts +++ b/apps/red-ui/src/app/screens/file/model/annotation.wrapper.ts @@ -5,6 +5,7 @@ export class AnnotationWrapper { superType: | 'add-dictionary' | 'remove-dictionary' + | 'remove-only-here' | 'suggestion-add-dictionary' | 'suggestion-remove-dictionary' | 'suggestion-add' @@ -12,8 +13,10 @@ export class AnnotationWrapper { | 'ignore' | 'redaction' | 'manual-redaction' + | 'recommendation' | 'hint' | 'declined-suggestion'; + dictionary: string; color: string; comments: Comment[] = []; @@ -29,18 +32,26 @@ export class AnnotationWrapper { status: string; dictionaryOperation: boolean; positions: Rectangle[]; - - recommendation: boolean; recommendationType: string; - pendingRecommendationAnnotationId: string; textAfter?: string; textBefore?: string; constructor() {} + get isUndoableSuperType() { + return ( + this.superType === 'add-dictionary' || + this.superType === 'remove-dictionary' || + this.superType === 'suggestion-add-dictionary' || + this.superType === 'suggestion-remove-dictionary' || + this.superType === 'suggestion-add' || + this.superType === 'suggestion-remove' + ); + } + get isDictionaryBased() { - return (this.isHint || this.isRedacted) && !this.isManualRedaction; + return this.isHint || this.superType === 'redaction'; } get descriptor() { @@ -58,7 +69,7 @@ export class AnnotationWrapper { } get canBeMarkedAsFalsePositive() { - return this.isRedacted && (this.hasTextAfter || this.hasTextBefore); + return this.superType === 'redaction' && (this.hasTextAfter || this.hasTextBefore); } get isSuperTypeBasedColor() { @@ -86,7 +97,7 @@ export class AnnotationWrapper { } get isReadyForAnalysis() { - return this.superType === 'add-dictionary' || this.superType === 'remove-dictionary'; + return this.superType === 'add-dictionary' || this.superType === 'remove-dictionary' || this.superType === 'remove-only-here'; } get isApproved() { @@ -98,7 +109,7 @@ export class AnnotationWrapper { } get isRedacted() { - return this.superType === 'redaction'; + return this.superType === 'redaction' || this.superType === 'manual-redaction'; } get isSuggestion() { @@ -122,11 +133,11 @@ export class AnnotationWrapper { } get isRecommendation() { - return this.recommendation; + return this.superType === 'recommendation'; } get id() { - return this.isConvertedRecommendation ? this.pendingRecommendationAnnotationId : this.annotationId; + return this.annotationId; } get x() { @@ -153,61 +164,68 @@ export class AnnotationWrapper { annotationWrapper.textAfter = redactionLogEntry.textAfter; annotationWrapper.dictionaryOperation = redactionLogEntry.dictionaryEntry; annotationWrapper.userId = redactionLogEntry.userId; - annotationWrapper.content = AnnotationWrapper.createContent(redactionLogEntry); + 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); - } + annotationWrapper.typeLabel = 'annotation-type.' + annotationWrapper.superType; return annotationWrapper; } private static _handleRecommendations(annotationWrapper: AnnotationWrapper, redactionLogEntry: RedactionLogEntryWrapper) { - annotationWrapper.recommendation = !!redactionLogEntry?.recommendation; - if (annotationWrapper.recommendation) { + if (annotationWrapper.superType === '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') { + if (redactionLogEntryWrapper.recommendation) { + annotationWrapper.superType = 'recommendation'; + return; + } + + if (redactionLogEntryWrapper.status === 'DECLINED') { annotationWrapper.superType = 'declined-suggestion'; return; } - if (redactionLogEntryWrapper.type === 'manual') { - annotationWrapper.superType = redactionLogEntryWrapper.status === 'REQUESTED' ? 'suggestion-add' : 'manual-redaction'; - } else { + if (annotationWrapper.dictionary === 'false_positive') { 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'; - } + annotationWrapper.superType = 'suggestion-add-dictionary'; } if (redactionLogEntryWrapper.status === 'APPROVED') { - annotationWrapper.superType = redactionLogEntryWrapper.manualRedactionType === 'ADD' ? 'add-dictionary' : 'remove-dictionary'; + annotationWrapper.superType = 'add-dictionary'; + } + } else { + if (redactionLogEntryWrapper.redacted) { + 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') { + if (redactionLogEntryWrapper.dictionaryEntry) { + annotationWrapper.superType = redactionLogEntryWrapper.manualRedactionType === 'ADD' ? 'add-dictionary' : 'remove-dictionary'; + } else { + annotationWrapper.superType = redactionLogEntryWrapper.manualRedactionType === 'ADD' ? 'manual-redaction' : 'remove-only-here'; + } + } + } } } if (!annotationWrapper.superType) { annotationWrapper.superType = annotationWrapper.redaction ? 'redaction' : annotationWrapper.hint ? 'hint' : 'ignore'; } - - annotationWrapper.typeLabel = 'annotation-type.' + annotationWrapper.superType; } - private static createContent(entry: any) { + private static _createContent(entry: any) { let content = ''; if (entry.matchedRule) { content += 'Rule ' + entry.matchedRule + ' matched \n\n'; diff --git a/apps/red-ui/src/app/screens/file/model/file-data.model.ts b/apps/red-ui/src/app/screens/file/model/file-data.model.ts index b0a48ea99..0065cff8c 100644 --- a/apps/red-ui/src/app/screens/file/model/file-data.model.ts +++ b/apps/red-ui/src/app/screens/file/model/file-data.model.ts @@ -1,16 +1,9 @@ -import { Comment, IdRemoval, ManualRedactionEntry, ManualRedactions, RedactionLog, RedactionLogEntry, TypeValue, ViewedPages } from '@redaction/red-ui-http'; +import { IdRemoval, ManualRedactionEntry, ManualRedactions, RedactionLog, RedactionLogEntry, TypeValue, ViewedPages } from '@redaction/red-ui-http'; 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; - manualRedactionEntry?: ManualRedactionEntry; - idRemoval?: IdRemoval; - comments?: Comment[]; -} - export class FileDataModel { redactedFileData: Blob; @@ -47,113 +40,102 @@ export class FileDataModel { let result: RedactionLogEntryWrapper[] = []; 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); + // false positive entries from the redaction-log need to be skipped + if (redactionLogEntry.type !== 'false_positive') { + // copy the redactionLog Entry + const redactionLogEntryWrapper: RedactionLogEntryWrapper = { actionPendingReanalysis: false }; + Object.assign(redactionLogEntryWrapper, redactionLogEntry); + redactionLogEntryWrapper.comments = this.manualRedactions.comments[redactionLogEntryWrapper.id]; + result.push(redactionLogEntryWrapper); + } }); this.manualRedactions.entriesToAdd.forEach((manual) => { - let wrapper; - if (this._hasAlreadyBeenProcessed(manual) && this._isAcceptedOrRejected(manual)) { - 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; + const markedAsReasonRedactionLogEntry = result.find((r) => r.id === manual.reason); + + const relevantRedactionLogEntry = result.find((r) => r.id === manual.id); + + // a redaction-log entry is marked as a reason for another entry - hide it + if (!!markedAsReasonRedactionLogEntry) { + markedAsReasonRedactionLogEntry.hidden = true; } - // normal case - 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 + // an entry for this request already exists in the redactionLog + if (!!relevantRedactionLogEntry) { + relevantRedactionLogEntry.userId = manual.user; + relevantRedactionLogEntry.dictionaryEntry = manual.addToDictionary; - 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); + // if statuses differ + if (relevantRedactionLogEntry.status !== manual.status) { + relevantRedactionLogEntry.actionPendingReanalysis = true; + relevantRedactionLogEntry.status = manual.status; } } else { - wrapper.confirmed = true; + // dictionary modifying requests that have been processed already updated the dictionary and should not be drawn + if (manual.addToDictionary && this._hasAlreadyBeenProcessed(manual)) { + return; + } + + // no entry exists in the redaction log - create it + const dictionary = dictionaryData[manual.type]; + + const redactionLogEntryWrapper: RedactionLogEntryWrapper = {}; + + redactionLogEntryWrapper.id = manual.id; + redactionLogEntryWrapper.dictionaryEntry = manual.addToDictionary; + redactionLogEntryWrapper.legalBasis = manual.legalBasis; + redactionLogEntryWrapper.positions = manual.positions; + redactionLogEntryWrapper.reason = manual.reason; + redactionLogEntryWrapper.status = manual.status; + redactionLogEntryWrapper.type = manual.type; + redactionLogEntryWrapper.userId = manual.user; + redactionLogEntryWrapper.value = manual.value; + redactionLogEntryWrapper.redacted = !dictionary.hint; + redactionLogEntryWrapper.hint = dictionary.hint; + redactionLogEntryWrapper.manualRedactionType = 'ADD'; + redactionLogEntryWrapper.manual = true; + redactionLogEntryWrapper.comments = this.manualRedactions.comments[redactionLogEntryWrapper.id]; + + result.push(redactionLogEntryWrapper); } }); this.manualRedactions.idsToRemove.forEach((idToRemove) => { - let wrapper; - if (this._hasAlreadyBeenProcessed(idToRemove) && this._isAcceptedOrRejected(idToRemove)) { - wrapper = result.find((r) => r.id === idToRemove.id); - if (wrapper && wrapper.dictionaryEntry) { - wrapper.manual = false; - wrapper.manualRedactionType = null; - wrapper.status = null; - } + const relevantRedactionLogEntry = result.find((r) => r.id === idToRemove.id); + + if (!relevantRedactionLogEntry) { + // idRemove for something that doesn't exist - skip return; - } + } else { + relevantRedactionLogEntry.userId = idToRemove.user; + relevantRedactionLogEntry.dictionaryEntry = idToRemove.removeFromDictionary; - 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'; + // if statuses differ + if (relevantRedactionLogEntry.status !== idToRemove.status) { + relevantRedactionLogEntry.actionPendingReanalysis = true; + relevantRedactionLogEntry.status = idToRemove.status; + } + + if (this._hasAlreadyBeenProcessed(idToRemove)) { + if (idToRemove.status === 'DECLINED') { + relevantRedactionLogEntry.status = null; + } + } } }); - result.forEach((id) => { - if (id.id === '57205cfd183653d4d159dc8d07f86a4c') { - console.log(' ========= ', id); - } - }); - - // remove undone entriesToAdd and idsToRemove - result = result.filter((redactionLogEntry) => { + result.forEach((redactionLogEntry) => { if (redactionLogEntry.manual) { if (redactionLogEntry.manualRedactionType === 'ADD') { const foundManualEntry = this.manualRedactions.entriesToAdd.find((me) => me.id === redactionLogEntry.id); + // ADD has been undone - not yet processed if (!foundManualEntry) { - return false; + redactionLogEntry.hidden = true; } } if (redactionLogEntry.manualRedactionType === 'REMOVE') { const foundManualEntry = this.manualRedactions.idsToRemove.find((me) => me.id === redactionLogEntry.id); + // REMOVE has been undone - not yet processed if (!foundManualEntry) { redactionLogEntry.manual = false; redactionLogEntry.manualRedactionType = null; @@ -161,14 +143,12 @@ export class FileDataModel { } } } - return true; }); - return result; - } + // remove undone entriesToAdd and idsToRemove + result = result.filter((redactionLogEntry) => !redactionLogEntry.hidden); - private _isAcceptedOrRejected(entry: ManualRedactionEntry | IdRemoval): boolean { - return entry.status === 'APPROVED' || entry.status === 'DECLINED'; + return result; } private _hasAlreadyBeenProcessed(entry: ManualRedactionEntry | IdRemoval): boolean { diff --git a/apps/red-ui/src/app/screens/file/model/redaction-log-entry.wrapper.ts b/apps/red-ui/src/app/screens/file/model/redaction-log-entry.wrapper.ts index b7ba39dfe..767fbcb59 100644 --- a/apps/red-ui/src/app/screens/file/model/redaction-log-entry.wrapper.ts +++ b/apps/red-ui/src/app/screens/file/model/redaction-log-entry.wrapper.ts @@ -11,8 +11,6 @@ export interface RedactionLogEntryWrapper { matchedRule?: number; positions?: Array; reason?: string; - recommendation?: boolean; - pendingRecommendationAnnotationId?: string; redacted?: boolean; section?: string; sectionNumber?: number; @@ -21,7 +19,14 @@ export interface RedactionLogEntryWrapper { textBefore?: string; type?: string; value?: string; + + recommendation?: boolean; + recommendationAnnotationId?: string; + + actionPendingReanalysis?: boolean; + + hidden?: boolean; + userId?: string; comments?: Comment[]; - confirmed?: boolean; } diff --git a/apps/red-ui/src/app/state/app-state.service.ts b/apps/red-ui/src/app/state/app-state.service.ts index 74f836e23..4c0109137 100644 --- a/apps/red-ui/src/app/state/app-state.service.ts +++ b/apps/red-ui/src/app/state/app-state.service.ts @@ -498,6 +498,11 @@ export class AppStateService { type: 'remove-dictionary', virtual: true }; + dictionaryData['remove-only-here'] = { + hexColor: '#dd4d50', + type: 'remove-only-here', + virtual: true + }; // generic suggestions dictionaryData['suggestion'] = { hexColor: colors.requestAdd, diff --git a/apps/red-ui/src/assets/i18n/en.json b/apps/red-ui/src/assets/i18n/en.json index e0ad9b4b5..76fae47c0 100644 --- a/apps/red-ui/src/assets/i18n/en.json +++ b/apps/red-ui/src/assets/i18n/en.json @@ -454,6 +454,8 @@ "hints": "Hint Dictionaries" }, "annotation-type": { + "recommendation": "Recommendation", + "remove-only-here": "Pending removal ( only here )", "add-dictionary": "Pending add to dictionary", "remove-dictionary": "Pending remove from dictionary", "suggestion-add-dictionary": "Suggested dictionary add",