diff --git a/apps/red-ui/src/app/modules/file-preview/components/annotation-actions/annotation-actions.component.ts b/apps/red-ui/src/app/modules/file-preview/components/annotation-actions/annotation-actions.component.ts index eb5b07569..57b5a5910 100644 --- a/apps/red-ui/src/app/modules/file-preview/components/annotation-actions/annotation-actions.component.ts +++ b/apps/red-ui/src/app/modules/file-preview/components/annotation-actions/annotation-actions.component.ts @@ -29,7 +29,7 @@ export class AnnotationActionsComponent implements OnChanges { @Input() tooltipPosition: 'before' | 'above' = 'before'; @Input() canPerformAnnotationActions: boolean; @Input() alwaysVisible: boolean; - @Output() readonly annotationsChanged = new EventEmitter(); + @Output() readonly annotationsChanged = new EventEmitter(); annotationPermissions: AnnotationPermissions; constructor( diff --git a/apps/red-ui/src/app/modules/file-preview/components/file-workload/file-workload.component.ts b/apps/red-ui/src/app/modules/file-preview/components/file-workload/file-workload.component.ts index 231577a37..143a20401 100644 --- a/apps/red-ui/src/app/modules/file-preview/components/file-workload/file-workload.component.ts +++ b/apps/red-ui/src/app/modules/file-preview/components/file-workload/file-workload.component.ts @@ -58,7 +58,7 @@ export class FileWorkloadComponent { @Input() annotationActionsTemplate: TemplateRef; @Output() readonly selectAnnotations = new EventEmitter(); @Output() readonly selectPage = new EventEmitter(); - @Output() readonly annotationsChanged = new EventEmitter(); + @Output() readonly annotationsChanged = new EventEmitter(); displayedPages: number[] = []; pagesPanelActive = true; readonly displayedAnnotations$: Observable>; diff --git a/apps/red-ui/src/app/modules/file-preview/components/pdf-viewer/pdf-viewer.component.ts b/apps/red-ui/src/app/modules/file-preview/components/pdf-viewer/pdf-viewer.component.ts index 3e6a774f7..cf3e6c4fb 100644 --- a/apps/red-ui/src/app/modules/file-preview/components/pdf-viewer/pdf-viewer.component.ts +++ b/apps/red-ui/src/app/modules/file-preview/components/pdf-viewer/pdf-viewer.component.ts @@ -60,7 +60,7 @@ export class PdfViewerComponent extends AutoUnsubscribe implements OnInit, OnCha @Output() readonly manualAnnotationRequested = new EventEmitter(); @Output() readonly pageChanged = new EventEmitter(); @Output() readonly keyUp = new EventEmitter(); - @Output() readonly annotationsChanged = new EventEmitter(); + @Output() readonly annotationsChanged = new EventEmitter(); @ViewChild('viewer', { static: true }) viewer: ElementRef; @ViewChild('compareFileInput', { static: true }) compareFileInput: ElementRef; instance: WebViewerInstance; diff --git a/apps/red-ui/src/app/modules/file-preview/services/annotation-actions.service.ts b/apps/red-ui/src/app/modules/file-preview/services/annotation-actions.service.ts index 09b704baf..acb3b5e32 100644 --- a/apps/red-ui/src/app/modules/file-preview/services/annotation-actions.service.ts +++ b/apps/red-ui/src/app/modules/file-preview/services/annotation-actions.service.ts @@ -9,7 +9,15 @@ import { AnnotationPermissions } from '@models/file/annotation.permissions'; import { BASE_HREF } from '../../../tokens'; import { UserService } from '@services/user.service'; import { Core } from '@pdftron/webviewer'; -import { DictionaryEntryTypes, Dossier, IAddRedactionRequest, ILegalBasisChangeRequest, IRectangle, IResizeRequest } from '@red/domain'; +import { + DictionaryEntryTypes, + Dossier, + IAddRedactionRequest, + ILegalBasisChangeRequest, + IRecategorizationRequest, + IRectangle, + IResizeRequest, +} from '@red/domain'; import { toPosition } from '../../dossier/utils/pdf-calculation.utils'; import { AnnotationDrawService } from './annotation-draw.service'; import { ActiveDossiersService } from '@services/dossiers/active-dossiers.service'; @@ -18,7 +26,7 @@ import { AcceptRecommendationDialogComponent, AcceptRecommendationReturnType, } from '../dialogs/accept-recommendation-dialog/accept-recommendation-dialog.component'; -import { defaultDialogConfig } from '@iqser/common-ui'; +import { defaultDialogConfig, List } from '@iqser/common-ui'; import { filter } from 'rxjs/operators'; import { MatDialog } from '@angular/material/dialog'; import { FilePreviewStateService } from './file-preview-state.service'; @@ -41,86 +49,85 @@ export class AnnotationActionsService { private readonly _pdf: PdfViewer, private readonly _annotationDrawService: AnnotationDrawService, private readonly _activeDossiersService: ActiveDossiersService, - private readonly _screenStateService: FilePreviewStateService, private readonly _dictionariesMapService: DictionariesMapService, + private readonly _state: FilePreviewStateService, ) {} private get _dossier(): Dossier { - return this._activeDossiersService.find(this._screenStateService.dossierId); + return this._activeDossiersService.find(this._state.dossierId); } - acceptSuggestion($event: MouseEvent, annotations: AnnotationWrapper[], annotationsChanged: EventEmitter) { + acceptSuggestion($event: MouseEvent, annotations: AnnotationWrapper[], annotationsChanged: EventEmitter) { $event?.stopPropagation(); - const { dossierId, fileId } = this._screenStateService; - annotations.forEach(annotation => { - this._processObsAndEmit( - this._manualRedactionService.approve(annotation.id, dossierId, fileId, annotation.isModifyDictionary), - annotation, - annotationsChanged, - ); - }); + const { dossierId, fileId } = this._state; + this._processObsAndEmit( + this._manualRedactionService.approve( + annotations.map(a => a.id), + dossierId, + fileId, + ), + annotations, + annotationsChanged, + ); } - rejectSuggestion($event: MouseEvent, annotations: AnnotationWrapper[], annotationsChanged: EventEmitter) { + rejectSuggestion($event: MouseEvent, annotations: AnnotationWrapper[], annotationsChanged: EventEmitter) { $event?.stopPropagation(); - const { dossierId, fileId } = this._screenStateService; - annotations.forEach(annotation => { - this._processObsAndEmit( - this._manualRedactionService.declineOrRemoveRequest(annotation, dossierId, fileId), - annotation, - annotationsChanged, - ); - }); + const { dossierId, fileId } = this._state; + const modifyDictionary = annotations[0].isModifyDictionary; + this._processObsAndEmit( + this._manualRedactionService.declineOrRemoveRequest( + annotations.map(a => a.id), + dossierId, + fileId, + modifyDictionary, + ), + annotations, + annotationsChanged, + ); } forceAnnotation( $event: MouseEvent, annotations: AnnotationWrapper[], - annotationsChanged: EventEmitter, + annotationsChanged: EventEmitter, hint: boolean = false, ) { - const { dossierId, fileId } = this._screenStateService; + const { dossierId, fileId } = this._state; const data = { dossier: this._dossier, annotations, hint }; this._dialogService.openDialog('forceAnnotation', $event, data, (request: ILegalBasisChangeRequest) => { - annotations.forEach(annotation => { - this._processObsAndEmit( - this._manualRedactionService.force( - { - ...request, - annotationId: annotation.id, - }, - dossierId, - fileId, - ), - annotation, - annotationsChanged, - ); - }); + this._processObsAndEmit( + this._manualRedactionService.bulkForce( + annotations.map(a => ({ ...request, annotationId: a.id })), + dossierId, + fileId, + ), + annotations, + annotationsChanged, + ); }); } - changeLegalBasis($event: MouseEvent, annotations: AnnotationWrapper[], annotationsChanged: EventEmitter) { - const { dossierId, fileId } = this._screenStateService; + changeLegalBasis($event: MouseEvent, annotations: AnnotationWrapper[], annotationsChanged: EventEmitter) { + const { dossierId, fileId } = this._state; this._dialogService.openDialog( 'changeLegalBasis', $event, { annotations, dossier: this._dossier }, (data: { comment: string; legalBasis: string; section: string; value: string }) => { - annotations.forEach(annotation => { - this._processObsAndEmit( - this._manualRedactionService.changeLegalBasis( - annotation.annotationId, - dossierId, - fileId, - data.section, - data.value, - data.legalBasis, - data.comment, - ), - annotation, - annotationsChanged, - ); - }); + const body = annotations.map(annotation => ({ + annotationId: annotation.id, + comment: data.comment, + legalBasis: data.legalBasis, + section: data.section, + value: data.value, + })); + + this._processObsAndEmit( + this._manualRedactionService.changeLegalBasis(body, dossierId, fileId), + annotations, + annotationsChanged, + ); }, ); } @@ -129,7 +136,7 @@ export class AnnotationActionsService { $event: MouseEvent, annotations: AnnotationWrapper[], removeFromDictionary: boolean, - annotationsChanged: EventEmitter, + annotationsChanged: EventEmitter, ) { const data = { annotationsToRemove: annotations, @@ -137,65 +144,63 @@ export class AnnotationActionsService { dossier: this._dossier, hint: annotations[0].hintDictionary, }; - const { dossierId, fileId } = this._screenStateService; + const { dossierId, fileId } = this._state; this._dialogService.openDialog('removeAnnotations', $event, data, (result: { comment: string }) => { - annotations.forEach(annotation => { - this._processObsAndEmit( - this._manualRedactionService.removeOrSuggestRemoveAnnotation( - annotation, - dossierId, - fileId, - result.comment, - removeFromDictionary, - ), - annotation, - annotationsChanged, - ); - }); - }); - } - - markAsFalsePositive($event: MouseEvent, annotations: AnnotationWrapper[], annotationsChanged: EventEmitter) { - annotations.forEach(annotation => { - this._markAsFalsePositive($event, annotation, this._getFalsePositiveText(annotation), annotationsChanged); - }); - } - - recategorizeImages($event: MouseEvent, annotations: AnnotationWrapper[], annotationsChanged: EventEmitter) { - const data = { annotations, dossier: this._dossier }; - const { dossierId, fileId } = this._screenStateService; - this._dialogService.openDialog('recategorizeImage', $event, data, (res: { type: string; comment: string }) => { - annotations.forEach(annotation => { - this._processObsAndEmit( - this._manualRedactionService.recategorizeImg(annotation.annotationId, dossierId, fileId, res.type, res.comment), - annotation, - annotationsChanged, - ); - }); - }); - } - - undoDirectAction($event: MouseEvent, annotations: AnnotationWrapper[], annotationsChanged: EventEmitter) { - $event?.stopPropagation(); - - const { dossierId, fileId } = this._screenStateService; - annotations.forEach(annotation => { + const body = annotations.map(annotation => ({ + annotationId: annotation.id, + removeFromDictionary, + comment: result.comment, + })); this._processObsAndEmit( - this._manualRedactionService.undoRequest(annotation, dossierId, fileId), - annotation, + this._manualRedactionService.removeOrSuggestRemoveAnnotation(body, dossierId, fileId, removeFromDictionary), + annotations, annotationsChanged, ); }); } + recategorizeImages($event: MouseEvent, annotations: AnnotationWrapper[], annotationsChanged: EventEmitter) { + const data = { annotations, dossier: this._dossier }; + const { dossierId, fileId } = this._state; + this._dialogService.openDialog('recategorizeImage', $event, data, ({ comment, type }: { type: string; comment: string }) => { + const body: List = annotations.map(({ annotationId }) => ({ + annotationId, + type, + comment, + })); + this._processObsAndEmit( + this._manualRedactionService.recategorizeImage(body, dossierId, fileId), + annotations, + annotationsChanged, + ); + }); + } + + undoDirectAction($event: MouseEvent, annotations: AnnotationWrapper[], annotationsChanged: EventEmitter) { + $event?.stopPropagation(); + + const { dossierId, fileId } = this._state; + const modifyDictionary = annotations[0].isModifyDictionary; + this._processObsAndEmit( + this._manualRedactionService.undoRequest( + annotations.map(a => a.id), + dossierId, + fileId, + modifyDictionary, + ), + annotations, + annotationsChanged, + ); + } + convertRecommendationToAnnotation( $event: any, recommendations: AnnotationWrapper[], - annotationsChanged: EventEmitter, + annotationsChanged: EventEmitter, ) { $event?.stopPropagation(); - const { dossierId, fileId } = this._screenStateService; + const { dossierId, fileId } = this._state; const dialogRef = this._dialog.open( AcceptRecommendationDialogComponent, { ...defaultDialogConfig, autoFocus: true, data: { annotations: recommendations, dossierId } }, @@ -203,20 +208,18 @@ export class AnnotationActionsService { const dialogClosed = dialogRef.afterClosed().pipe(filter(value => !!value && !!value.annotations)); dialogClosed.subscribe(({ annotations, comment: commentText }) => { const comment = commentText ? { text: commentText } : undefined; - annotations.forEach(annotation => { - this._processObsAndEmit( - this._manualRedactionService.addRecommendation(annotation, dossierId, fileId, comment), - annotation, - annotationsChanged, - ); - }); + this._processObsAndEmit( + this._manualRedactionService.addRecommendation(annotations, dossierId, fileId, comment), + annotations, + annotationsChanged, + ); }); } getViewerAvailableActions( dossier: Dossier, annotations: AnnotationWrapper[], - annotationsChanged: EventEmitter, + annotationsChanged: EventEmitter, ): Record[] { const availableActions = []; const annotationPermissions = annotations.map(annotation => ({ @@ -428,9 +431,9 @@ export class AnnotationActionsService { this._annotationDrawService.annotationToQuads(viewerAnnotation); } - acceptResize($event: MouseEvent, annotationWrapper: AnnotationWrapper, annotationsChanged?: EventEmitter) { + acceptResize($event: MouseEvent, annotationWrapper: AnnotationWrapper, annotationsChanged?: EventEmitter) { const data = { dossier: this._dossier }; - const fileId = this._screenStateService.fileId; + const fileId = this._state.fileId; this._dialogService.openDialog('resizeAnnotation', $event, data, async (result: { comment: string }) => { const textAndPositions = await this._extractTextAndPositions(annotationWrapper.id); const text = @@ -444,14 +447,14 @@ export class AnnotationActionsService { }; this._processObsAndEmit( - this._manualRedactionService.resizeOrSuggestToResize(annotationWrapper, data.dossier.dossierId, fileId, resizeRequest), - annotationWrapper, + this._manualRedactionService.resizeOrSuggestToResize([resizeRequest], data.dossier.dossierId, fileId), + [annotationWrapper], annotationsChanged, ); }); } - async cancelResize($event: MouseEvent, annotationWrapper: AnnotationWrapper, annotationsChanged: EventEmitter) { + async cancelResize($event: MouseEvent, annotationWrapper: AnnotationWrapper, annotationsChanged: EventEmitter) { $event?.stopPropagation(); annotationWrapper.resizing = false; @@ -461,17 +464,36 @@ export class AnnotationActionsService { this._pdf.deleteAnnotations([viewerAnnotation.Id]); await this._annotationDrawService.drawAnnotations([annotationWrapper]); this._pdf.annotationManager.deselectAllAnnotations(); - annotationsChanged.emit(annotationWrapper); + annotationsChanged.emit([annotationWrapper]); + } + + markAsFalsePositive($event: MouseEvent, annotations: AnnotationWrapper[], annotationsChanged: EventEmitter) { + $event?.stopPropagation(); + + const requests: List = annotations.map(annotation => ({ + reason: annotation.id, + value: this._getFalsePositiveText(annotation), + type: annotation.type, + positions: annotation.positions, + addToDictionary: true, + comment: { text: 'False Positive' }, + dictionaryEntryType: annotation.isRecommendation + ? DictionaryEntryTypes.FALSE_RECOMMENDATION + : DictionaryEntryTypes.FALSE_POSITIVE, + })); + const { dossierId, fileId } = this._state; + + this._processObsAndEmit(this._manualRedactionService.addAnnotation(requests, dossierId, fileId), annotations, annotationsChanged); } private _processObsAndEmit( obs: Observable, - annotation: AnnotationWrapper, - annotationsChanged: EventEmitter, + annotations: AnnotationWrapper[], + annotationsChanged: EventEmitter, ) { obs.subscribe({ next: () => { - annotationsChanged.emit(annotation); + annotationsChanged.emit(annotations); }, error: () => { annotationsChanged.emit(); @@ -495,34 +517,6 @@ export class AnnotationActionsService { } } - private _markAsFalsePositive( - $event: MouseEvent, - annotation: AnnotationWrapper, - text: string, - annotationsChanged: EventEmitter, - ) { - $event?.stopPropagation(); - - const falsePositiveRequest: IAddRedactionRequest = { - reason: annotation.id, - value: text, - type: annotation.type, - positions: annotation.positions, - addToDictionary: true, - comment: { text: 'False Positive' }, - dictionaryEntryType: annotation.isRecommendation - ? DictionaryEntryTypes.FALSE_RECOMMENDATION - : DictionaryEntryTypes.FALSE_POSITIVE, - }; - const { dossierId, fileId } = this._screenStateService; - - this._processObsAndEmit( - this._manualRedactionService.addAnnotation(falsePositiveRequest, dossierId, fileId), - annotation, - annotationsChanged, - ); - } - private _convertPath(path: string): string { return this._baseHref + path; } diff --git a/apps/red-ui/src/app/modules/file-preview/services/manual-redaction.service.ts b/apps/red-ui/src/app/modules/file-preview/services/manual-redaction.service.ts index ea253885f..0b8a2df61 100644 --- a/apps/red-ui/src/app/modules/file-preview/services/manual-redaction.service.ts +++ b/apps/red-ui/src/app/modules/file-preview/services/manual-redaction.service.ts @@ -1,44 +1,32 @@ import { Injectable, Injector } from '@angular/core'; import type { - AnnotationActionMode, DictionaryActions, Dossier, IAddRedactionRequest, - IApproveRequest, - IImageRecategorizationRequest, ILegalBasisChangeRequest, IManualAddResponse, + IRecategorizationRequest, IRemoveRedactionRequest, IResizeRequest, ManualRedactionActions, } from '@red/domain'; import { type AnnotationWrapper } from '../../../models/file/annotation.wrapper'; -import { GenericService, RequiredParam, Toaster, Validate } from '@iqser/common-ui'; +import { GenericService, List, RequiredParam, Toaster, Validate } from '@iqser/common-ui'; import { map, tap } from 'rxjs/operators'; import { PermissionsService } from '../../../services/permissions.service'; -import { - annotationActionsTranslations, - dictionaryActionsTranslations, - manualRedactionActionsTranslations, -} from '../../../translations/annotation-actions-translations'; +import { dictionaryActionsTranslations, manualRedactionActionsTranslations } from '../../../translations/annotation-actions-translations'; import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; import { HttpErrorResponse, HttpStatusCode } from '@angular/common/http'; import { ActiveDossiersService } from '../../../services/dossiers/active-dossiers.service'; -import { type Observable } from 'rxjs'; import { DictionariesMapService } from '../../../services/entity-services/dictionaries-map.service'; import { type ManualRedactionEntryType } from '../../../models/file/manual-redaction-entry.wrapper'; - -function getMessage(mode: AnnotationActionMode, modifyDictionary?: boolean, error = false, isConflict = false) { - const type = modifyDictionary ? 'dictionary' : 'manual-redaction'; - const resultType = error ? (isConflict ? 'conflictError' : 'error') : 'success'; - return annotationActionsTranslations[type][mode][resultType]; -} +import { NGXLogger } from 'ngx-logger'; function getResponseType(error: boolean, isConflict: boolean) { return error ? (isConflict ? 'conflictError' : 'error') : 'success'; } -function getDictionaryMessage(action: DictionaryActions, error = false, isConflict = false) { +function getDictionaryMessage(action: DictionaryActions, error = false, isConflict = false): string { return dictionaryActionsTranslations[action][getResponseType(error, isConflict)]; } @@ -48,70 +36,18 @@ function getManualRedactionMessage(action: ManualRedactionActions, error = false @Injectable() export class ManualRedactionService extends GenericService { - readonly request = `${this._defaultModelPath}/request`; - readonly redaction = `${this._defaultModelPath}/redaction`; - CONFIG: { - [key in AnnotationActionMode]: string; - }; + readonly bulkRequest = `${this._defaultModelPath}/bulk/request`; + readonly bulkRedaction = `${this._defaultModelPath}/bulk/redaction`; constructor( private readonly _dictionariesMapService: DictionariesMapService, private readonly _toaster: Toaster, + private readonly _logger: NGXLogger, private readonly _permissionsService: PermissionsService, private readonly _activeDossiersService: ActiveDossiersService, protected readonly _injector: Injector, ) { super(_injector, 'manualRedaction'); - 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', - resize: 'resize', - 'request-resize': 'requestResize', - }; - } - - _makeRequest( - mode: AnnotationActionMode, - dossierId: string, - fileId: string, - body: any, - secondParam: any = null, - modifyDictionary = false, - ): Observable { - const obs = !secondParam - ? this[this.CONFIG[mode]](body, dossierId, fileId) - : this[this.CONFIG[mode]](body, secondParam, dossierId, fileId); - - return obs.pipe( - tap({ - next: () => this._toaster.success(getMessage(mode, modifyDictionary), { positionClass: 'toast-file-preview' }), - error: (error: HttpErrorResponse) => { - const isConflict = error.status === HttpStatusCode.Conflict; - this._toaster.error(getMessage(mode, modifyDictionary, true, isConflict), { - error, - params: { - dictionaryName: this._dictionariesMapService.getDictionary( - body.type as string, - this.#dossier(dossierId).dossierTemplateId, - ).label, - content: body.value, - }, - positionClass: 'toast-file-preview', - }); - }, - }), - ); } @Validate() @@ -126,124 +62,93 @@ export class ManualRedactionService extends GenericService { return super.delete({}, url); } - addRecommendation(annotation: AnnotationWrapper, dossierId: string, fileId: string, comment = { text: 'Accepted Recommendation' }) { - const manualRedactionEntry: IAddRedactionRequest = { - addToDictionary: true, - // set the ID as reason, so we can hide the suggestion - reason: annotation.annotationId, - value: annotation.value, - positions: annotation.positions, - type: annotation.recommendationType, - comment: comment, - }; - return this.addAnnotation(manualRedactionEntry, dossierId, fileId); - } - - changeLegalBasis( - annotationId: string, - dossierId: string, - fileId: string, - section: string, - value: string, - legalBasis: string, - comment?: string, - ) { - const mode: AnnotationActionMode = this._permissionsService.isApprover(this.#dossier(dossierId)) - ? 'change-legal-basis' - : 'request-change-legal-basis'; - return this._makeRequest(mode, dossierId, fileId, { annotationId, legalBasis, comment, section, value }); - } - - recategorizeImg(annotationId: string, dossierId: string, fileId: string, type: string, comment: string) { - const mode: AnnotationActionMode = this._permissionsService.isApprover(this.#dossier(dossierId)) - ? 'recategorize-image' - : 'request-image-recategorization'; - return this._makeRequest(mode, dossierId, fileId, { annotationId, type, comment }); - } - - addAnnotation(manualRedactionEntry: IAddRedactionRequest, dossierId: string, fileId: string) { - const mode: AnnotationActionMode = this._permissionsService.isApprover(this.#dossier(dossierId)) ? 'add' : 'suggest'; - return this._makeRequest(mode, dossierId, fileId, manualRedactionEntry, null, manualRedactionEntry.addToDictionary); - } - - force(request: ILegalBasisChangeRequest, dossierId: string, fileId: string) { - const mode: AnnotationActionMode = this._permissionsService.isApprover(this.#dossier(dossierId)) - ? 'force-redaction' - : 'request-force-redaction'; - return this._makeRequest(mode, dossierId, fileId, request); - } - - _force(request: ILegalBasisChangeRequest, dossierId: string, fileId: string) { - const mode: AnnotationActionMode = this._permissionsService.isApprover(this.#dossier(dossierId)) - ? 'force-redaction' - : 'request-force-redaction'; - return this._makeRequest(mode, dossierId, fileId, request); - } - - _requestForce(request: ILegalBasisChangeRequest, dossierId: string, fileId: string) { - const mode: AnnotationActionMode = this._permissionsService.isApprover(this.#dossier(dossierId)) - ? 'force-redaction' - : 'request-force-redaction'; - return this._makeRequest(mode, dossierId, fileId, request); - } - - approve(annotationId: string, dossierId: string, fileId: string, addToDictionary: boolean = false) { - // for only here - approve the request - return this._makeRequest( - 'approve', - dossierId, - fileId, - { addOrRemoveFromDictionary: addToDictionary }, - annotationId, - addToDictionary, + addRecommendation(annotations: AnnotationWrapper[], dossierId: string, fileId: string, comment = { text: 'Accepted Recommendation' }) { + const recommendations = annotations.map( + annotation => + ({ + addToDictionary: true, + // set the ID as reason, so we can hide the suggestion + reason: annotation.annotationId, + value: annotation.value, + positions: annotation.positions, + type: annotation.recommendationType, + comment: comment, + } as IAddRedactionRequest), ); + return this.addAnnotation(recommendations, dossierId, fileId); } - undoRequest(annotationWrapper: AnnotationWrapper, dossierId: string, fileId: string) { - return this._makeRequest('undo', dossierId, fileId, annotationWrapper.id, null, annotationWrapper.isModifyDictionary); + changeLegalBasis(body: List, dossierId: string, fileId: string) { + if (this._permissionsService.isApprover(this.#dossier(dossierId))) { + return this.bulkLegalBasisChange(body, dossierId, fileId).pipe(this.#showToast('change-legal-basis')); + } + + return this.bulkRequestLegalBasisChange(body, dossierId, fileId).pipe(this.#showToast('request-change-legal-basis')); } - declineOrRemoveRequest(annotationWrapper: AnnotationWrapper, dossierId: string, fileId: string) { - const mode: AnnotationActionMode = this._permissionsService.isApprover(this.#dossier(dossierId)) ? 'decline' : 'undo'; - return this._makeRequest(mode, dossierId, fileId, annotationWrapper.id, null, annotationWrapper.isModifyDictionary); + recategorizeImage(body: List, dossierId: string, fileId: string) { + if (this._permissionsService.isApprover(this.#dossier(dossierId))) { + return this.recategorize(body, dossierId, fileId).pipe(this.#showToast('recategorize-image')); + } + + return this.bulkRequestImageRecategorization(body, dossierId, fileId).pipe(this.#showToast('request-image-recategorization')); } - resizeOrSuggestToResize(annotationWrapper: AnnotationWrapper, dossierId: string, fileId: string, resizeRequest: IResizeRequest) { - const mode: AnnotationActionMode = this._permissionsService.isApprover(this.#dossier(dossierId)) ? 'resize' : 'request-resize'; - return this._makeRequest(mode, dossierId, fileId, resizeRequest); + addAnnotation(requests: List, dossierId: string, fileId: string) { + const toast = requests[0].addToDictionary ? this.#showAddToDictionaryToast(requests, dossierId) : this.#showToast('add'); + if (this._permissionsService.isApprover(this.#dossier(dossierId))) { + return this.add(requests, dossierId, fileId).pipe(toast); + } + + return this.bulkRequestAddRedaction(requests, dossierId, fileId).pipe(toast); + } + + bulkForce(requests: List, dossierId: string, fileId: string) { + if (this._permissionsService.isApprover(this.#dossier(dossierId))) { + return this.bulkForceRedaction(requests, dossierId, fileId).pipe(this.#showToast('force-redaction')); + } + + return this.bulkForceRequest(requests, dossierId, fileId).pipe(this.#showToast('request-force-redaction')); + } + + approve(annotationIds: List, dossierId: string, fileId: string) { + return this.bulkApprove(annotationIds, dossierId, fileId).pipe(this.#showToast('approve')); + } + + undoRequest(annotationIds: List, dossierId: string, fileId: string, modifyDictionary = false) { + const toast = modifyDictionary ? this.#showDictionaryToast : this.#showToast; + return this.bulkUndo(annotationIds, dossierId, fileId).pipe(toast('undo')); + } + + declineOrRemoveRequest(annotationIds: List, dossierId: string, fileId: string, modifyDictionary = false) { + const toast = modifyDictionary ? this.#showDictionaryToast : this.#showToast; + if (this._permissionsService.isApprover(this.#dossier(dossierId))) { + return this.bulkDecline(annotationIds, dossierId, fileId).pipe(toast('decline')); + } + + return this.bulkUndo(annotationIds, dossierId, fileId).pipe(toast('undo')); + } + + resizeOrSuggestToResize(requests: List, dossierId: string, fileId: string) { + if (this._permissionsService.isApprover(this.#dossier(dossierId))) { + return this.bulkResize(requests, dossierId, fileId); + } + + return this.bulkRequestResize(requests, dossierId, fileId); } removeOrSuggestRemoveAnnotation( - annotationWrapper: AnnotationWrapper, + body: List, dossierId: string, fileId: string, - comment: string, removeFromDictionary: boolean = false, ) { - let mode: AnnotationActionMode, - body: any, - removeDict = false; - + const toast = removeFromDictionary ? this.#showDictionaryToast : this.#showToast; if (this._permissionsService.isApprover(this.#dossier(dossierId))) { - // if it was something manual simply decline the existing request - mode = 'remove'; - body = { - annotationId: annotationWrapper.id, - removeFromDictionary, - comment: comment, - }; - removeDict = removeFromDictionary; - } else { - mode = 'request-remove'; - body = { - annotationId: annotationWrapper.id, - removeFromDictionary, - comment: comment, - }; - removeDict = removeFromDictionary; + return this.bulkRemoveRedaction(body, dossierId, fileId).pipe(toast('remove')); } - return this._makeRequest(mode, dossierId, fileId, body, null, removeDict); + return this.bulkRequestRemoveRedaction(body, dossierId, fileId).pipe(toast('request-remove')); } getTitle(type: ManualRedactionEntryType, dossier: Dossier) { @@ -269,127 +174,162 @@ export class ManualRedactionService extends GenericService { } @Validate() - addRedaction(@RequiredParam() body: IAddRedactionRequest, @RequiredParam() dossierId: string, @RequiredParam() fileId: string) { - const url = `${this.redaction}/add/${dossierId}/${fileId}`; - return this._post(body, url); + add(@RequiredParam() body: List, @RequiredParam() dossierId: string, @RequiredParam() fileId: string) { + return this._post(body, `${this.bulkRedaction}/add/${dossierId}/${fileId}`).pipe( + tap(response => { + this._logger.info('[MANUAL-REDACTIONS] Add ', body, response); + }), + ); } @Validate() - recategorizeImage( - @RequiredParam() body: IImageRecategorizationRequest, + recategorize( + @RequiredParam() body: List, @RequiredParam() dossierId: string, @RequiredParam() fileId: string, ) { - const url = `${this.redaction}/recategorize/${dossierId}/${fileId}`; - return this._post(body, url); + return this._post(body, `${this.bulkRedaction}/recategorize/${dossierId}/${fileId}`); + // .pipe( + // tap(response => { + // this._logger.info('[MANUAL-REDACTIONS] Recategorize', body, response); + // }), + // ); } @Validate() - requestImageRecategorization( - @RequiredParam() body: IImageRecategorizationRequest, + bulkRequestImageRecategorization( + @RequiredParam() body: List, @RequiredParam() dossierId: string, @RequiredParam() fileId: string, ) { - const url = `${this.request}/recategorize/${dossierId}/${fileId}`; - return this._post(body, url); + return this._post(body, `${this.bulkRequest}/recategorize/${dossierId}/${fileId}`); } @Validate() - legalBasisChange(@RequiredParam() body: ILegalBasisChangeRequest, @RequiredParam() dossierId: string, @RequiredParam() fileId: string) { - const url = `${this.redaction}/legalBasisChange/${dossierId}/${fileId}`; - return this._post(body, url); - } - - @Validate() - requestLegalBasisChange( - @RequiredParam() body: ILegalBasisChangeRequest, + bulkLegalBasisChange( + @RequiredParam() body: List, @RequiredParam() dossierId: string, @RequiredParam() fileId: string, ) { - const url = `${this.request}/legalBasis/${dossierId}/${fileId}`; - return this._post(body, url); + return this._post(body, `${this.bulkRedaction}/legalBasisChange/${dossierId}/${fileId}`); } @Validate() - requestRemoveRedaction( - @RequiredParam() body: IRemoveRedactionRequest, + bulkRequestLegalBasisChange( + @RequiredParam() body: List, @RequiredParam() dossierId: string, @RequiredParam() fileId: string, ) { - const url = `${this.request}/remove/${dossierId}/${fileId}`; - return this._post(body, url); + return this._post(body, `${this.bulkRequest}/legalBasis/${dossierId}/${fileId}`); } @Validate() - approveRequest( - @RequiredParam() body: IApproveRequest, - @RequiredParam() annotationId: string, + bulkRequestRemoveRedaction( + @RequiredParam() body: List, @RequiredParam() dossierId: string, @RequiredParam() fileId: string, ) { - const url = `${this._defaultModelPath}/approve/${dossierId}/${fileId}/${annotationId}`; - return this._post(body, url); + return this._post(body, `${this.bulkRequest}/remove/${dossierId}/${fileId}`); } @Validate() - declineRequest(@RequiredParam() annotationId: string, @RequiredParam() dossierId: string, @RequiredParam() fileId: string) { - const url = `${this._defaultModelPath}/decline/${dossierId}/${fileId}/${annotationId}`; - return this._post({}, url); + bulkApprove(@RequiredParam() annotationIds: List, @RequiredParam() dossierId: string, @RequiredParam() fileId: string) { + return this._post(annotationIds, `${this._defaultModelPath}/bulk/approve/${dossierId}/${fileId}`); } @Validate() - undo(@RequiredParam() annotationId: string, @RequiredParam() dossierId: string, @RequiredParam() fileId: string) { - const url = `${this._defaultModelPath}/undo/${dossierId}/${fileId}/${annotationId}`; - return super.delete({}, url); + bulkDecline(@RequiredParam() annotationIds: List, @RequiredParam() dossierId: string, @RequiredParam() fileId: string) { + return this._post(annotationIds, `${this._defaultModelPath}/bulk/decline/${dossierId}/${fileId}`); } @Validate() - removeRedaction(@RequiredParam() body: IRemoveRedactionRequest, @RequiredParam() dossierId: string, @RequiredParam() fileId: string) { - return this._post(body, `${this.redaction}/remove/${dossierId}/${fileId}`); + bulkUndo(@RequiredParam() annotationIds: List, @RequiredParam() dossierId: string, @RequiredParam() fileId: string) { + return super.delete(annotationIds, `${this._defaultModelPath}/bulk/undo/${dossierId}/${fileId}`); } @Validate() - requestAddRedaction(@RequiredParam() body: IAddRedactionRequest, @RequiredParam() dossierId: string, @RequiredParam() fileId: string) { - return this._post(body, `${this.request}/add/${dossierId}/${fileId}`); - } - - @Validate() - forceRedaction(@RequiredParam() body: ILegalBasisChangeRequest, @RequiredParam() dossierId: string, @RequiredParam() fileId: string) { - return this._post(body, `${this.redaction}/force/${dossierId}/${fileId}`); - } - - @Validate() - requestForceRedaction( - @RequiredParam() body: ILegalBasisChangeRequest, + bulkRemoveRedaction( + @RequiredParam() body: List, @RequiredParam() dossierId: string, @RequiredParam() fileId: string, ) { - return this._post(body, `${this.request}/force/${dossierId}/${fileId}`); + return this._post(body, `${this.bulkRedaction}/remove/${dossierId}/${fileId}`); } @Validate() - resize(@RequiredParam() body: IResizeRequest, @RequiredParam() dossierId: string, @RequiredParam() fileId: string) { - return this._post(body, `${this.redaction}/resize/${dossierId}/${fileId}`); + bulkRequestAddRedaction( + @RequiredParam() body: List, + @RequiredParam() dossierId: string, + @RequiredParam() fileId: string, + ) { + return this._post(body, `${this.bulkRequest}/add/${dossierId}/${fileId}`); } @Validate() - requestResize(@RequiredParam() body: IResizeRequest, @RequiredParam() dossierId: string, @RequiredParam() fileId: string) { - return this._post(body, `${this.request}/resize/${dossierId}/${fileId}`); + bulkForceRedaction( + @RequiredParam() body: List, + @RequiredParam() dossierId: string, + @RequiredParam() fileId: string, + ) { + return this._post(body, `${this.bulkRedaction}/force/${dossierId}/${fileId}`); } - #showToast(mode: AnnotationActionMode, body, dossierId: string, modifyDictionary = false) { + @Validate() + bulkForceRequest( + @RequiredParam() body: List, + @RequiredParam() dossierId: string, + @RequiredParam() fileId: string, + ) { + return this._post(body, `${this.bulkRequest}/force/${dossierId}/${fileId}`); + } + + @Validate() + bulkResize(@RequiredParam() body: List, @RequiredParam() dossierId: string, @RequiredParam() fileId: string) { + return this._post(body, `${this.bulkRedaction}/resize/${dossierId}/${fileId}`); + } + + @Validate() + bulkRequestResize(@RequiredParam() body: List, @RequiredParam() dossierId: string, @RequiredParam() fileId: string) { + return this._post(body, `${this.bulkRequest}/resize/${dossierId}/${fileId}`); + } + + #showToast(action: ManualRedactionActions) { return tap({ - next: () => this._toaster.success(getMessage(mode, modifyDictionary), { positionClass: 'toast-file-preview' }), + next: () => this._toaster.success(getManualRedactionMessage(action), { positionClass: 'toast-file-preview' }), error: (error: HttpErrorResponse) => { const isConflict = error.status === HttpStatusCode.Conflict; - this._toaster.error(getMessage(mode, modifyDictionary, true, isConflict), { + this._toaster.error(getManualRedactionMessage(action, true, isConflict), { + error, + positionClass: 'toast-file-preview', + }); + }, + }); + } + + #showDictionaryToast(action: DictionaryActions) { + return tap({ + next: () => this._toaster.success(getDictionaryMessage(action), { positionClass: 'toast-file-preview' }), + error: (error: HttpErrorResponse) => { + const isConflict = error.status === HttpStatusCode.Conflict; + this._toaster.error(getDictionaryMessage(action, true, isConflict), { + error, + positionClass: 'toast-file-preview', + }); + }, + }); + } + + #showAddToDictionaryToast(body: List, dossierId: string) { + return tap({ + next: () => this._toaster.success(getDictionaryMessage('add'), { positionClass: 'toast-file-preview' }), + error: (error: HttpErrorResponse) => { + const isConflict = error.status === HttpStatusCode.Conflict; + this._toaster.error(getDictionaryMessage('add', true, isConflict), { error, params: { - dictionaryName: this._dictionariesMapService.getDictionary( - body.type as string, - this.#dossier(dossierId).dossierTemplateId, - ).label, - content: body.value, + dictionaryName: this._dictionariesMapService.getDictionary(body[0].type, this.#dossier(dossierId).dossierTemplateId) + .label, + content: body[0].value, }, positionClass: 'toast-file-preview', }); diff --git a/apps/red-ui/src/app/translations/annotation-actions-translations.ts b/apps/red-ui/src/app/translations/annotation-actions-translations.ts index b8287e31b..82e9eb6ac 100644 --- a/apps/red-ui/src/app/translations/annotation-actions-translations.ts +++ b/apps/red-ui/src/app/translations/annotation-actions-translations.ts @@ -1,4 +1,4 @@ -import { AnnotationActionMode, DictionaryActions, ManualRedactionActions } from '@red/domain'; +import { DictionaryActions, ManualRedactionActions } from '@red/domain'; import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; interface AnnotationActionResponses { @@ -7,99 +7,6 @@ interface AnnotationActionResponses { conflictError?: string; } -type ActionType = { [key in AnnotationActionMode]?: AnnotationActionResponses }; - -export const annotationActionsTranslations: { - dictionary: ActionType; - 'manual-redaction': ActionType; -} = { - dictionary: { - add: { - error: _('annotation-actions.message.dictionary.add.error'), - conflictError: _('annotation-actions.message.dictionary.add.conflict-error'), - success: _('annotation-actions.message.dictionary.add.success'), - }, - approve: { - error: _('annotation-actions.message.dictionary.approve.error'), - success: _('annotation-actions.message.dictionary.approve.success'), - }, - decline: { - error: _('annotation-actions.message.dictionary.decline.error'), - success: _('annotation-actions.message.dictionary.decline.success'), - }, - remove: { - error: _('annotation-actions.message.dictionary.remove.error'), - success: _('annotation-actions.message.dictionary.remove.success'), - }, - 'request-remove': { - error: _('annotation-actions.message.dictionary.request-remove.error'), - success: _('annotation-actions.message.dictionary.request-remove.success'), - }, - suggest: { - error: _('annotation-actions.message.dictionary.suggest.error'), - success: _('annotation-actions.message.dictionary.suggest.success'), - }, - undo: { - error: _('annotation-actions.message.dictionary.undo.error'), - success: _('annotation-actions.message.dictionary.undo.success'), - }, - }, - 'manual-redaction': { - add: { - error: _('annotation-actions.message.manual-redaction.add.error'), - success: _('annotation-actions.message.manual-redaction.add.success'), - }, - approve: { - error: _('annotation-actions.message.manual-redaction.approve.error'), - success: _('annotation-actions.message.manual-redaction.approve.success'), - }, - 'change-legal-basis': { - error: _('annotation-actions.message.manual-redaction.change-legal-basis.error'), - success: _('annotation-actions.message.manual-redaction.change-legal-basis.success'), - }, - decline: { - error: _('annotation-actions.message.manual-redaction.decline.error'), - success: _('annotation-actions.message.manual-redaction.decline.success'), - }, - 'force-redaction': { - error: _('annotation-actions.message.manual-redaction.force-redaction.error'), - success: _('annotation-actions.message.manual-redaction.force-redaction.success'), - }, - 'recategorize-image': { - error: _('annotation-actions.message.manual-redaction.recategorize-image.error'), - success: _('annotation-actions.message.manual-redaction.recategorize-image.success'), - }, - 'request-change-legal-basis': { - error: _('annotation-actions.message.manual-redaction.request-change-legal-basis.error'), - success: _('annotation-actions.message.manual-redaction.request-change-legal-basis.success'), - }, - 'request-force-redaction': { - error: _('annotation-actions.message.manual-redaction.request-force-redaction.error'), - success: _('annotation-actions.message.manual-redaction.request-force-redaction.success'), - }, - 'request-image-recategorization': { - error: _('annotation-actions.message.manual-redaction.request-image-recategorization.error'), - success: _('annotation-actions.message.manual-redaction.request-image-recategorization.success'), - }, - suggest: { - error: _('annotation-actions.message.manual-redaction.suggest.error'), - success: _('annotation-actions.message.manual-redaction.suggest.success'), - }, - undo: { - error: _('annotation-actions.message.manual-redaction.undo.error'), - success: _('annotation-actions.message.manual-redaction.undo.success'), - }, - remove: { - error: _('annotation-actions.message.manual-redaction.remove.error'), - success: _('annotation-actions.message.manual-redaction.remove.success'), - }, - 'request-remove': { - error: _('annotation-actions.message.manual-redaction.request-remove.error'), - success: _('annotation-actions.message.manual-redaction.request-remove.success'), - }, - }, -}; - export const dictionaryActionsTranslations: Record = { add: { error: _('annotation-actions.message.dictionary.add.error'), diff --git a/libs/red-domain/src/lib/annotations/types.ts b/libs/red-domain/src/lib/annotations/types.ts index d629a24fa..75ae1f6a9 100644 --- a/libs/red-domain/src/lib/annotations/types.ts +++ b/libs/red-domain/src/lib/annotations/types.ts @@ -1,23 +1,7 @@ export type ImageCategory = 'signature' | 'logo' | 'formula' | 'image'; -export type AnnotationActionMode = - | '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' - | 'resize' - | 'request-resize'; - export type DictionaryActions = 'add' | 'approve' | 'remove' | 'decline' | 'request-remove' | 'suggest' | 'undo'; + export type ManualRedactionActions = | 'add' | 'approve' diff --git a/libs/red-domain/src/lib/redaction-log/index.ts b/libs/red-domain/src/lib/redaction-log/index.ts index 746bdbb73..3ae3bf570 100644 --- a/libs/red-domain/src/lib/redaction-log/index.ts +++ b/libs/red-domain/src/lib/redaction-log/index.ts @@ -8,7 +8,7 @@ export * from './redaction-log'; export * from './remove-redaction.request'; export * from './manual-add.response'; export * from './approve-request'; -export * from './image-recategorization.request'; +export * from './recategorization.request'; export * from './resize.request'; export * from './manual-change'; export * from './dictionary-entry-types'; diff --git a/libs/red-domain/src/lib/redaction-log/image-recategorization.request.ts b/libs/red-domain/src/lib/redaction-log/recategorization.request.ts similarity index 66% rename from libs/red-domain/src/lib/redaction-log/image-recategorization.request.ts rename to libs/red-domain/src/lib/redaction-log/recategorization.request.ts index f6601b675..6c374d977 100644 --- a/libs/red-domain/src/lib/redaction-log/image-recategorization.request.ts +++ b/libs/red-domain/src/lib/redaction-log/recategorization.request.ts @@ -1,4 +1,4 @@ -export interface IImageRecategorizationRequest { +export interface IRecategorizationRequest { readonly annotationId?: string; readonly comment?: string; readonly type?: string;