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 3cba9018f..6832b96df 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 @@ -5,6 +5,8 @@ import { DialogService } from '../../dialogs/dialog.service'; import { AnnotationWrapper } from '../../screens/file/model/annotation.wrapper'; import { Observable } from 'rxjs'; import { TranslateService } from '@ngx-translate/core'; +import { AddRedactionRequest } from '@redaction/red-ui-http'; +import { getFirstRelevantTextPart } from '../../utils/functions'; @Injectable({ providedIn: 'root' @@ -54,12 +56,12 @@ export class AnnotationActionsService { } public acceptSuggestion($event: MouseEvent, annotation: AnnotationWrapper, annotationsChanged: EventEmitter) { - if ($event) $event.stopPropagation(); + $event?.stopPropagation(); this._processObsAndEmit(this._manualAnnotationService.approveRequest(annotation.id, annotation.isModifyDictionary), annotation, annotationsChanged); } public rejectSuggestion($event: MouseEvent, annotation: AnnotationWrapper, annotationsChanged: EventEmitter) { - if ($event) $event.stopPropagation(); + $event?.stopPropagation(); this._processObsAndEmit(this._manualAnnotationService.declineOrRemoveRequest(annotation), annotation, annotationsChanged); } @@ -78,19 +80,33 @@ export class AnnotationActionsService { }); } + public markAsFalsePositive($event: MouseEvent, annotation: AnnotationWrapper, annotationsChanged: EventEmitter) { + $event?.stopPropagation(); + + const falsePositiveRequest: AddRedactionRequest = {}; + falsePositiveRequest.reason = annotation.id; + falsePositiveRequest.value = this._getFalsePositiveText(annotation); + falsePositiveRequest.type = 'false_positive'; + falsePositiveRequest.positions = annotation.positions; + falsePositiveRequest.addToDictionary = true; + falsePositiveRequest.comment = { text: 'False Positive' }; + + this._processObsAndEmit(this._manualAnnotationService.addAnnotation(falsePositiveRequest), annotation, annotationsChanged); + } + public undoDirectAction($event: MouseEvent, annotation: AnnotationWrapper, annotationsChanged: EventEmitter) { - if ($event) $event.stopPropagation(); + $event?.stopPropagation(); this._processObsAndEmit(this._manualAnnotationService.undoRequest(annotation), annotation, annotationsChanged); } public convertRecommendationToAnnotation($event: any, annotation: AnnotationWrapper, annotationsChanged: EventEmitter) { - if ($event) $event.stopPropagation(); + $event?.stopPropagation(); this._processObsAndEmit(this._manualAnnotationService.addRecommendation(annotation), annotation, annotationsChanged); } private _processObsAndEmit(obs: Observable, annotation: AnnotationWrapper, annotationsChanged: EventEmitter) { obs.subscribe( - (data) => { + () => { annotationsChanged.emit(annotation); }, () => { @@ -109,7 +125,7 @@ export class AnnotationActionsService { title: this._translateService.instant('annotation-actions.accept-recommendation.label'), onClick: () => { this._ngZone.run(() => { - this.undoDirectAction(null, annotation, annotationsChanged); + this.convertRecommendationToAnnotation(null, annotation, annotationsChanged); }); } }); @@ -167,20 +183,7 @@ export class AnnotationActionsService { }); } - // TODO: probably need icons for these? - if (this.requiresSuggestionRemoveMenu(annotation)) { - availableActions.push({ - type: 'actionButton', - img: '/assets/icons/general/trash.svg', - title: this._translateService.instant('annotation-actions.remove-annotation.remove-from-dict'), - onClick: () => { - this._ngZone.run(() => { - this.suggestRemoveAnnotation(null, annotation, true, annotationsChanged); - }); - } - }); - if (!annotation.isIgnored) { availableActions.push({ type: 'actionButton', @@ -193,8 +196,45 @@ export class AnnotationActionsService { } }); } + + availableActions.push({ + type: 'actionButton', + img: '/assets/icons/general/trash.svg', + title: this._translateService.instant('annotation-actions.remove-annotation.remove-from-dict'), + onClick: () => { + this._ngZone.run(() => { + this.suggestRemoveAnnotation(null, annotation, true, annotationsChanged); + }); + } + }); + + 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); + }); + } + }); + } } return availableActions; } + + private _getFalsePositiveText(annotation: AnnotationWrapper) { + if (annotation.canBeMarkedAsFalsePositive) { + let text; + if (annotation.hasTextAfter) { + text = getFirstRelevantTextPart(annotation.textAfter, 'FORWARD'); + return annotation.value + text; + } else { + text = getFirstRelevantTextPart(annotation.textBefore, 'BACKWARD'); + return text + annotation.value; + } + } + } } diff --git a/apps/red-ui/src/app/icons/icons.module.ts b/apps/red-ui/src/app/icons/icons.module.ts index c77fc9980..79a1d8474 100644 --- a/apps/red-ui/src/app/icons/icons.module.ts +++ b/apps/red-ui/src/app/icons/icons.module.ts @@ -59,6 +59,7 @@ export class IconsModule { 'sort-desc', 'status', 'trash', + 'thumb-down', 'template', 'user', 'check-alt', 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 38fce3fc4..ca16d9455 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 @@ -74,5 +74,14 @@
+ +
+ +
+
diff --git a/apps/red-ui/src/app/screens/file/annotation-actions/annotation-actions.component.scss b/apps/red-ui/src/app/screens/file/annotation-actions/annotation-actions.component.scss index d8e7ffe06..717e7b5f8 100644 --- a/apps/red-ui/src/app/screens/file/annotation-actions/annotation-actions.component.scss +++ b/apps/red-ui/src/app/screens/file/annotation-actions/annotation-actions.component.scss @@ -23,3 +23,8 @@ display: flex; } } + +.false-positive-icon { + height: 16px; + width: 16px; +} 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 b6cadc0be..7acf2749d 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 @@ -33,6 +33,20 @@ export class AnnotationWrapper { recommendation: boolean; positions: Rectangle[]; recommendationType: string; + textAfter?: string; + textBefore?: string; + + get hasTextAfter() { + return this.textAfter && this.textAfter.trim().length > 0; + } + + get hasTextBefore() { + return this.textBefore && this.textBefore.trim().length > 0; + } + + get canBeMarkedAsFalsePositive() { + return this.isRedacted && (this.hasTextAfter || this.hasTextBefore); + } get isIgnored() { return this.superType === 'ignore'; @@ -138,6 +152,8 @@ export class AnnotationWrapper { 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]; 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 a3ba75b4d..3f918b584 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 @@ -41,6 +41,11 @@ export class FileDataModel { 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; } diff --git a/apps/red-ui/src/app/screens/file/pdf-viewer/pdf-viewer.component.ts b/apps/red-ui/src/app/screens/file/pdf-viewer/pdf-viewer.component.ts index 9e42098f2..a4f0b9bd3 100644 --- a/apps/red-ui/src/app/screens/file/pdf-viewer/pdf-viewer.component.ts +++ b/apps/red-ui/src/app/screens/file/pdf-viewer/pdf-viewer.component.ts @@ -257,18 +257,19 @@ export class PdfViewerComponent implements OnInit, AfterViewInit, OnChanges { } }); - this.instance.textPopup.add({ - type: 'actionButton', - dataElement: 'add-false-positive', - img: '/assets/icons/general/pdftron-action-false-positive.svg', - title: this._translateService.instant(this._manualAnnotationService.getTitle('FALSE_POSITIVE')), - onClick: () => { - const selectedQuads = this.instance.docViewer.getSelectedTextQuads(); - const text = this.instance.docViewer.getSelectedText(); - const mre = this._getManualRedactionEntry(selectedQuads, text); - this.manualAnnotationRequested.emit(new ManualRedactionEntryWrapper(this.instance.docViewer.getSelectedTextQuads(), mre, 'FALSE_POSITIVE')); - } - }); + // Temporary removed false positive action + // this.instance.textPopup.add({ + // type: 'actionButton', + // dataElement: 'add-false-positive', + // img: '/assets/icons/general/pdftron-action-false-positive.svg', + // title: this._translateService.instant(this._manualAnnotationService.getTitle('FALSE_POSITIVE')), + // onClick: () => { + // const selectedQuads = this.instance.docViewer.getSelectedTextQuads(); + // const text = this.instance.docViewer.getSelectedText(); + // const mre = this._getManualRedactionEntry(selectedQuads, text); + // this.manualAnnotationRequested.emit(new ManualRedactionEntryWrapper(this.instance.docViewer.getSelectedTextQuads(), mre, 'FALSE_POSITIVE')); + // } + // }); this.instance.textPopup.add({ type: 'actionButton', diff --git a/apps/red-ui/src/app/screens/file/service/annotation-processing.service.ts b/apps/red-ui/src/app/screens/file/service/annotation-processing.service.ts index b689f23b2..5a375b438 100644 --- a/apps/red-ui/src/app/screens/file/service/annotation-processing.service.ts +++ b/apps/red-ui/src/app/screens/file/service/annotation-processing.service.ts @@ -4,6 +4,7 @@ import { AnnotationWrapper } from '../model/annotation.wrapper'; import { FilterModel } from '../../../common/filter/model/filter.model'; import { handleCheckedValue } from '../../../common/filter/utils/filter-utils'; import { SuperTypeSorter } from '../../../common/sorters/super-type-sorter'; +import { getFirstRelevantTextPart } from '../../../utils/functions'; @Injectable({ providedIn: 'root' diff --git a/apps/red-ui/src/app/screens/file/service/manual-annotation.service.ts b/apps/red-ui/src/app/screens/file/service/manual-annotation.service.ts index dc350e838..2cc22d7f4 100644 --- a/apps/red-ui/src/app/screens/file/service/manual-annotation.service.ts +++ b/apps/red-ui/src/app/screens/file/service/manual-annotation.service.ts @@ -58,7 +58,7 @@ export class ManualAnnotationService { // this wraps // /manualRedaction/redaction/add // /manualRedaction/request/add - addAnnotation(manualRedactionEntry: ManualRedactionEntry) { + addAnnotation(manualRedactionEntry: AddRedactionRequest) { if (this._permissionsService.isManagerAndOwner()) { return this._makeRedaction(manualRedactionEntry); } else { @@ -174,7 +174,7 @@ export class ManualAnnotationService { } } - private _makeRedactionRequest(manualRedactionEntry: ManualRedactionEntry) { + private _makeRedactionRequest(manualRedactionEntry: AddRedactionRequest) { return this._manualRedactionControllerService .requestAddRedaction(manualRedactionEntry, this._appStateService.activeProject.project.projectId, this._appStateService.activeFile.fileId) .pipe( @@ -185,7 +185,7 @@ export class ManualAnnotationService { ); } - private _makeRedaction(manualRedactionEntry: ManualRedactionEntry) { + private _makeRedaction(manualRedactionEntry: AddRedactionRequest) { return this._manualRedactionControllerService .addRedaction(manualRedactionEntry, this._appStateService.activeProject.project.projectId, this._appStateService.activeFile.fileId) .pipe( diff --git a/apps/red-ui/src/app/screens/project-listing-screen/project-listing-actions/project-listing-actions.component.ts b/apps/red-ui/src/app/screens/project-listing-screen/project-listing-actions/project-listing-actions.component.ts index 960128d3a..5344a95b3 100644 --- a/apps/red-ui/src/app/screens/project-listing-screen/project-listing-actions/project-listing-actions.component.ts +++ b/apps/red-ui/src/app/screens/project-listing-screen/project-listing-actions/project-listing-actions.component.ts @@ -43,11 +43,11 @@ export class ProjectListingActionsComponent implements OnInit { }); } - async reanalyseProject($event: MouseEvent, project: ProjectWrapper) { + reanalyseProject($event: MouseEvent, project: ProjectWrapper) { $event.stopPropagation(); - await this.appStateService.reanalyzeProject(project); - await this.appStateService.loadAllProjects(); - this.actionPerformed.emit(); + this.appStateService.reanalyzeProject(project).then(() => { + this.appStateService.loadAllProjects().then(() => this.actionPerformed.emit()); + }); } // Download Files diff --git a/apps/red-ui/src/app/utils/functions.ts b/apps/red-ui/src/app/utils/functions.ts index 852fe8c73..0e6d5c879 100644 --- a/apps/red-ui/src/app/utils/functions.ts +++ b/apps/red-ui/src/app/utils/functions.ts @@ -42,3 +42,42 @@ export function keypress(key: string) { export function reference(x: any) { return x; } + +export function getFirstRelevantTextPart(text, direction: 'FORWARD' | 'BACKWARD') { + let spaceCount = 0; + let accumulator = ''; + const breakChars = ['/', ':']; + + const handle = (i) => { + const char = text[i]; + if (char === ' ') { + spaceCount += 1; + } + if (spaceCount >= 2) { + return true; + } + accumulator += char; + + return breakChars.indexOf(char) >= 0; + }; + + if (direction === 'FORWARD') { + for (let i = 0; i < text.length; i++) { + const shouldBreak = handle(i); + if (shouldBreak) { + break; + } + } + } else { + for (let i = text.length - 1; i >= 0; i--) { + const shouldBreak = handle(i); + if (shouldBreak) { + break; + } + } + } + if (direction === 'BACKWARD') { + accumulator = accumulator.split('').reverse().join(''); + } + return accumulator; +} diff --git a/apps/red-ui/src/assets/i18n/en.json b/apps/red-ui/src/assets/i18n/en.json index 41f9758fd..6792c1ae2 100644 --- a/apps/red-ui/src/assets/i18n/en.json +++ b/apps/red-ui/src/assets/i18n/en.json @@ -365,7 +365,8 @@ "suggest-remove-from-dict": "Suggest to remove from dictionary", "suggest-only-here": "Suggest to remove only here", "remove-from-dict": "Remove from dictionary", - "only-here": "Remove only here" + "only-here": "Remove only here", + "false-positive": "False Positive" }, "remove": "Remove", "undo": "Undo", diff --git a/apps/red-ui/src/assets/icons/general/thumb-down.svg b/apps/red-ui/src/assets/icons/general/thumb-down.svg new file mode 100644 index 000000000..b8f41c22e --- /dev/null +++ b/apps/red-ui/src/assets/icons/general/thumb-down.svg @@ -0,0 +1,32 @@ + + + + + diff --git a/libs/red-ui-http/src/lib/model/redactionLogEntry.ts b/libs/red-ui-http/src/lib/model/redactionLogEntry.ts index 310a51c8d..f2b3e5395 100644 --- a/libs/red-ui-http/src/lib/model/redactionLogEntry.ts +++ b/libs/red-ui-http/src/lib/model/redactionLogEntry.ts @@ -27,9 +27,12 @@ export interface RedactionLogEntry { section?: string; sectionNumber?: number; status?: RedactionLogEntry.StatusEnum; + textAfter?: string; + textBefore?: string; type?: string; value?: string; } + export namespace RedactionLogEntry { export type ManualRedactionTypeEnum = 'ADD' | 'REMOVE'; export const ManualRedactionTypeEnum = {