WIP on VM/RED-10256

RED-10256 - Bulk-local: Changes should not be filtered + Remove for image-based redactions
This commit is contained in:
Valentin Mihai 2024-10-28 13:47:29 +02:00
parent d8b20774f4
commit 7dd24698b1
9 changed files with 113 additions and 93 deletions

View File

@ -46,12 +46,13 @@ export class RemoveAnnotationDialogComponent extends IqserDialogComponent<
readonly iconButtonTypes = IconButtonTypes;
readonly options: DetailsRadioOption<RemoveAnnotationOption>[];
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();
}

View File

@ -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<ForceAnnotationDialogComponent, ForceAnnotationData, ForceAnnotationResult>
implements OnInit
{
readonly isDocumine = getConfig().IS_DOCUMINE;
readonly options: DetailsRadioOption<ForceAnnotationOption>[];
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<ForceAnnotationDialogComponent>,
@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),
});

View File

@ -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({

View File

@ -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<unknown>;
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<unknown>;
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,
}));
}
}

View File

@ -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<DialogType> {
@ -22,9 +21,6 @@ export class FilePreviewDialogService extends DialogService<DialogType> {
changeLegalBasis: {
component: ChangeLegalBasisDialogComponent,
},
forceAnnotation: {
component: ForceAnnotationDialogComponent,
},
highlightAction: {
component: HighlightActionDialogComponent,
},

View File

@ -132,22 +132,21 @@ export const getResizeRedactionOptions = (
isApprover: boolean,
canResizeInDictionary: boolean,
): DetailsRadioOption<ResizeRedactionOption>[] => {
if (isRss || !canResizeInDictionary) {
return [];
}
const translations = resizeRedactionTranslations;
const options: DetailsRadioOption<ResizeRedactionOption>[] = [
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<RemoveRedactionOption>[] => {
const translations = isDocumine ? removeAnnotationTranslations : removeRedactionTranslations;
@ -175,7 +174,7 @@ export const getRemoveRedactionOptions = (
const isBulk = redactions.length > 1;
const options: DetailsRadioOption<RemoveRedactionOption>[] = [];
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<ForceAnnotationOption>[] => {
if (isDocumine || isHint) {
export const getForceAnnotationOptions = (
isDocumine: boolean,
isHint: boolean,
isImage: boolean,
): DetailsRadioOption<ForceAnnotationOption>[] => {
if (isDocumine || isHint || isImage) {
return [];
}

View File

@ -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;

View File

@ -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": {

View File

@ -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[];