Pull request #233: RED-1306

Merge in RED/ui from RED-1306 to master

* commit 'b58e33d57c948cb8919d3ab6e6d03b82c7e239a8':
  Image recategorization done
  Recategorize image WIP & refactor manual annotation service
This commit is contained in:
Adina Teudan 2021-07-07 14:30:58 +02:00
commit 52ad0d3ca5
11 changed files with 335 additions and 309 deletions

View File

@ -18,6 +18,8 @@ export class AnnotationPermissions {
canChangeLegalBasis: boolean;
canRecategorizeImage: boolean;
get canPerformMultipleRemoveActions() {
return (
<any>this.canMarkTextOnlyAsFalsePositive +
@ -63,6 +65,8 @@ export class AnnotationPermissions {
permissions.canChangeLegalBasis = !annotation.isManualRedaction && annotation.isRedacted;
permissions.canRecategorizeImage = annotation.isImage;
return permissions;
}
}

View File

@ -56,6 +56,17 @@
>
</redaction-circle-button>
<redaction-circle-button
(action)="
annotationActionsService.recategorizeImage($event, annotation, annotationsChanged)
"
*ngIf="annotationPermissions.canRecategorizeImage"
icon="red:thumb-down"
tooltip="annotation-actions.recategorize-image"
type="dark-bg"
>
</redaction-circle-button>
<redaction-circle-button
(action)="
annotationActionsService.markAsFalsePositive($event, [annotation], annotationsChanged)

View File

@ -56,7 +56,6 @@ export class ChangeLegalBasisDialogComponent implements OnInit {
}))
.sort((a, b) => a.label.localeCompare(b.label));
// this.annotation.
this.legalBasisForm.patchValue({
reason: this.legalOptions.find(
option => option.legalBasis === this.annotation.legalBasis

View File

@ -0,0 +1,53 @@
<section class="dialog">
<form (submit)="save()" [formGroup]="recategorizeImageForm">
<div class="dialog-header heading-l" translate="recategorize-image-dialog.header"></div>
<div class="dialog-content">
<div class="red-input-group required w-400">
<label translate="recategorize-image-dialog.content.type"></label>
<mat-select
[placeholder]="'recategorize-image-dialog.content.type-placeholder' | translate"
class="full-width"
formControlName="type"
>
<mat-option *ngFor="let option of typeOptions" [value]="option">
{{ 'recategorize-image-dialog.options.' + option | translate }}
</mat-option>
</mat-select>
</div>
<div [class.required]="!isDocumentAdmin" class="red-input-group w-300">
<label translate="recategorize-image-dialog.content.comment"></label>
<textarea
formControlName="comment"
name="comment"
redactionHasScrollbar
rows="4"
type="text"
></textarea>
</div>
</div>
<div class="dialog-actions">
<button
[disabled]="!recategorizeImageForm.valid || !changed"
color="primary"
mat-flat-button
type="submit"
>
{{ 'recategorize-image-dialog.actions.save' | translate }}
</button>
<div
class="all-caps-label cancel"
mat-dialog-close
translate="recategorize-image-dialog.actions.cancel"
></div>
</div>
</form>
<redaction-circle-button
class="dialog-close"
icon="red:close"
mat-dialog-close
></redaction-circle-button>
</section>

View File

@ -0,0 +1,43 @@
import { Component, Inject, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { PermissionsService } from '../../../../services/permissions.service';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { AnnotationWrapper } from '../../../../models/file/annotation.wrapper';
@Component({
selector: 'redaction-recategorize-image-dialog',
templateUrl: './recategorize-image-dialog.component.html',
styleUrls: ['./recategorize-image-dialog.component.scss']
})
export class RecategorizeImageDialogComponent implements OnInit {
recategorizeImageForm: FormGroup;
isDocumentAdmin: boolean;
typeOptions: string[] = ['signature', 'logo', 'formula', 'image'];
constructor(
private readonly _permissionsService: PermissionsService,
private readonly _formBuilder: FormBuilder,
public dialogRef: MatDialogRef<RecategorizeImageDialogComponent>,
@Inject(MAT_DIALOG_DATA) public annotation: AnnotationWrapper
) {}
get changed(): boolean {
return this.recategorizeImageForm.get('type').value !== this.annotation.dictionary;
}
async ngOnInit() {
this.isDocumentAdmin = this._permissionsService.isApprover();
this.recategorizeImageForm = this._formBuilder.group({
type: [this.annotation.dictionary, Validators.required],
comment: this.isDocumentAdmin ? [null] : [null, Validators.required]
});
}
save() {
this.dialogRef.close({
type: this.recategorizeImageForm.get('type').value,
comment: this.recategorizeImageForm.get('comment').value
});
}
}

View File

@ -47,6 +47,7 @@ import { TeamMembersDialogComponent } from './dialogs/team-members-dialog/team-m
import { ScrollButtonComponent } from './components/scroll-button/scroll-button.component';
import { ChangeLegalBasisDialogComponent } from './dialogs/change-legal-basis-dialog/change-legal-basis-dialog.component';
import { PageExclusionComponent } from './components/page-exclusion/page-exclusion.component';
import { RecategorizeImageDialogComponent } from './dialogs/recategorize-image-dialog/recategorize-image-dialog.component';
const screens = [
DossierListingScreenComponent,
@ -64,7 +65,8 @@ const dialogs = [
DocumentInfoDialogComponent,
AssignReviewerApproverDialogComponent,
DossierDictionaryDialogComponent,
ChangeLegalBasisDialogComponent
ChangeLegalBasisDialogComponent,
RecategorizeImageDialogComponent
];
const components = [

View File

@ -140,6 +140,28 @@ export class AnnotationActionsService {
});
}
recategorizeImage(
$event: MouseEvent,
annotation: AnnotationWrapper,
annotationsChanged: EventEmitter<AnnotationWrapper>
) {
this._dialogService.openRecategorizeImageDialog(
$event,
annotation,
(data: { type: string; comment: string }) => {
this._processObsAndEmit(
this._manualAnnotationService.recategorizeImage(
annotation.annotationId,
data.type,
data.comment
),
annotation,
annotationsChanged
);
}
);
}
undoDirectAction(
$event: MouseEvent,
annotations: AnnotationWrapper[],
@ -187,6 +209,21 @@ export class AnnotationActionsService {
)
}));
const canRecategorizeImage =
annotations.length === 1 && annotationPermissions[0].permissions.canRecategorizeImage;
if (canRecategorizeImage) {
availableActions.push({
type: 'actionButton',
img: this._convertPath('/assets/icons/general/thumb-down.svg'),
title: this._translateService.instant('annotation-actions.recategorize-image'),
onClick: () => {
this._ngZone.run(() => {
this.recategorizeImage(null, annotations[0], annotationsChanged);
});
}
});
}
const canRemoveOrSuggestToRemoveFromDictionary = annotationPermissions.reduce(
(acc, next) => acc && next.permissions.canRemoveOrSuggestToRemoveFromDictionary,
true

View File

@ -31,6 +31,7 @@ import { AssignReviewerApproverDialogComponent } from '../dialogs/assign-reviewe
import { TeamMembersDialogComponent } from '../dialogs/team-members-dialog/team-members-dialog.component';
import { AppConfigService } from '../../app-config/app-config.service';
import { ChangeLegalBasisDialogComponent } from '../dialogs/change-legal-basis-dialog/change-legal-basis-dialog.component';
import { RecategorizeImageDialogComponent } from '../dialogs/recategorize-image-dialog/recategorize-image-dialog.component';
const dialogConfig = {
width: '662px',
@ -189,6 +190,24 @@ export class DossiersDialogService {
return ref;
}
openRecategorizeImageDialog(
$event: MouseEvent,
annotation: AnnotationWrapper,
cb?: Function
): MatDialogRef<RecategorizeImageDialogComponent> {
$event?.stopPropagation();
const ref = this._dialog.open(RecategorizeImageDialogComponent, {
...dialogConfig,
data: annotation
});
ref.afterClosed().subscribe(async result => {
if (result && cb) {
cb(result);
}
});
return ref;
}
openRemoveFromDictionaryDialog(
$event: MouseEvent,
annotations: AnnotationWrapper[],

View File

@ -13,8 +13,27 @@ import { tap } from 'rxjs/operators';
import { UserService } from '@services/user.service';
import { PermissionsService } from '@services/permissions.service';
type Mode =
| 'add'
| 'approve'
| 'remove'
| 'change-legal-basis'
| 'decline'
| 'request-remove'
| 'request-change-legal-basis'
| 'recategorize-image'
| 'request-image-recategorization'
| 'suggest'
| 'undo'
| 'force-redaction'
| 'request-force-redaction';
@Injectable()
export class ManualAnnotationService {
CONFIG: {
[key in Mode]: string;
};
constructor(
private readonly _appStateService: AppStateService,
private readonly _userService: UserService,
@ -23,7 +42,50 @@ export class ManualAnnotationService {
private readonly _manualRedactionControllerService: ManualRedactionControllerService,
private readonly _dictionaryControllerService: DictionaryControllerService,
private readonly _permissionsService: PermissionsService
) {}
) {
this.CONFIG = {
add: 'addRedaction',
'recategorize-image': 'recategorizeImage',
'request-image-recategorization': 'requestImageRecategorization',
'change-legal-basis': 'legalBasisChange',
'request-change-legal-basis': 'requestLegalBasisChange',
'request-remove': 'requestRemoveRedaction',
approve: 'approveRequest',
decline: 'declineRequest',
undo: 'undo',
remove: 'removeRedaction',
suggest: 'requestAddRedaction',
'force-redaction': 'forceRedaction',
'request-force-redaction': 'requestForceRedaction'
};
}
_makeRequest(mode: Mode, body: any, secondParam: any = null, modifyDictionary = false) {
const obs = !secondParam
? this._manualRedactionControllerService[this.CONFIG[mode]](
body,
this._appStateService.activeDossierId,
this._appStateService.activeFileId
)
: this._manualRedactionControllerService[this.CONFIG[mode]](
body,
secondParam,
this._appStateService.activeDossierId,
this._appStateService.activeFileId
);
return obs.pipe(
tap(
() => this._notify(this._getMessage(mode)),
error =>
this._notify(
this._getMessage(mode, modifyDictionary, true),
NotificationType.ERROR,
error
)
)
);
}
// Comments
// this wraps /manualRedaction/comment/add
@ -62,137 +124,77 @@ export class ManualAnnotationService {
// /manualRedaction/redaction/legalBasisChange
// /manualRedaction/request/legalBasis
changeLegalBasis(annotationId: string, legalBasis: string, comment?: string) {
if (this._permissionsService.isApprover()) {
return this._makeLegalBasisChange(annotationId, legalBasis, comment);
} else {
return this._makeLegalBasisChangeRequest(annotationId, legalBasis, comment);
}
const mode: Mode = this._permissionsService.isApprover()
? 'change-legal-basis'
: 'request-change-legal-basis';
return this._makeRequest(mode, { annotationId, legalBasis, comment });
}
// this wraps
// /manualRedaction/redaction/recategorize
// /manualRedaction/request/recategorize
recategorizeImage(annotationId: string, type: string, comment: string) {
const mode: Mode = this._permissionsService.isApprover()
? 'recategorize-image'
: 'request-image-recategorization';
return this._makeRequest(mode, { annotationId, type, comment });
}
// this wraps
// /manualRedaction/redaction/add
// /manualRedaction/request/add
addAnnotation(manualRedactionEntry: AddRedactionRequest) {
if (this._permissionsService.isApprover()) {
return this._makeRedaction(manualRedactionEntry);
} else {
return this._makeRedactionRequest(manualRedactionEntry);
}
const mode: Mode = this._permissionsService.isApprover() ? 'add' : 'suggest';
return this._makeRequest(
mode,
manualRedactionEntry,
null,
manualRedactionEntry.addToDictionary
);
}
// this wraps
// /manualRedaction/redaction/force
// /manualRedaction/request/force
forceRedaction(request: ForceRedactionRequest) {
if (this._permissionsService.isApprover()) {
return this._makeForceRedaction(request);
} else {
return this._makeForceRedactionRequest(request);
}
const mode: Mode = this._permissionsService.isApprover()
? 'force-redaction'
: 'request-force-redaction';
return this._makeRequest(mode, request);
}
// this wraps
// /manualRedaction/approve
approveRequest(annotationId: string, addToDictionary: boolean = false) {
// for only here - approve the request
return this._manualRedactionControllerService
.approveRequest(
{ addOrRemoveFromDictionary: addToDictionary },
annotationId,
this._appStateService.activeDossierId,
this._appStateService.activeFile.fileId
)
.pipe(
tap(
() => this._notify(this._getMessage('approve', addToDictionary)),
error =>
this._notify(
this._getMessage('approve', addToDictionary, true),
NotificationType.ERROR,
error
)
)
);
return this._makeRequest(
'approve',
{ addOrRemoveFromDictionary: addToDictionary },
annotationId,
addToDictionary
);
}
undoRequest(annotationWrapper: AnnotationWrapper) {
return this._manualRedactionControllerService
.undo(
annotationWrapper.id,
this._appStateService.activeDossierId,
this._appStateService.activeFileId
)
.pipe(
tap(
() =>
this._notify(
this._getMessage('undo', annotationWrapper.isModifyDictionary)
),
error =>
this._notify(
this._getMessage('undo', annotationWrapper.isModifyDictionary, true),
NotificationType.ERROR,
error
)
)
);
return this._makeRequest(
'undo',
annotationWrapper.id,
null,
annotationWrapper.isModifyDictionary
);
}
// this wraps
// /manualRedaction/decline/remove
// /manualRedaction/undo
declineOrRemoveRequest(annotationWrapper: AnnotationWrapper) {
if (this._permissionsService.isApprover()) {
return this._manualRedactionControllerService
.declineRequest(
annotationWrapper.id,
this._appStateService.activeDossierId,
this._appStateService.activeFileId
)
.pipe(
tap(
() =>
this._notify(
this._getMessage('decline', annotationWrapper.isModifyDictionary)
),
error =>
this._notify(
this._getMessage(
'decline',
annotationWrapper.isModifyDictionary,
true
),
NotificationType.ERROR,
error
)
)
);
} else {
return this._manualRedactionControllerService
.undo(
annotationWrapper.id,
this._appStateService.activeDossierId,
this._appStateService.activeFileId
)
.pipe(
tap(
() =>
this._notify(
this._getMessage('undo', annotationWrapper.isModifyDictionary)
),
error =>
this._notify(
this._getMessage(
'undo',
annotationWrapper.isModifyDictionary,
true
),
NotificationType.ERROR,
error
)
)
);
}
const mode: Mode = this._permissionsService.isApprover() ? 'decline' : 'undo';
return this._makeRequest(
mode,
annotationWrapper.id,
null,
annotationWrapper.isModifyDictionary
);
}
// this wraps
@ -202,73 +204,35 @@ export class ManualAnnotationService {
annotationWrapper: AnnotationWrapper,
removeFromDictionary: boolean = false
) {
let mode: Mode,
body: any,
removeDict = false;
if (this._permissionsService.isApprover()) {
// if it was something manual simply decline the existing request
if (annotationWrapper.dictionary === 'manual') {
return this._manualRedactionControllerService
.declineRequest(
annotationWrapper.id,
this._appStateService.activeDossierId,
this._appStateService.activeFileId
)
.pipe(
tap(
() => this._notify(this._getMessage('decline', false)),
error =>
this._notify(
this._getMessage('decline', false, true),
NotificationType.ERROR,
error
)
)
);
mode = 'decline';
body = annotationWrapper.id;
} else {
return this._manualRedactionControllerService
.removeRedaction(
{
annotationId: annotationWrapper.id,
removeFromDictionary: removeFromDictionary,
comment: '-'
},
this._appStateService.activeDossierId,
this._appStateService.activeFileId
)
.pipe(
tap(
() => this._notify(this._getMessage('remove', removeFromDictionary)),
error =>
this._notify(
this._getMessage('remove', removeFromDictionary, true),
NotificationType.ERROR,
error
)
)
);
mode = 'remove';
body = {
annotationId: annotationWrapper.id,
removeFromDictionary,
comment: '-'
};
removeDict = removeFromDictionary;
}
} else {
return this._manualRedactionControllerService
.requestRemoveRedaction(
{
annotationId: annotationWrapper.id,
removeFromDictionary: removeFromDictionary,
comment: '-'
},
this._appStateService.activeDossierId,
this._appStateService.activeFileId
)
.pipe(
tap(
() =>
this._notify(this._getMessage('request-remove', removeFromDictionary)),
error =>
this._notify(
this._getMessage('request-remove', removeFromDictionary, true),
NotificationType.ERROR,
error
)
)
);
mode = 'request-remove';
body = {
annotationId: annotationWrapper.id,
removeFromDictionary,
comment: '-'
};
removeDict = removeFromDictionary;
}
return this._makeRequest(mode, body, null, removeDict);
}
getTitle(type: 'DICTIONARY' | 'REDACTION' | 'FALSE_POSITIVE') {
@ -293,134 +257,6 @@ export class ManualAnnotationService {
}
}
private _makeForceRedactionRequest(forceRedactionRequest: ForceRedactionRequest) {
return this._manualRedactionControllerService
.requestForceRedaction(
forceRedactionRequest,
this._appStateService.activeDossierId,
this._appStateService.activeFileId
)
.pipe(
tap(
() => this._notify(this._getMessage('suggest', false)),
error =>
this._notify(
this._getMessage('suggest', false, true),
NotificationType.ERROR,
error
)
)
);
}
private _makeForceRedaction(forceRedactionRequest: ForceRedactionRequest) {
return this._manualRedactionControllerService
.forceRedaction(
forceRedactionRequest,
this._appStateService.activeDossierId,
this._appStateService.activeFileId
)
.pipe(
tap(
() => this._notify(this._getMessage('add', false)),
error =>
this._notify(
this._getMessage('add', false, true),
NotificationType.ERROR,
error
)
)
);
}
private _makeRedactionRequest(manualRedactionEntry: AddRedactionRequest) {
return this._manualRedactionControllerService
.requestAddRedaction(
manualRedactionEntry,
this._appStateService.activeDossierId,
this._appStateService.activeFileId
)
.pipe(
tap(
() =>
this._notify(
this._getMessage('suggest', manualRedactionEntry.addToDictionary)
),
error =>
this._notify(
this._getMessage('suggest', manualRedactionEntry.addToDictionary, true),
NotificationType.ERROR,
error
)
)
);
}
private _makeLegalBasisChange(annotationId: string, legalBasis: string, comment?: string) {
return this._manualRedactionControllerService
.legalBasisChange(
{ annotationId, legalBasis, comment },
this._appStateService.activeDossierId,
this._appStateService.activeFileId
)
.pipe(
tap(
() => this._notify(this._getMessage('change-legal-basis')),
error =>
this._notify(
this._getMessage('change-legal-basis', false, true),
NotificationType.ERROR,
error
)
)
);
}
private _makeLegalBasisChangeRequest(
annotationId: string,
legalBasis: string,
comment?: string
) {
return this._manualRedactionControllerService
.requestLegalBasisChange(
{ annotationId, legalBasis, comment },
this._appStateService.activeDossierId,
this._appStateService.activeFileId
)
.pipe(
tap(
() => this._notify(this._getMessage('request-change-legal-basis')),
error =>
this._notify(
this._getMessage('request-change-legal-basis', false, true),
NotificationType.ERROR,
error
)
)
);
}
private _makeRedaction(manualRedactionEntry: AddRedactionRequest) {
return this._manualRedactionControllerService
.addRedaction(
manualRedactionEntry,
this._appStateService.activeDossierId,
this._appStateService.activeFileId
)
.pipe(
tap(
() =>
this._notify(this._getMessage('add', manualRedactionEntry.addToDictionary)),
error =>
this._notify(
this._getMessage('add', manualRedactionEntry.addToDictionary, true),
NotificationType.ERROR,
error
)
)
);
}
private _notify(key: string, type: NotificationType = NotificationType.SUCCESS, data?: any) {
this._notificationService.showToastNotification(
this._translateService.instant(key, data),
@ -433,20 +269,7 @@ export class ManualAnnotationService {
);
}
private _getMessage(
mode:
| 'add'
| 'remove'
| 'request-remove'
| 'suggest'
| 'approve'
| 'decline'
| 'undo'
| 'change-legal-basis'
| 'request-change-legal-basis',
modifyDictionary?: boolean,
error: boolean = false
) {
private _getMessage(mode: Mode, modifyDictionary?: boolean, error: boolean = false) {
return (
'annotation-actions.message.' +
(modifyDictionary ? 'dictionary.' : 'manual-redaction.') +

View File

@ -155,6 +155,10 @@
"error": "Failed to save redaction: {{error}}",
"success": "Redaction added!"
},
"force-redaction": {
"error": "Failed to save redaction: {{error}}",
"success": "Redaction added!"
},
"approve": {
"error": "Failed to approve suggestion: {{error}}",
"success": "Suggestion approved."
@ -171,17 +175,30 @@
"error": "Failed to request annotation reason change: {{error}}",
"success": "Annotation reason change requested."
},
"recategorize-image": {
"error": "Failed to recategorize image: {{error}}",
"success": "Image recategorized."
},
"request-image-recategorization": {
"error": "Failed to request image recategorization: {{error}}",
"success": "Image recategorization requested."
},
"search": "Document name...",
"suggest": {
"error": "Failed to save redaction suggestion: {{error}}",
"success": "Redaction suggestion saved"
},
"request-force-redaction": {
"error": "Failed to save redaction suggestion: {{error}}",
"success": "Redaction suggestion saved"
},
"undo": {
"error": "Failed to undo: {{error}}",
"success": "Undo successful"
}
}
},
"recategorize-image": "Recategorize",
"reject": "Reject",
"reject-suggestion": "Reject Suggestion",
"remove": "Remove",
@ -303,6 +320,24 @@
},
"header": "Edit Redaction Reason"
},
"recategorize-image-dialog": {
"actions": {
"cancel": "Cancel",
"save": "Save Changes"
},
"content": {
"comment": "Comment",
"type": "Select image type",
"type-placeholder": "Select a type..."
},
"header": "Edit Image Type",
"options": {
"image": "Image",
"logo": "Logo",
"signature": "Signature",
"formula": "Formula"
}
},
"comment": "Comment",
"comments": {
"add-comment": "Enter comment",