diff --git a/apps/red-ui/src/app/modules/file-preview/dialogs/docu-mine/remove-annotation-dialog/remove-annotation-dialog.component.ts b/apps/red-ui/src/app/modules/file-preview/dialogs/docu-mine/remove-annotation-dialog/remove-annotation-dialog.component.ts index 6571d4ecc..11c80ccc4 100644 --- a/apps/red-ui/src/app/modules/file-preview/dialogs/docu-mine/remove-annotation-dialog/remove-annotation-dialog.component.ts +++ b/apps/red-ui/src/app/modules/file-preview/dialogs/docu-mine/remove-annotation-dialog/remove-annotation-dialog.component.ts @@ -46,12 +46,13 @@ export class RemoveAnnotationDialogComponent extends IqserDialogComponent< readonly iconButtonTypes = IconButtonTypes; readonly options: DetailsRadioOption[]; readonly redactedTexts: string[]; + readonly isImage = this.data.redactions.reduce((acc, next) => acc && next.isImage, true); form!: UntypedFormGroup; constructor(private readonly _formBuilder: FormBuilder) { super(); - this.options = getRemoveRedactionOptions(this.data, this.data.applyToAllDossiers, true); + this.options = getRemoveRedactionOptions(this.data, this.data.applyToAllDossiers, this.isImage, true); this.redactedTexts = this.data.redactions.map(annotation => annotation.value); this.form = this.#getForm(); } diff --git a/apps/red-ui/src/app/modules/file-preview/dialogs/force-redaction-dialog/force-annotation-dialog.component.ts b/apps/red-ui/src/app/modules/file-preview/dialogs/force-redaction-dialog/force-annotation-dialog.component.ts index a6eab119f..7b87874ef 100644 --- a/apps/red-ui/src/app/modules/file-preview/dialogs/force-redaction-dialog/force-annotation-dialog.component.ts +++ b/apps/red-ui/src/app/modules/file-preview/dialogs/force-redaction-dialog/force-annotation-dialog.component.ts @@ -1,14 +1,13 @@ -import { Component, Inject, OnInit } from '@angular/core'; -import { ReactiveFormsModule, UntypedFormGroup, Validators } from '@angular/forms'; -import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { Component, OnInit } from '@angular/core'; +import { FormBuilder, FormGroup, ReactiveFormsModule, UntypedFormGroup, Validators } from '@angular/forms'; import { - BaseDialogComponent, CircleButtonComponent, getConfig, HasScrollbarDirective, HelpButtonComponent, IconButtonComponent, IqserDenyDirective, + IqserDialogComponent, } from '@iqser/common-ui'; import { JustificationsService } from '@services/entity-services/justifications.service'; import { Dossier, ILegalBasisChangeRequest } from '@red/domain'; @@ -21,20 +20,19 @@ import { ValueColumn, } from '../../components/selected-annotations-table/selected-annotations-table.component'; import { NgForOf, NgIf } from '@angular/common'; -import { MatFormField } from '@angular/material/form-field'; import { MatOption, MatSelect, MatSelectTrigger } from '@angular/material/select'; +import { DetailsRadioOption } from '@common-ui/inputs/details-radio/details-radio-option'; +import { ForceAnnotationData, ForceAnnotationOption, ForceAnnotationResult, LegalBasisOption } from '../../utils/dialog-types'; +import { getForceAnnotationOptions } from '../../utils/dialog-options'; +import { SystemDefaults } from '../../../account/utils/dialog-defaults'; +import { MatFormField } from '@angular/material/form-field'; import { MatTooltip } from '@angular/material/tooltip'; import { TranslateModule } from '@ngx-translate/core'; -import { DetailsRadioOption } from '@common-ui/inputs/details-radio/details-radio-option'; -import { ForceAnnotationOption, LegalBasisOption } from '../../utils/dialog-types'; -import { getForceAnnotationOptions } from '../../utils/dialog-options'; import { DetailsRadioComponent } from '@common-ui/inputs/details-radio/details-radio.component'; -import { SystemDefaults } from '../../../account/utils/dialog-defaults'; const DOCUMINE_LEGAL_BASIS = 'n-a.'; @Component({ - selector: 'redaction-force-annotation-dialog', templateUrl: './force-annotation-dialog.component.html', styleUrls: ['./force-annotation-dialog.component.scss'], standalone: true, @@ -57,12 +55,16 @@ const DOCUMINE_LEGAL_BASIS = 'n-a.'; DetailsRadioComponent, ], }) -export class ForceAnnotationDialogComponent extends BaseDialogComponent implements OnInit { +export class ForceAnnotationDialogComponent + extends IqserDialogComponent + implements OnInit +{ readonly isDocumine = getConfig().IS_DOCUMINE; readonly options: DetailsRadioOption[]; + readonly form: FormGroup; readonly tableColumns: ValueColumn[] = [{ label: 'Value' }, { label: 'Type' }]; - readonly tableData: ValueColumn[][] = this._data.annotations.map(redaction => [ + readonly tableData: ValueColumn[][] = this.data.annotations.map(redaction => [ { label: redaction.value, bold: true }, { label: redaction.typeLabel }, ]); @@ -72,21 +74,23 @@ export class ForceAnnotationDialogComponent extends BaseDialogComponent implemen constructor( private readonly _justificationsService: JustificationsService, - protected readonly _dialogRef: MatDialogRef, - @Inject(MAT_DIALOG_DATA) - private readonly _data: { readonly dossier: Dossier; readonly hint: boolean; annotations: AnnotationWrapper[] }, + private readonly _formBuilder: FormBuilder, ) { - super(_dialogRef); - this.options = getForceAnnotationOptions(this.isDocumine, this.isHintDialog); + super(); + this.options = getForceAnnotationOptions(this.isDocumine, this.isHintDialog, this.isImageDialog); this.form = this.#getForm(); } get isImageHint() { - return this._data.annotations.every(annotation => annotation.IMAGE_HINT); + return this.data.annotations.every(annotation => annotation.IMAGE_HINT); } get isHintDialog() { - return this._data.hint; + return this.data.hint; + } + + get isImageDialog() { + return this.data.image; } get disabled(): boolean { @@ -103,7 +107,7 @@ export class ForceAnnotationDialogComponent extends BaseDialogComponent implemen async ngOnInit() { if (!this.isDocumine) { - const data = await firstValueFrom(this._justificationsService.getForDossierTemplate(this._data.dossier.dossierTemplateId)); + const data = await firstValueFrom(this._justificationsService.getForDossierTemplate(this.data.dossier.dossierTemplateId)); this.legalOptions = data.map(lbm => ({ legalBasis: lbm.reason, @@ -114,8 +118,8 @@ export class ForceAnnotationDialogComponent extends BaseDialogComponent implemen this.legalOptions.sort((a, b) => a.label.localeCompare(b.label)); // Set pre-existing reason if it exists - const existingReason = this.legalOptions.find(option => option.legalBasis === this._data.annotations[0].legalBasis); - if (!this._data.hint && existingReason) { + const existingReason = this.legalOptions.find(option => option.legalBasis === this.data.annotations[0].legalBasis); + if (!this.data.hint && existingReason) { this.form.patchValue({ reason: existingReason }, { emitEvent: false }); } } @@ -123,12 +127,12 @@ export class ForceAnnotationDialogComponent extends BaseDialogComponent implemen } save() { - this._dialogRef.close(this.#createForceRedactionRequest()); + this.close(this.#createForceRedactionRequest()); } #getForm(): UntypedFormGroup { return this._formBuilder.group({ - reason: this._data.hint ? ['Forced Hint'] : [null, !this.isDocumine ? Validators.required : null], + reason: this.data.hint ? ['Forced Hint'] : [null, !this.isDocumine ? Validators.required : null], comment: [null], option: this.options.find(o => o.value === SystemDefaults.FORCE_REDACTION_DEFAULT), }); diff --git a/apps/red-ui/src/app/modules/file-preview/dialogs/remove-redaction-dialog/remove-redaction-dialog.component.ts b/apps/red-ui/src/app/modules/file-preview/dialogs/remove-redaction-dialog/remove-redaction-dialog.component.ts index 25a3a3865..5ec97ffbf 100644 --- a/apps/red-ui/src/app/modules/file-preview/dialogs/remove-redaction-dialog/remove-redaction-dialog.component.ts +++ b/apps/red-ui/src/app/modules/file-preview/dialogs/remove-redaction-dialog/remove-redaction-dialog.component.ts @@ -66,6 +66,7 @@ export class RemoveRedactionDialogComponent extends IqserDialogComponent< readonly recommendation = this.data.redactions.every(redaction => redaction.isRecommendation); readonly hint = this.data.redactions.every(redaction => redaction.isHint); readonly annotationsType = this.hint ? 'hint' : this.recommendation ? 'recommendation' : 'redaction'; + readonly isImage = this.data.redactions.reduce((acc, next) => acc && next.isImage, true); readonly optionByType = { recommendation: { main: this._userPreferences.getRemoveRecommendationDefaultOption(), @@ -107,6 +108,7 @@ export class RemoveRedactionDialogComponent extends IqserDialogComponent< : getRemoveRedactionOptions( this.data, this.isSystemDefault || this.isExtraOptionSystemDefault ? this.#applyToAllDossiers : this.extraOptionPreference, + this.isImage, ); readonly skipped = this.data.redactions.some(annotation => annotation.isSkipped); readonly redactedTexts = this.data.redactions.map(annotation => annotation.value); @@ -185,12 +187,9 @@ export class RemoveRedactionDialogComponent extends IqserDialogComponent< } save(): void { - const optionValue = this.form.controls.option.value.value; - const pageNumbers = parseSelectedPageNumbers( - this.form.get('option').value.additionalInput?.value, - this.data.file, - this.data.redactions[0], - ); + const optionValue = this.form.controls.option?.value?.value; + const optionInputValue = this.form.controls.option?.value?.additionalInput?.value; + const pageNumbers = parseSelectedPageNumbers(optionInputValue, this.data.file, this.data.redactions[0]); const position = parseRectanglePosition(this.data.redactions[0]); this.close({ 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 852c3b9e3..4d5d6890c 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 @@ -7,6 +7,7 @@ import { AnnotationWrapper } from '@models/file/annotation.wrapper'; import { Core } from '@pdftron/webviewer'; import { DictionaryEntryTypes, + DownloadFileTypes, EarmarkOperation, type IBulkLocalRemoveRequest, IBulkRecategorizationRequest, @@ -51,6 +52,7 @@ import { FilePreviewStateService } from './file-preview-state.service'; import { ManualRedactionService } from './manual-redaction.service'; import { SkippedService } from './skipped.service'; import { NON_READABLE_CONTENT } from '../dialogs/rectangle-annotation-dialog/rectangle-annotation-dialog.component'; +import { ForceAnnotationDialogComponent } from '../dialogs/force-redaction-dialog/force-annotation-dialog.component'; @Injectable() export class AnnotationActionsService { @@ -81,34 +83,41 @@ export class AnnotationActionsService { this._dialogService.openDialog('highlightAction', data); } - forceAnnotation(annotations: AnnotationWrapper[], hint: boolean = false) { + async forceAnnotation(annotations: AnnotationWrapper[], hint: boolean = false) { const { dossierId, fileId } = this._state; - const data = { dossier: this._state.dossier(), annotations, hint }; - this._dialogService.openDialog('forceAnnotation', data, (request: ILegalBasisChangeRequest) => { - let obs$: Observable; - if (request.option === ForceAnnotationOptions.ONLY_HERE || hint) { - obs$ = this._manualRedactionService.bulkForce( - annotations.map(a => ({ ...request, annotationId: a.id })), - dossierId, - fileId, - annotations[0].isIgnoredHint, - ); - } else { - const addAnnotationRequest = annotations.map(a => ({ - comment: request.comment, - legalBasis: request.legalBasis, - reason: request.reason, - positions: a.positions, - type: a.type, - value: a.value, - })); - obs$ = this._manualRedactionService.addAnnotation(addAnnotationRequest, dossierId, fileId, { - hint, - bulkLocal: true, - }); - } - this.#processObsAndEmit(obs$).then(); - }); + const image = annotations.every(a => a.isImage); + const data = { dossier: this._state.dossier(), annotations, hint, image }; + + const dialogRef = this._iqserDialog.openDefault(ForceAnnotationDialogComponent, { data }); + const result = await dialogRef.result(); + + if (!result) { + return; + } + + let obs$: Observable; + if (result.option === ForceAnnotationOptions.ONLY_HERE || hint || image) { + obs$ = this._manualRedactionService.bulkForce( + annotations.map(a => ({ ...result, annotationId: a.id })), + dossierId, + fileId, + annotations[0].isIgnoredHint, + ); + } else { + const addAnnotationRequest = annotations.map(a => ({ + comment: result.comment, + legalBasis: result.legalBasis, + reason: result.reason, + positions: a.positions, + type: a.type, + value: a.value, + })); + obs$ = this._manualRedactionService.addAnnotation(addAnnotationRequest, dossierId, fileId, { + hint, + bulkLocal: true, + }); + } + this.#processObsAndEmit(obs$).then(); } async editRedaction(annotations: AnnotationWrapper[]) { @@ -147,15 +156,11 @@ export class AnnotationActionsService { return body; }); } else { - const originTypes = annotations.map(a => a.type); - const originLegalBases = annotations.map(a => a.legalBasis); recategorizeBody = { value: annotations[0].value, type: result.type, legalBasis: result.legalBasis, section: result.section, - originTypes, - originLegalBases, rectangle: annotations[0].AREA, pageNumbers: result.pageNumbers, position: result.position, @@ -206,8 +211,8 @@ export class AnnotationActionsService { } if ( - result.option.value === RemoveRedactionOptions.FALSE_POSITIVE || - result.option.value === RemoveRedactionOptions.DO_NOT_RECOMMEND + result.option?.value === RemoveRedactionOptions.FALSE_POSITIVE || + result.option?.value === RemoveRedactionOptions.DO_NOT_RECOMMEND ) { this.#setAsFalsePositive(redactions, result); } else { @@ -476,7 +481,7 @@ export class AnnotationActionsService { } #removeRedaction(redactions: AnnotationWrapper[], dialogResult: RemoveRedactionResult) { - const removeFromDictionary = dialogResult.option.value === RemoveRedactionOptions.IN_DOSSIER; + const removeFromDictionary = dialogResult.option?.value === RemoveRedactionOptions.IN_DOSSIER; const includeUnprocessed = redactions.every(redaction => this.#includeUnprocessed(redaction, true)); const body = this.#getRemoveRedactionBody(redactions, dialogResult); // todo: might not be correct, probably shouldn't get to this point if they are not all the same @@ -615,8 +620,8 @@ export class AnnotationActionsService { annotationId: redaction.id, value: redaction.value, comment: dialogResult.comment, - removeFromDictionary: dialogResult.option.value === RemoveRedactionOptions.IN_DOSSIER, - removeFromAllDossiers: !!dialogResult.option.additionalCheck?.checked || !!dialogResult.applyToAllDossiers, + removeFromDictionary: dialogResult.option?.value === RemoveRedactionOptions.IN_DOSSIER, + removeFromAllDossiers: !!dialogResult.option?.additionalCheck?.checked || !!dialogResult.applyToAllDossiers, })); } } diff --git a/apps/red-ui/src/app/modules/file-preview/services/file-preview-dialog.service.ts b/apps/red-ui/src/app/modules/file-preview/services/file-preview-dialog.service.ts index 4028b154a..aa961c0e9 100644 --- a/apps/red-ui/src/app/modules/file-preview/services/file-preview-dialog.service.ts +++ b/apps/red-ui/src/app/modules/file-preview/services/file-preview-dialog.service.ts @@ -3,10 +3,9 @@ import { MatDialog } from '@angular/material/dialog'; import { ConfirmationDialogComponent, DialogConfig, DialogService } from '@iqser/common-ui'; import { ChangeLegalBasisDialogComponent } from '../dialogs/change-legal-basis-dialog/change-legal-basis-dialog.component'; import { DocumentInfoDialogComponent } from '../dialogs/document-info-dialog/document-info-dialog.component'; -import { ForceAnnotationDialogComponent } from '../dialogs/force-redaction-dialog/force-annotation-dialog.component'; import { HighlightActionDialogComponent } from '../dialogs/highlight-action-dialog/highlight-action-dialog.component'; -type DialogType = 'confirm' | 'documentInfo' | 'changeLegalBasis' | 'forceAnnotation' | 'highlightAction'; +type DialogType = 'confirm' | 'documentInfo' | 'changeLegalBasis' | 'highlightAction'; @Injectable() export class FilePreviewDialogService extends DialogService { @@ -22,9 +21,6 @@ export class FilePreviewDialogService extends DialogService { changeLegalBasis: { component: ChangeLegalBasisDialogComponent, }, - forceAnnotation: { - component: ForceAnnotationDialogComponent, - }, highlightAction: { component: HighlightActionDialogComponent, }, diff --git a/apps/red-ui/src/app/modules/file-preview/utils/dialog-options.ts b/apps/red-ui/src/app/modules/file-preview/utils/dialog-options.ts index a0a680575..9016bce7f 100644 --- a/apps/red-ui/src/app/modules/file-preview/utils/dialog-options.ts +++ b/apps/red-ui/src/app/modules/file-preview/utils/dialog-options.ts @@ -132,22 +132,21 @@ export const getResizeRedactionOptions = ( isApprover: boolean, canResizeInDictionary: boolean, ): DetailsRadioOption[] => { + if (isRss || !canResizeInDictionary) { + return []; + } + const translations = resizeRedactionTranslations; - const options: DetailsRadioOption[] = [ + const dictBasedType = redaction.isModifyDictionary; + + return [ { label: translations.onlyHere.label, description: translations.onlyHere.description, icon: PIN_ICON, value: ResizeOptions.ONLY_HERE, }, - ]; - - if (isRss) { - return options; - } - if (canResizeInDictionary) { - const dictBasedType = redaction.isModifyDictionary; - options.push({ + { label: translations.inDossier.label, description: translations.inDossier.description, descriptionParams: { dossierName: dossier.dossierName }, @@ -160,14 +159,14 @@ export const getResizeRedactionOptions = ( checked: applyToAllDossiers, hidden: !isApprover, }, - }); - } - return options; + }, + ]; }; export const getRemoveRedactionOptions = ( data: RemoveRedactionData, applyToAllDossiers: boolean, + isImage: boolean, isDocumine: boolean = false, ): DetailsRadioOption[] => { const translations = isDocumine ? removeAnnotationTranslations : removeRedactionTranslations; @@ -175,7 +174,7 @@ export const getRemoveRedactionOptions = ( const isBulk = redactions.length > 1; const options: DetailsRadioOption[] = []; - if (permissions.canRemoveOnlyHere) { + if (permissions.canRemoveOnlyHere && !isImage) { options.push({ label: translations.ONLY_HERE.label, description: isBulk ? translations.ONLY_HERE.descriptionBulk : translations.ONLY_HERE.description, @@ -191,9 +190,6 @@ export const getRemoveRedactionOptions = ( options.push({ label: removeRedactionTranslations.IN_DOCUMENT.label, description: removeRedactionTranslations.IN_DOCUMENT.description, - descriptionParams: { - isImage: redactions[0].isImage ? 'image' : redactions[0].typeLabel, - }, icon: DOCUMENT_ICON, value: RemoveRedactionOptions.IN_DOCUMENT, }); @@ -265,8 +261,12 @@ export const getRemoveRedactionOptions = ( return options; }; -export const getForceAnnotationOptions = (isDocumine: boolean, isHint: boolean): DetailsRadioOption[] => { - if (isDocumine || isHint) { +export const getForceAnnotationOptions = ( + isDocumine: boolean, + isHint: boolean, + isImage: boolean, +): DetailsRadioOption[] => { + if (isDocumine || isHint || isImage) { return []; } diff --git a/apps/red-ui/src/app/modules/file-preview/utils/dialog-types.ts b/apps/red-ui/src/app/modules/file-preview/utils/dialog-types.ts index e0f84cba2..c1c5ac931 100644 --- a/apps/red-ui/src/app/modules/file-preview/utils/dialog-types.ts +++ b/apps/red-ui/src/app/modules/file-preview/utils/dialog-types.ts @@ -73,6 +73,21 @@ export interface EditRedactionData { export type AddAnnotationData = RedactTextData; export type AddHintData = RedactTextData; +export interface ForceAnnotationData { + readonly dossier: Dossier; + readonly annotations: AnnotationWrapper[]; + readonly hint: boolean; + readonly image: boolean; +} + +export interface ForceAnnotationResult { + readonly annotationId?: string; + readonly comment?: string; + readonly legalBasis?: string; + readonly reason?: string; + readonly option?: ForceAnnotationOption; +} + export interface RedactTextResult { redaction: IManualRedactionEntry; dictionary: Dictionary; diff --git a/apps/red-ui/src/assets/i18n/redact/en.json b/apps/red-ui/src/assets/i18n/redact/en.json index 8c9615cb3..70ac26653 100644 --- a/apps/red-ui/src/assets/i18n/redact/en.json +++ b/apps/red-ui/src/assets/i18n/redact/en.json @@ -2224,7 +2224,7 @@ "label": "Remove from dossier in this context" }, "in-document": { - "description": "Do not auto-redact the selected {isImage, select, image{image} other{term}} on any page of this document.", + "description": "Do not auto-redact the selected term on any page of this document.", "label": "Remove from document" }, "in-dossier": { diff --git a/libs/red-domain/src/lib/redaction-log/recategorization.request.ts b/libs/red-domain/src/lib/redaction-log/recategorization.request.ts index 9939b140a..57ec25dd7 100644 --- a/libs/red-domain/src/lib/redaction-log/recategorization.request.ts +++ b/libs/red-domain/src/lib/redaction-log/recategorization.request.ts @@ -14,8 +14,8 @@ export interface IBulkRecategorizationRequest { readonly type: string; readonly legalBasis: string; readonly section: string; - readonly originTypes: string[]; - readonly originLegalBases: string[]; + readonly originTypes?: string[]; + readonly originLegalBases?: string[]; readonly rectangle: boolean; readonly position?: IEntityLogEntryPosition; readonly pageNumbers?: number[];