Merge branch 'VM/RED-7980' into 'master'

RED-7980 - Display unprocessed annotations correctly as pending

Closes RED-7980

See merge request redactmanager/red-ui!333
This commit is contained in:
Dan Percic 2024-03-08 10:06:45 +01:00
commit 829515aaef
11 changed files with 106 additions and 41 deletions

View File

@ -9,8 +9,10 @@ import {
Dictionary,
Earmark,
EntityTypes,
EntryState,
EntryStates,
FalsePositiveSuperTypes,
IEntityLog,
IEntityLogEntry,
ILegalBasis,
IPoint,
@ -209,6 +211,7 @@ export class AnnotationWrapper implements IListable {
static fromData(
logEntry: IEntityLogEntry,
allLogEntries: IEntityLogEntry[],
dictionary: Dictionary,
changeLogType: ChangeType,
legalBasisList: ILegalBasis[],
@ -268,7 +271,13 @@ export class AnnotationWrapper implements IListable {
const lastRelevantManualChange = logEntry.manualChanges?.at(-1);
annotationWrapper.lastManualChange = lastRelevantManualChange?.manualRedactionType;
annotationWrapper.pending = lastRelevantManualChange && !lastRelevantManualChange.processed;
annotationWrapper.pending = logEntry.state === EntryStates.PENDING;
if (annotationWrapper.pending) {
const removedEntry = allLogEntries.find(
(e: IEntityLogEntry) => e.id === annotationWrapper.id && e.state === EntryStates.REMOVED,
);
logEntry.oldState = removedEntry?.state;
}
annotationWrapper.superType = SuperTypeMapper[logEntry.entryType][logEntry.state](logEntry);
annotationWrapper.superTypeLabel = annotationTypesTranslations[annotationWrapper.superType];
@ -277,9 +286,13 @@ export class AnnotationWrapper implements IListable {
annotationWrapper.typeLabel = dictionary?.virtual ? undefined : dictionary?.label;
const colorKey = annotationEntityColorConfig[annotationWrapper.superType];
const defaultColor = annotationDefaultColorConfig[annotationWrapper.superType];
annotationWrapper.color = dictionary ? (dictionary[colorKey] as string) : (defaultColors[defaultColor] as string);
if (annotationWrapper.pending) {
annotationWrapper.color = defaultColors[annotationDefaultColorConfig.analysis] as string;
} else {
const colorKey = annotationEntityColorConfig[annotationWrapper.superType];
const defaultColor = annotationDefaultColorConfig[annotationWrapper.superType];
annotationWrapper.color = dictionary ? (dictionary[colorKey] as string) : (defaultColors[defaultColor] as string);
}
annotationWrapper['entry'] = logEntry;
return annotationWrapper;

View File

@ -10,7 +10,7 @@
<div>
<strong>{{ annotation.superTypeLabel | translate }}</strong>
&nbsp;
<strong *ngIf="pending" class="pending-analysis">
<strong *ngIf="annotation.pending" class="pending-analysis">
{{ 'annotation.pending' | translate }}
</strong>
</div>

View File

@ -18,17 +18,4 @@ export class AnnotationCardComponent {
@Input() isSelected = false;
constructor(readonly multiSelectService: MultiSelectService) {}
get pending() {
return (
this.annotation.pending &&
((this.annotation.isModifyDictionary &&
!this.annotation.isRemovedLocally &&
!this.annotation.hasBeenForcedHint &&
this.annotation.lastManualChange !== ManualRedactionTypes.LEGAL_BASIS_CHANGE &&
this.annotation.lastManualChange !== ManualRedactionTypes.RESIZE_LOCALLY) ||
this.annotation.type === ImageCategory.SIGNATURE ||
this.annotation.IMAGE_HINT)
);
}
}

View File

@ -6,7 +6,6 @@
}
.popover {
width: 260px;
padding: 10px;
border-radius: 3px;
background-color: var(--iqser-grey-1);

View File

@ -229,6 +229,7 @@ export class FileDataService extends EntitiesService<AnnotationWrapper, Annotati
const changeType = this.#getChangeLogType(entry, file);
const annotation = AnnotationWrapper.fromData(
entry,
entityLog.entityLogEntry,
dictionary,
changeType,
entityLog.legalBasis ?? [],

View File

@ -2,7 +2,7 @@ import { Injectable } from '@angular/core';
import { List } from '@iqser/common-ui/lib/utils';
import { AnnotationWrapper } from '@models/file/annotation.wrapper';
import { Core } from '@pdftron/webviewer';
import { IRectangle, ISectionRectangle, SuperTypes } from '@red/domain';
import { IRectangle, ISectionRectangle, IPoint, SuperTypes } from '@red/domain';
import { DefaultColorsService } from '@services/entity-services/default-colors.service';
import { hexToRgb } from '@utils/functions';
import { BoundingBox, Table } from '../../file-preview/services/tables.service';
@ -11,6 +11,7 @@ import { REDDocumentViewer } from './document-viewer.service';
import { PdfViewer } from './pdf-viewer.service';
import Annotation = Core.Annotations.Annotation;
import Quad = Core.Math.Quad;
import PolylineAnnotation = Core.Annotations.PolylineAnnotation;
const DEFAULT_TEXT_ANNOTATION_OPACITY = 1;
const DEFAULT_REMOVED_ANNOTATION_OPACITY = 0.2;
@ -128,6 +129,24 @@ export class AnnotationDrawService {
return;
}
if (annotationWrapper.pending) {
const polylineAnnot = this._pdf.polyline();
polylineAnnot.ReadOnly = true;
polylineAnnot.StrokeColor = this.convertColor(annotationWrapper.color);
polylineAnnot.StrokeThickness = 4;
polylineAnnot.Id = annotationWrapper.id;
polylineAnnot.PageNumber = pageNumber;
polylineAnnot.Opacity = 0.5;
const points = this.#computePolylinePoints(annotationWrapper.positions, annotationWrapper.pageNumber);
for (let i = 0; i < points.length; i++) {
const y = i % 2 === 1 ? points[i - 1].y : points[i].y;
polylineAnnot.addPathPoint(points[i].x, y);
}
return polylineAnnot;
}
if (annotationWrapper.superType === SuperTypes.TextHighlight) {
const rectangleAnnot = this._pdf.rectangle();
const pageHeight = this._documentViewer.getHeight(pageNumber);
@ -184,18 +203,53 @@ export class AnnotationDrawService {
}
#rectangleToQuad(rectangle: IRectangle, pageHeight: number): Quad {
const x1 = rectangle.topLeft.x;
const y1 = pageHeight - (rectangle.topLeft.y + rectangle.height);
const x2 = rectangle.topLeft.x + rectangle.width;
const y2 = pageHeight - (rectangle.topLeft.y + rectangle.height);
const x3 = rectangle.topLeft.x + rectangle.width;
const y3 = pageHeight - rectangle.topLeft.y;
const x4 = rectangle.topLeft.x;
const y4 = pageHeight - rectangle.topLeft.y;
const { x: x1, y: y1 } = this.#topLeft(rectangle, pageHeight);
const { x: x2, y: y2 } = this.#topRight(rectangle, pageHeight);
const { x: x3, y: y3 } = this.#bottomRight(rectangle, pageHeight);
const { x: x4, y: y4 } = this.#bottomLeft(rectangle, pageHeight);
return this._pdf.quad(x1, y1, x2, y2, x3, y3, x4, y4);
}
#computePolylinePoints(positions: IRectangle[], pageNumber: number): IPoint[] {
const pageHeight = this._documentViewer.getHeight(pageNumber);
const points = [this.#topLeft(positions[0], pageHeight)];
for (let i = 0; i < positions.length; i++) {
points.push(this.#topRight(positions[i], pageHeight));
points.push(this.#bottomRight(positions[i], pageHeight));
}
for (let i = positions.length - 1; i >= 0; i--) {
points.push(this.#bottomLeft(positions[i], pageHeight));
points.push(this.#topLeft(positions[i], pageHeight));
}
points.push(this.#topRight(positions[0], pageHeight));
return points;
}
#topLeft(rectangle: IRectangle, pageHeight: number): IPoint {
const x = rectangle.topLeft.x;
const y = pageHeight - (rectangle.topLeft.y + rectangle.height);
return { x, y };
}
#topRight(rectangle: IRectangle, pageHeight: number): IPoint {
const x = rectangle.topLeft.x + rectangle.width;
const y = pageHeight - (rectangle.topLeft.y + rectangle.height);
return { x, y };
}
#bottomRight(rectangle: IRectangle, pageHeight: number): IPoint {
const x = rectangle.topLeft.x + rectangle.width;
const y = pageHeight - rectangle.topLeft.y;
return { x, y };
}
#bottomLeft(rectangle: IRectangle, pageHeight: number): IPoint {
const x = rectangle.topLeft.x;
const y = pageHeight - rectangle.topLeft.y;
return { x, y };
}
}

View File

@ -243,6 +243,10 @@ export class PdfViewer {
return new this.#instance.Core.Annotations.TextHighlightAnnotation();
}
polyline() {
return new this.#instance.Core.Annotations.PolylineAnnotation();
}
isTextHighlight(annotation: Annotation): annotation is TextHighlightAnnotation {
return annotation instanceof this.#instance.Core.Annotations.TextHighlightAnnotation;
}

@ -1 +1 @@
Subproject commit 3ea4e45b87b94868370492c475864390d984f65d
Subproject commit ecf9c8912e366fdcc0a454d112c0f3252245666a

View File

@ -19,18 +19,15 @@ function wrongSuperTypeHandler(): never | undefined {
}
function resolveRedactionType(entry: IEntityLogEntry, hint = false) {
if (entry.state === EntryStates.PENDING && entry.oldState) {
return SuperTypeMapper[entry.entryType][entry.oldState](entry);
}
const redaction = hint ? SuperTypes.Hint : SuperTypes.Redaction;
const manualRedaction = hint ? SuperTypes.ManualHint : SuperTypes.ManualRedaction;
const manualChanges = entry.manualChanges;
const hasEngines = entry.engines.length;
const lastRelevantManualChange = manualChanges?.at(-1);
if (!!lastRelevantManualChange && !hasEngines) {
const pending = lastRelevantManualChange && !lastRelevantManualChange.processed;
if (pending && entry.dictionaryEntry) {
return redaction;
}
return manualRedaction;
if (!entry.engines.length) {
return entry.state === EntryStates.PENDING && entry.dictionaryEntry ? redaction : manualRedaction;
}
return redaction;
}
@ -44,48 +41,56 @@ export const SuperTypeMapper: Record<EntityType, Record<EntryState, (entry: IEnt
[EntryStates.SKIPPED]: () => SuperTypes.Skipped,
[EntryStates.IGNORED]: () => SuperTypes.Skipped,
[EntryStates.REMOVED]: wrongSuperTypeHandler,
[EntryStates.PENDING]: entry => resolveRedactionType(entry),
},
[EntityTypes.HINT]: {
[EntryStates.APPLIED]: wrongSuperTypeHandler,
[EntryStates.SKIPPED]: entry => resolveRedactionType(entry, true),
[EntryStates.IGNORED]: () => SuperTypes.IgnoredHint,
[EntryStates.REMOVED]: wrongSuperTypeHandler,
[EntryStates.PENDING]: entry => resolveRedactionType(entry),
},
[EntityTypes.FALSE_POSITIVE]: {
[EntryStates.APPLIED]: wrongSuperTypeHandler,
[EntryStates.SKIPPED]: wrongSuperTypeHandler,
[EntryStates.IGNORED]: wrongSuperTypeHandler,
[EntryStates.REMOVED]: wrongSuperTypeHandler,
[EntryStates.PENDING]: entry => resolveRedactionType(entry),
},
[EntityTypes.RECOMMENDATION]: {
[EntryStates.APPLIED]: wrongSuperTypeHandler,
[EntryStates.SKIPPED]: () => SuperTypes.Recommendation,
[EntryStates.IGNORED]: wrongSuperTypeHandler,
[EntryStates.REMOVED]: wrongSuperTypeHandler,
[EntryStates.PENDING]: entry => resolveRedactionType(entry),
},
[EntityTypes.FALSE_RECOMMENDATION]: {
[EntryStates.APPLIED]: wrongSuperTypeHandler,
[EntryStates.SKIPPED]: wrongSuperTypeHandler,
[EntryStates.IGNORED]: wrongSuperTypeHandler,
[EntryStates.REMOVED]: wrongSuperTypeHandler,
[EntryStates.PENDING]: entry => resolveRedactionType(entry),
},
[EntityTypes.AREA]: {
[EntryStates.APPLIED]: () => SuperTypes.Redaction,
[EntryStates.SKIPPED]: wrongSuperTypeHandler,
[EntryStates.IGNORED]: wrongSuperTypeHandler,
[EntryStates.REMOVED]: wrongSuperTypeHandler,
[EntryStates.PENDING]: entry => resolveRedactionType(entry),
},
[EntityTypes.IMAGE]: {
[EntryStates.APPLIED]: () => SuperTypes.Redaction,
[EntryStates.SKIPPED]: () => SuperTypes.Skipped,
[EntryStates.IGNORED]: () => SuperTypes.Skipped,
[EntryStates.REMOVED]: wrongSuperTypeHandler,
[EntryStates.PENDING]: entry => resolveRedactionType(entry),
},
[EntityTypes.IMAGE_HINT]: {
[EntryStates.APPLIED]: wrongSuperTypeHandler,
[EntryStates.SKIPPED]: () => SuperTypes.Hint,
[EntryStates.IGNORED]: () => SuperTypes.IgnoredHint,
[EntryStates.REMOVED]: wrongSuperTypeHandler,
[EntryStates.PENDING]: entry => resolveRedactionType(entry),
},
};

View File

@ -14,6 +14,7 @@ export interface IEntityLogEntry extends ITrackable {
type: string;
entryType: EntityType;
state: EntryState;
oldState?: EntryState;
value: string;
reason: string;
matchedRule: string;

View File

@ -3,6 +3,7 @@ export const EntryStates = {
SKIPPED: 'SKIPPED',
IGNORED: 'IGNORED',
REMOVED: 'REMOVED',
PENDING: 'PENDING',
} as const;
export type EntryState = (typeof EntryStates)[keyof typeof EntryStates];