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 8c519683f..de7e54be2 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 @@ -1,8 +1,19 @@
+ + + @@ -13,6 +24,7 @@ *ngIf="canUndoAnnotation" type="dark-bg" icon="red:undo" + tooltipPosition="before" tooltip="annotation-actions.undo" > @@ -22,6 +34,7 @@ type="dark-bg" icon="red:close" *ngIf="canRejectSuggestion" + tooltipPosition="before" tooltip="annotation-actions.reject-suggestion" > @@ -31,6 +44,7 @@ type="dark-bg" icon="red:trash" *ngIf="canDirectlySuggestToRemoveAnnotation" + tooltipPosition="before" tooltip="annotation-actions.suggest-remove-annotation" > @@ -40,6 +54,7 @@ (action)="openMenu($event)" [class.active]="menuOpen" [matMenuTriggerFor]="menu" + tooltipPosition="before" tooltip="annotation-actions.suggest-remove-annotation" type="dark-bg" icon="red:trash" 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 d9aa71af1..ced12d2c4 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 @@ -13,6 +13,10 @@ padding-top: 8px; background: linear-gradient(to right, rgba(255, 255, 255, 0) 0%, #f9fafb, #f9fafb, #f9fafb); + redaction-circle-button { + display: block; + } + .confirm.active { background-color: $grey-2; } 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 f875544e0..0fb865b73 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 @@ -16,7 +16,7 @@ export class AnnotationActionsComponent implements OnInit { @Input() annotation: AnnotationWrapper; @Input() canPerformAnnotationActions: boolean; - @Output() annotationsChanged = new EventEmitter(); + @Output() annotationsChanged = new EventEmitter(); suggestionType: TypeValue; menuOpen: boolean; @@ -46,11 +46,18 @@ export class AnnotationActionsComponent implements OnInit { } get canDirectlySuggestToRemoveAnnotation() { - return this.annotation.isHint || (this.annotation.isManual && this.permissionsService.isManagerAndOwner() && !this.canUndoAnnotation); + return ( + (this.annotation.isHint || (this.annotation.isManual && this.permissionsService.isManagerAndOwner() && !this.canUndoAnnotation)) && + !this.annotation.isRecommendation + ); } get requiresSuggestionRemoveMenu() { - return this.annotation.isRedacted || this.annotation.isIgnored; + return (this.annotation.isRedacted || this.annotation.isIgnored) && !this.annotation.isRecommendation; + } + + get canConvertRecommendationToAnnotation() { + return this.annotation.isRecommendation; } get canUndoAnnotation() { @@ -84,8 +91,8 @@ export class AnnotationActionsComponent implements OnInit { private _processObsAndEmit(obs: Observable) { obs.subscribe( - () => { - this.annotationsChanged.emit(); + (data) => { + this.annotationsChanged.emit(!!data?.annotationId); }, () => { this.annotationsChanged.emit(); @@ -109,4 +116,9 @@ export class AnnotationActionsComponent implements OnInit { get dictionaryColor() { return this.appStateService.getDictionaryColor('suggestion-add-dictionary'); } + + convertRecommendationToAnnotation($event: any, annotation: AnnotationWrapper) { + $event.stopPropagation(); + this._processObsAndEmit(this._manualAnnotationService.addRecommendation(annotation)); + } } diff --git a/apps/red-ui/src/app/screens/file/file-preview-screen/file-preview-screen.component.html b/apps/red-ui/src/app/screens/file/file-preview-screen/file-preview-screen.component.html index fb9ea1e45..7ce8378cd 100644 --- a/apps/red-ui/src/app/screens/file/file-preview-screen/file-preview-screen.component.html +++ b/apps/red-ui/src/app/screens/file/file-preview-screen/file-preview-screen.component.html @@ -200,7 +200,7 @@
diff --git a/apps/red-ui/src/app/screens/file/file-preview-screen/file-preview-screen.component.ts b/apps/red-ui/src/app/screens/file/file-preview-screen/file-preview-screen.component.ts index 22daacf31..17838a479 100644 --- a/apps/red-ui/src/app/screens/file/file-preview-screen/file-preview-screen.component.ts +++ b/apps/red-ui/src/app/screens/file/file-preview-screen/file-preview-screen.component.ts @@ -441,13 +441,41 @@ export class FilePreviewScreenComponent implements OnInit, OnDestroy { }); } - async annotationsChangedByReviewAction(annotation: AnnotationWrapper) { - const viewerAnnotation = this.activeViewer.annotManager.getAnnotationById(annotation.id); + private async _cleanupAndRedrawManualAnnotationsForEntirePage(page: number) { + const currentPageAnnotationIds = this.displayedAnnotations[page].annotations.map((a) => a.id); + this.fileData.fileStatus = await this.appStateService.reloadActiveFile(); + + this._fileDownloadService.loadActiveFileManualAnnotations().subscribe((manualRedactions) => { + this.fileData.manualRedactions = manualRedactions; + this._rebuildFilters(); + if (!this.redactedView) { + currentPageAnnotationIds.forEach((id) => { + this._findAndDeleteAnnotation(id); + }); + this._annotationDrawService.drawAnnotations( + this.instance, + this.annotations.filter((item) => item.pageNumber === page) + ); + document.querySelectorAll('iframe')[0].click(); + } + }); + } + + async annotationsChangedByReviewAction(requiresCompletePageRedraw: boolean, annotation: AnnotationWrapper) { + if (!requiresCompletePageRedraw) { + this._findAndDeleteAnnotation(annotation.id); + this.fileData.fileStatus = await this.appStateService.reloadActiveFile(); + this._cleanupAndRedrawManualAnnotations(annotation.id); + } else { + await this._cleanupAndRedrawManualAnnotationsForEntirePage(annotation.pageNumber); + } + } + + private _findAndDeleteAnnotation(id: string) { + const viewerAnnotation = this.activeViewer.annotManager.getAnnotationById(id); if (viewerAnnotation) { this.activeViewer.annotManager.deleteAnnotation(viewerAnnotation, true, true); } - this.fileData.fileStatus = await this.appStateService.reloadActiveFile(); - this._cleanupAndRedrawManualAnnotations(annotation.id); } async fileActionPerformed(action: string) { @@ -496,5 +524,6 @@ export class FilePreviewScreenComponent implements OnInit, OnDestroy { async openHTMLDebug() { window.open(`/html-debug/${this.fileId}`, '_blank'); } + // } 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 0bab6c3c9..068970687 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 @@ -1,7 +1,6 @@ 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 { humanize } from '../../../utils/functions'; export class AnnotationWrapper { superType: @@ -31,7 +30,9 @@ export class AnnotationWrapper { redaction: boolean; status: string; dictionaryOperation: boolean; + recommendation: boolean; positions: Rectangle[]; + recommendationType: string; get isIgnored() { return this.superType === 'ignore'; @@ -81,6 +82,10 @@ export class AnnotationWrapper { return this.dictionaryOperation; } + get isRecommendation() { + return this.recommendation; + } + static fromData( user: UserWrapper, dictionaryData: { [p: string]: TypeValue }, @@ -107,6 +112,14 @@ export class AnnotationWrapper { 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; 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 5f0300937..9bb714b7a 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 @@ -63,16 +63,16 @@ export class FileDataModel { pairs.push({ redactionLogEntry: rdl, // only not declined - manualRedactionEntry: this.manualRedactions.entriesToAdd.find((eta) => eta.id === rdl.id), + manualRedactionEntry: this.manualRedactions.entriesToAdd.find((eta) => (eta.id === rdl.id || eta.reason === rdl.id) && this._dateValid(eta)), // only not declined - idRemoval: this.manualRedactions.idsToRemove.find((idr) => idr.id === rdl.id), + idRemoval: this.manualRedactions.idsToRemove.find((idr) => idr.id === rdl.id && this._dateValid(idr)), comments: this.manualRedactions.comments[rdl.id] }); }); this.manualRedactions.entriesToAdd.forEach((eta) => { // only not declined - if (new Date(eta.processedDate).getTime() > new Date(this.fileStatus.lastProcessed).getTime() || !eta.processedDate) { + if (this._dateValid(eta)) { const redactionLogEntry = this.redactionLog.redactionLogEntry.find((rdl) => rdl.id === eta.id); if (!redactionLogEntry) { pairs.push({ @@ -88,4 +88,8 @@ export class FileDataModel { return pairs; } + + private _dateValid(entry: ManualRedactionEntry | IdRemoval): boolean { + return new Date(entry.processedDate).getTime() > new Date(this.fileStatus.lastProcessed).getTime() || !entry.processedDate; + } } 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 39464c893..f2050be6c 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 @@ -1,12 +1,13 @@ import { Injectable } from '@angular/core'; import { AppStateService } from '../../../state/app-state.service'; -import { DictionaryControllerService, ManualRedactionControllerService, ManualRedactionEntry } from '@redaction/red-ui-http'; +import { AddRedactionRequest, DictionaryControllerService, ManualRedactionControllerService, ManualRedactionEntry } from '@redaction/red-ui-http'; import { AnnotationWrapper } from '../model/annotation.wrapper'; import { NotificationService, NotificationType } from '../../../notification/notification.service'; import { TranslateService } from '@ngx-translate/core'; import { tap } from 'rxjs/operators'; import { UserService } from '../../../user/user.service'; import { PermissionsService } from '../../../common/service/permissions.service'; +import { of } from 'rxjs'; @Injectable({ providedIn: 'root' @@ -43,6 +44,17 @@ export class ManualAnnotationService { ); } + addRecommendation(annotation: AnnotationWrapper) { + const manualRedactionEntry: AddRedactionRequest = {}; + manualRedactionEntry.addToDictionary = true; + manualRedactionEntry.reason = annotation.id; // set the ID as reason, so we can hide the suggestion + manualRedactionEntry.value = annotation.value; + manualRedactionEntry.positions = annotation.positions; + manualRedactionEntry.type = annotation.recommendationType; + manualRedactionEntry.comment = { text: 'Accepted Recommendation' }; + return this.addAnnotation(manualRedactionEntry); + } + // this wraps // /manualRedaction/redaction/add // /manualRedaction/request/add diff --git a/apps/red-ui/src/assets/i18n/en.json b/apps/red-ui/src/assets/i18n/en.json index c4b2c9ae5..30592aebb 100644 --- a/apps/red-ui/src/assets/i18n/en.json +++ b/apps/red-ui/src/assets/i18n/en.json @@ -342,6 +342,9 @@ "remove-from-dict": "Approve and remove from dictionary", "only-here": "Approve only here" }, + "accept-recommendation": { + "label": "Accept Recommendation" + }, "suggest-remove-annotation": "Remove or Suggest to remove this entry", "reject-suggestion": "Reject Suggestion", "remove-annotation": {