Merge branch 'RED-7550' into 'master'

Resolve RED-7550

Closes RED-7550

See merge request redactmanager/red-ui!88
This commit is contained in:
Dan Percic 2023-09-18 16:41:23 +02:00
commit c9de81b82f
10 changed files with 140 additions and 60 deletions

View File

@ -16,6 +16,7 @@ import {
canUndo, canUndo,
} from './annotation-permissions.utils'; } from './annotation-permissions.utils';
import { AnnotationWrapper } from './annotation.wrapper'; import { AnnotationWrapper } from './annotation.wrapper';
import { IMAGE_CATEGORIES } from '../../modules/file-preview/utils/constants';
export class AnnotationPermissions { export class AnnotationPermissions {
canUndo = true; canUndo = true;
@ -30,6 +31,8 @@ export class AnnotationPermissions {
canRecategorizeAnnotation = true; canRecategorizeAnnotation = true;
canForceHint = true; canForceHint = true;
canEditAnnotations = true; canEditAnnotations = true;
canEditHints = true;
canEditImages = true;
static forUser( static forUser(
isApprover: boolean, isApprover: boolean,
@ -58,7 +61,11 @@ export class AnnotationPermissions {
permissions.canChangeLegalBasis = canChangeLegalBasis(annotation, canAddRedaction); permissions.canChangeLegalBasis = canChangeLegalBasis(annotation, canAddRedaction);
permissions.canRecategorizeAnnotation = canRecategorizeAnnotation(annotation, canAddRedaction); permissions.canRecategorizeAnnotation = canRecategorizeAnnotation(annotation, canAddRedaction);
permissions.canResizeAnnotation = canResizeAnnotation(annotation, canAddRedaction); permissions.canResizeAnnotation = canResizeAnnotation(annotation, canAddRedaction);
permissions.canEditAnnotations = annotation.isSkipped || annotation.isRedacted;
permissions.canEditAnnotations = (annotation.isSkipped || annotation.isRedacted) && !annotation.isImage;
permissions.canEditHints =
annotation.isIgnoredHint || annotation.isDictBasedHint || annotation.isHint || annotation.isSuggestionForceHint;
permissions.canEditImages = [...IMAGE_CATEGORIES, 'ocr'].includes(annotation.type);
summedPermissions._merge(permissions); summedPermissions._merge(permissions);
} }
@ -79,6 +86,8 @@ export class AnnotationPermissions {
result.canRemoveRedaction = permissions.reduce((acc, next) => acc && next.canRemoveRedaction, true); result.canRemoveRedaction = permissions.reduce((acc, next) => acc && next.canRemoveRedaction, true);
result.canUndo = permissions.reduce((acc, next) => acc && next.canUndo, true); result.canUndo = permissions.reduce((acc, next) => acc && next.canUndo, true);
result.canEditAnnotations = permissions.reduce((acc, next) => acc && next.canEditAnnotations, true); result.canEditAnnotations = permissions.reduce((acc, next) => acc && next.canEditAnnotations, true);
result.canEditHints = permissions.reduce((acc, next) => acc && next.canEditHints, true);
result.canEditImages = permissions.reduce((acc, next) => acc && next.canEditImages, true);
return result; return result;
} }

View File

@ -39,24 +39,35 @@ export class AnnotationActionsComponent implements OnChanges {
}); });
readonly #isDocumine = getConfig().IS_DOCUMINE; readonly #isDocumine = getConfig().IS_DOCUMINE;
constructor(
readonly viewModeService: ViewModeService,
readonly helpModeService: HelpModeService,
readonly multiSelectService: MultiSelectService,
private readonly _state: FilePreviewStateService,
private readonly _permissionsService: PermissionsService,
private readonly _iqserPermissionsService: IqserPermissionsService,
private readonly _annotationManager: REDAnnotationManager,
readonly annotationActionsService: AnnotationActionsService,
readonly annotationReferencesService: AnnotationReferencesService,
) {}
get annotations(): AnnotationWrapper[] { get annotations(): AnnotationWrapper[] {
return this.#annotations; return this.#annotations;
} }
@Input()
set annotations(annotations: AnnotationWrapper[]) {
this.#annotations = annotations.filter(a => a !== undefined);
this.isImage = this.#annotations?.reduce((accumulator, annotation) => annotation.isImage && accumulator, true);
this._annotationId = this.#annotations[0]?.id;
}
get canEdit(): boolean { get canEdit(): boolean {
const documineCanEditRedactions = const canEditRedactions =
this.annotationPermissions.canChangeLegalBasis || this.annotationPermissions.canChangeLegalBasis ||
this.annotationPermissions.canRecategorizeAnnotation || this.annotationPermissions.canRecategorizeAnnotation ||
this.annotationPermissions.canForceHint || this.annotationPermissions.canForceHint ||
this.annotationPermissions.canForceRedaction; this.annotationPermissions.canForceRedaction;
return this.#isDocumine && this.annotations.length > 1 ? this.annotationPermissions.canEditAnnotations : documineCanEditRedactions; return this.annotations.length > 1
? this.#isDocumine
? this.annotationPermissions.canEditAnnotations
: this.annotationPermissions.canEditHints ||
this.annotationPermissions.canEditImages ||
this.annotationPermissions.canEditAnnotations
: canEditRedactions;
} }
get canRemoveRedaction(): boolean { get canRemoveRedaction(): boolean {
@ -76,17 +87,12 @@ export class AnnotationActionsComponent implements OnChanges {
return this.annotations.every(a => a.superType === type); return this.annotations.every(a => a.superType === type);
} }
constructor( @Input()
readonly viewModeService: ViewModeService, set annotations(annotations: AnnotationWrapper[]) {
readonly helpModeService: HelpModeService, this.#annotations = annotations.filter(a => a !== undefined);
readonly multiSelectService: MultiSelectService, this.isImage = this.#annotations?.reduce((accumulator, annotation) => annotation.isImage && accumulator, true);
private readonly _state: FilePreviewStateService, this._annotationId = this.#annotations[0]?.id;
private readonly _permissionsService: PermissionsService, }
private readonly _iqserPermissionsService: IqserPermissionsService,
private readonly _annotationManager: REDAnnotationManager,
readonly annotationActionsService: AnnotationActionsService,
readonly annotationReferencesService: AnnotationReferencesService,
) {}
ngOnChanges(): void { ngOnChanges(): void {
this.#setPermissions(); this.#setPermissions();

View File

@ -7,20 +7,27 @@
></div> ></div>
<div class="dialog-content redaction"> <div class="dialog-content redaction">
<div *ngIf="!allRectangles && redactedText" class="iqser-input-group w-450"> <div *ngIf="!isImage && redactedTexts" class="iqser-input-group">
<label <label
[translateParams]="{ type: isImage ? 'image' : isHint ? 'hint' : 'redaction' }" [translateParams]="{ type: isImage ? 'image' : isHint ? 'hint' : 'redaction', length: redactedTexts.length }"
[translate]="'edit-redaction.dialog.content.redacted-text'" [translate]="'edit-redaction.dialog.content.redacted-text'"
class="selected-text" class="selected-text"
></label> ></label>
{{ redactedText }} <cdk-virtual-scroll-viewport
[itemSize]="16"
[ngStyle]="{ height: redactedTexts.length <= 5 ? 16 * redactedTexts.length + 'px' : 80 + 'px' }"
>
<ul *cdkVirtualFor="let text of redactedTexts">
<li>{{ text }}</li>
</ul>
</cdk-virtual-scroll-viewport>
</div> </div>
<div *ngIf="!isManualRedaction" class="iqser-input-group required w-450"> <div *ngIf="!isManualRedaction" class="iqser-input-group required w-450">
<label [translate]="'edit-redaction.dialog.content.type'"></label> <label [translate]="'edit-redaction.dialog.content.type'"></label>
<mat-form-field> <mat-form-field>
<mat-select formControlName="type"> <mat-select formControlName="type" [placeholder]="'edit-redaction.dialog.content.unchanged' | translate">
<mat-select-trigger>{{ displayedDictionaryLabel }}</mat-select-trigger> <mat-select-trigger>{{ displayedDictionaryLabel }}</mat-select-trigger>
<mat-option <mat-option
(click)="typeChanged()" (click)="typeChanged()"
@ -39,7 +46,11 @@
<div class="iqser-input-group required w-450"> <div class="iqser-input-group required w-450">
<label [translate]="'edit-redaction.dialog.content.reason'"></label> <label [translate]="'edit-redaction.dialog.content.reason'"></label>
<mat-form-field> <mat-form-field>
<mat-select class="full-width" formControlName="reason"> <mat-select
class="full-width"
formControlName="reason"
[placeholder]="'edit-redaction.dialog.content.unchanged' | translate"
>
<mat-option <mat-option
*ngFor="let option of legalOptions" *ngFor="let option of legalOptions"
[matTooltip]="option.description" [matTooltip]="option.description"
@ -59,19 +70,33 @@
<div class="iqser-input-group w-450"> <div class="iqser-input-group w-450">
<label [translate]="'edit-redaction.dialog.content.section'"></label> <label [translate]="'edit-redaction.dialog.content.section'"></label>
<input formControlName="section" name="section" type="text" /> <input
formControlName="section"
name="section"
type="text"
[placeholder]="'edit-redaction.dialog.content.unchanged' | translate"
/>
</div> </div>
</ng-container> </ng-container>
<div *ngIf="allRectangles" class="iqser-input-group w-400"> <div *ngIf="allRectangles" class="iqser-input-group w-400">
<label [translate]="'change-legal-basis-dialog.content.classification'"></label> <label [translate]="'change-legal-basis-dialog.content.classification'"></label>
<input formControlName="value" name="classification" type="text" /> <input
formControlName="value"
name="classification"
type="text"
[placeholder]="'edit-redaction.dialog.content.unchanged' | translate"
/>
</div> </div>
<div class="iqser-input-group w-450"> <div class="iqser-input-group w-450">
<label [translate]="'edit-redaction.dialog.content.comment'"></label> <label [translate]="'edit-redaction.dialog.content.comment'"></label>
<textarea <textarea
[placeholder]="'edit-redaction.dialog.content.comment-placeholder' | translate" [placeholder]="
!isImage && redactedTexts.length === 1
? ('edit-redaction.dialog.content.comment-placeholder' | translate)
: ('edit-redaction.dialog.content.unchanged' | translate)
"
formControlName="comment" formControlName="comment"
iqserHasScrollbar iqserHasScrollbar
name="comment" name="comment"
@ -83,7 +108,7 @@
<div class="dialog-actions"> <div class="dialog-actions">
<iqser-icon-button <iqser-icon-button
[disabled]="showLegalReason && disabled" [disabled]="disabled || !changed"
[label]="'edit-redaction.dialog.actions.save' | translate" [label]="'edit-redaction.dialog.actions.save' | translate"
[submit]="true" [submit]="true"
[type]="iconButtonTypes.primary" [type]="iconButtonTypes.primary"

View File

@ -0,0 +1,22 @@
@use 'common-mixins';
cdk-virtual-scroll-viewport {
margin-top: 8px;
@include common-mixins.scroll-bar;
}
:host ::ng-deep .cdk-virtual-scroll-orientation-vertical .cdk-virtual-scroll-content-wrapper {
max-width: 100% !important;
}
ul {
padding-left: 16px;
}
li {
white-space: nowrap;
text-overflow: ellipsis;
list-style-position: inside;
overflow: hidden;
padding-right: 10px;
}

View File

@ -1,5 +1,5 @@
import { Component, inject, OnInit } from '@angular/core'; import { Component, inject, OnInit } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms'; import { FormBuilder, FormControl } from '@angular/forms';
import { DetailsRadioOption, IconButtonTypes, IqserDialogComponent } from '@iqser/common-ui'; import { DetailsRadioOption, IconButtonTypes, IqserDialogComponent } from '@iqser/common-ui';
import { Dictionary, SuperTypes } from '@red/domain'; import { Dictionary, SuperTypes } from '@red/domain';
import { ActiveDossiersService } from '@services/dossiers/active-dossiers.service'; import { ActiveDossiersService } from '@services/dossiers/active-dossiers.service';
@ -13,6 +13,7 @@ import { LegalBasisOption } from '../manual-redaction-dialog/manual-annotation-d
@Component({ @Component({
templateUrl: './edit-redaction-dialog.component.html', templateUrl: './edit-redaction-dialog.component.html',
styleUrls: ['./edit-redaction-dialog.component.scss'],
}) })
export class EditRedactionDialogComponent export class EditRedactionDialogComponent
extends IqserDialogComponent<EditRedactionDialogComponent, EditRedactionData, EditRedactResult> extends IqserDialogComponent<EditRedactionDialogComponent, EditRedactionData, EditRedactResult>
@ -21,38 +22,30 @@ export class EditRedactionDialogComponent
readonly #dossier = inject(ActiveDossiersService).find(this.data.dossierId); readonly #dossier = inject(ActiveDossiersService).find(this.data.dossierId);
readonly #applyToAllDossiers = this.data.applyToAllDossiers; readonly #applyToAllDossiers = this.data.applyToAllDossiers;
readonly iconButtonTypes = IconButtonTypes; readonly iconButtonTypes = IconButtonTypes;
readonly redactedText: string; readonly redactedTexts?: string[];
readonly isModifyDictionary: boolean; readonly isModifyDictionary: boolean;
readonly isImage: boolean; readonly isImage: boolean;
readonly isManualRedaction: boolean; readonly isManualRedaction: boolean;
readonly showLegalReason: boolean;
readonly isHint: boolean; readonly isHint: boolean;
readonly allRectangles = this.data.annotations.reduce((acc, a) => acc && a.rectangle, true); readonly allRectangles = this.data.annotations.reduce((acc, a) => acc && a.rectangle, true);
readonly showExtras: boolean; readonly showExtras: boolean;
options: DetailsRadioOption<RedactOrHintOption>[] | undefined; options: DetailsRadioOption<RedactOrHintOption>[] | undefined;
legalOptions: LegalBasisOption[] = []; legalOptions: LegalBasisOption[] = [];
dictionaries: Dictionary[] = []; dictionaries: Dictionary[] = [];
readonly form = new FormGroup({ readonly form = this.#getForm();
reason: new FormControl<LegalBasisOption>(null),
comment: new FormControl<string>(null),
type: new FormControl<string>(this.data.annotations[0].type),
section: new FormControl<string>(this.data.annotations[0].section),
option: new FormControl<LegalBasisOption>(null),
value: new FormControl<string>(this.allRectangles ? this.data.annotations[0].value : null),
});
constructor( constructor(
private readonly _justificationsService: JustificationsService, private readonly _justificationsService: JustificationsService,
private readonly _dictionaryService: DictionaryService, private readonly _dictionaryService: DictionaryService,
private readonly _formBuilder: FormBuilder,
) { ) {
super(); super();
const annotations = this.data.annotations; const annotations = this.data.annotations;
const firstEntry = annotations[0]; this.isImage = annotations.reduce((acc, next) => acc && [...IMAGE_CATEGORIES, 'ocr'].includes(next.type), true);
this.isImage = [...IMAGE_CATEGORIES, 'ocr'].includes(firstEntry.type); this.redactedTexts = !this.isImage ? annotations.map(annotation => annotation.value) : null;
this.redactedText = annotations.length === 1 && !this.isImage ? firstEntry.value : null; this.isModifyDictionary = annotations.every(annotation => annotation.isModifyDictionary);
this.isModifyDictionary = firstEntry.isModifyDictionary; this.isManualRedaction = annotations.every(annotation => annotation.type === SuperTypes.ManualRedaction);
this.isManualRedaction = firstEntry.type === SuperTypes.ManualRedaction; this.isHint = annotations.every(annotation => annotation.isHint);
this.isHint = firstEntry.isHint;
this.showExtras = !(this.isImage || this.isHint); this.showExtras = !(this.isImage || this.isHint);
} }
@ -65,7 +58,7 @@ export class EditRedactionDialogComponent
} }
get disabled() { get disabled() {
return !this.form.controls.reason.value; return this.showExtras ? !this.form.controls.reason.value : false;
} }
async ngOnInit() { async ngOnInit() {
@ -78,11 +71,13 @@ export class EditRedactionDialogComponent
})); }));
const reason = this.legalOptions.find(o => o.legalBasis === this.data.annotations[0].legalBasis); const reason = this.legalOptions.find(o => o.legalBasis === this.data.annotations[0].legalBasis);
const sameLegalBasis = this.data.annotations.every(annotation => annotation.legalBasis === reason?.legalBasis);
this.form.patchValue({ this.form.patchValue({
reason: reason, reason: sameLegalBasis ? reason : null,
}); });
this.legalOptions.sort((a, b) => a.label.localeCompare(b.label)); this.legalOptions.sort((a, b) => a.label.localeCompare(b.label));
this.initialFormValue = this.form.getRawValue();
} }
typeChanged() { typeChanged() {
@ -106,7 +101,7 @@ export class EditRedactionDialogComponent
this.#dossier.dossierTemplateId, this.#dossier.dossierTemplateId,
this.isImage, this.isImage,
this.isHint, this.isHint,
this.data.annotations[0].isOCR, this.data.annotations.every(annotation => annotation.isOCR),
); );
} }
@ -125,4 +120,17 @@ export class EditRedactionDialogComponent
{ emitEvent: false }, { emitEvent: false },
); );
} }
#getForm() {
const sameType = this.data.annotations.every(annotation => annotation.type === this.data.annotations[0].type);
const sameSection = this.data.annotations.every(annotation => annotation.section === this.data.annotations[0].section);
return this._formBuilder.group({
reason: new FormControl<LegalBasisOption>(null),
comment: new FormControl<string>(null),
type: new FormControl<string>(sameType ? this.data.annotations[0].type : null),
section: new FormControl<string>(sameSection ? this.data.annotations[0].section : null),
option: new FormControl<LegalBasisOption>(null),
value: new FormControl<string>(this.allRectangles ? this.data.annotations[0].value : null),
});
}
} }

View File

@ -104,12 +104,16 @@ export class AnnotationActionsService {
return; return;
} }
if (!this.#isDocumine && (result.legalBasis !== annotations[0].legalBasis || result.section !== annotations[0].section)) { if (
!this.#isDocumine &&
(!annotations.every(annotation => annotation.legalBasis === result.legalBasis) ||
!annotations.every(annotation => annotation.section === result.section))
) {
const changeLegalBasisBody = annotations.map(annotation => ({ const changeLegalBasisBody = annotations.map(annotation => ({
annotationId: annotation.id, annotationId: annotation.id,
legalBasis: result.legalBasis, legalBasis: result.legalBasis,
section: result.section, section: result.section ?? annotation.section,
value: result.value, value: result.value ?? annotation.value,
})); }));
requests.push(this._manualRedactionService.changeLegalBasis(changeLegalBasisBody, dossierId, fileId)); requests.push(this._manualRedactionService.changeLegalBasis(changeLegalBasisBody, dossierId, fileId));
} }

View File

@ -49,12 +49,18 @@ export class PdfAnnotationActionsService {
availableActions.push(resizeButton); availableActions.push(resizeButton);
} }
const canEditRedactions = const canEditRedactions =
permissions.canChangeLegalBasis || permissions.canChangeLegalBasis ||
permissions.canRecategorizeAnnotation || permissions.canRecategorizeAnnotation ||
permissions.canForceHint || permissions.canForceHint ||
permissions.canForceRedaction; permissions.canForceRedaction;
const canEdit = this.#isDocumine && annotations.length > 1 ? permissions.canEditAnnotations : canEditRedactions; const canEdit =
annotations.length > 1
? this.#isDocumine
? permissions.canEditAnnotations
: permissions.canEditHints || permissions.canEditImages || permissions.canEditAnnotations
: canEditRedactions;
if (canEdit) { if (canEdit) {
const editButton = this.#getButton('edit', _('annotation-actions.edit-redaction.label'), () => const editButton = this.#getButton('edit', _('annotation-actions.edit-redaction.label'), () =>
this.#annotationActionsService.editRedaction(annotations), this.#annotationActionsService.editRedaction(annotations),

View File

@ -1249,7 +1249,7 @@
} }
}, },
"reason": "Reason", "reason": "Reason",
"redacted-text": "{type, select, hint{Hint} other{Redacted}} text", "redacted-text": "Redacted text",
"section": "Paragraph / Location", "section": "Paragraph / Location",
"type": "Type", "type": "Type",
"unchanged": "Unchanged" "unchanged": "Unchanged"
@ -2541,4 +2541,4 @@
} }
}, },
"yesterday": "Gestern" "yesterday": "Gestern"
} }

View File

@ -1249,7 +1249,7 @@
} }
}, },
"reason": "Reason", "reason": "Reason",
"redacted-text": "{type, select, hint{Hint} other{Redacted}} text", "redacted-text": "Redacted text",
"section": "Paragraph / Location", "section": "Paragraph / Location",
"type": "Type", "type": "Type",
"unchanged": "Unchanged" "unchanged": "Unchanged"
@ -2069,7 +2069,7 @@
"label": "Remove here" "label": "Remove here"
} }
}, },
"redacted-text": "Selected redactions" "redacted-text": "Selected {length, plural, one{redaction} other {redactions}}"
}, },
"title": "Remove {count, plural, one{annotation} other {annotations}}" "title": "Remove {count, plural, one{annotation} other {annotations}}"
} }
@ -2541,4 +2541,4 @@
} }
}, },
"yesterday": "Yesterday" "yesterday": "Yesterday"
} }

@ -1 +1 @@
Subproject commit 6cb63fcf43fbbf522b847f57467156808bbe72a1 Subproject commit d1df30b56ea5abd03b0c8623f68fc96db9fef271