diff --git a/apps/red-ui/src/app/models/file/annotation.permissions.ts b/apps/red-ui/src/app/models/file/annotation.permissions.ts index db08b66c2..bf2197b92 100644 --- a/apps/red-ui/src/app/models/file/annotation.permissions.ts +++ b/apps/red-ui/src/app/models/file/annotation.permissions.ts @@ -18,6 +18,8 @@ export class AnnotationPermissions { canChangeLegalBasis: boolean; + canRecategorizeImage: boolean; + get canPerformMultipleRemoveActions() { return ( this.canMarkTextOnlyAsFalsePositive + @@ -63,6 +65,8 @@ export class AnnotationPermissions { permissions.canChangeLegalBasis = !annotation.isManualRedaction && annotation.isRedacted; + permissions.canRecategorizeImage = annotation.isImage; + return permissions; } } diff --git a/apps/red-ui/src/app/modules/dossier/components/annotation-actions/annotation-actions.component.html b/apps/red-ui/src/app/modules/dossier/components/annotation-actions/annotation-actions.component.html index 30af3e697..46c3227c9 100644 --- a/apps/red-ui/src/app/modules/dossier/components/annotation-actions/annotation-actions.component.html +++ b/apps/red-ui/src/app/modules/dossier/components/annotation-actions/annotation-actions.component.html @@ -56,6 +56,17 @@ > + + + +
+
+ +
+
+ + + + {{ 'recategorize-image-dialog.options.' + option | translate }} + + +
+ +
+ + +
+
+ +
+ +
+
+
+ + + diff --git a/apps/red-ui/src/app/modules/dossier/dialogs/recategorize-image-dialog/recategorize-image-dialog.component.scss b/apps/red-ui/src/app/modules/dossier/dialogs/recategorize-image-dialog/recategorize-image-dialog.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/apps/red-ui/src/app/modules/dossier/dialogs/recategorize-image-dialog/recategorize-image-dialog.component.ts b/apps/red-ui/src/app/modules/dossier/dialogs/recategorize-image-dialog/recategorize-image-dialog.component.ts new file mode 100644 index 000000000..e803cd26a --- /dev/null +++ b/apps/red-ui/src/app/modules/dossier/dialogs/recategorize-image-dialog/recategorize-image-dialog.component.ts @@ -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, + @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 + }); + } +} diff --git a/apps/red-ui/src/app/modules/dossier/dossiers.module.ts b/apps/red-ui/src/app/modules/dossier/dossiers.module.ts index 40f754d51..fdf77b8ed 100644 --- a/apps/red-ui/src/app/modules/dossier/dossiers.module.ts +++ b/apps/red-ui/src/app/modules/dossier/dossiers.module.ts @@ -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 = [ diff --git a/apps/red-ui/src/app/modules/dossier/services/annotation-actions.service.ts b/apps/red-ui/src/app/modules/dossier/services/annotation-actions.service.ts index fee998b6c..ebaf549c2 100644 --- a/apps/red-ui/src/app/modules/dossier/services/annotation-actions.service.ts +++ b/apps/red-ui/src/app/modules/dossier/services/annotation-actions.service.ts @@ -140,6 +140,28 @@ export class AnnotationActionsService { }); } + recategorizeImage( + $event: MouseEvent, + annotation: AnnotationWrapper, + annotationsChanged: EventEmitter + ) { + 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 diff --git a/apps/red-ui/src/app/modules/dossier/services/dossiers-dialog.service.ts b/apps/red-ui/src/app/modules/dossier/services/dossiers-dialog.service.ts index 7a4b3b8a3..1ed9fd248 100644 --- a/apps/red-ui/src/app/modules/dossier/services/dossiers-dialog.service.ts +++ b/apps/red-ui/src/app/modules/dossier/services/dossiers-dialog.service.ts @@ -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 { + $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[], diff --git a/apps/red-ui/src/app/modules/dossier/services/manual-annotation.service.ts b/apps/red-ui/src/app/modules/dossier/services/manual-annotation.service.ts index 329512a24..990fd71c5 100644 --- a/apps/red-ui/src/app/modules/dossier/services/manual-annotation.service.ts +++ b/apps/red-ui/src/app/modules/dossier/services/manual-annotation.service.ts @@ -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.') + diff --git a/apps/red-ui/src/assets/i18n/en.json b/apps/red-ui/src/assets/i18n/en.json index 69725fb84..1a7b16d64 100644 --- a/apps/red-ui/src/assets/i18n/en.json +++ b/apps/red-ui/src/assets/i18n/en.json @@ -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",