RED-5910: use manualChanges property to show delta changes

This commit is contained in:
Dan Percic 2023-01-11 16:15:38 +02:00
parent c9962a4fe2
commit ee812ee539
7 changed files with 155 additions and 120 deletions

View File

@ -12,9 +12,9 @@ import {
IManualChange,
IPoint,
IRectangle,
LogEntryStatus,
LogEntryStatuses,
LowLevelFilterTypes,
ManualRedactionType,
ManualRedactionTypes,
SuggestionAddSuperTypes,
SuggestionRemoveSuperTypes,
SuggestionsSuperTypes,
@ -218,10 +218,6 @@ export class AnnotationWrapper implements IListable, Record<string, unknown> {
);
}
get isConvertedRecommendation() {
return this.isRecommendation && this.superType === SuperTypes.SuggestionAddDictionary;
}
get isRecommendation() {
return this.superType === SuperTypes.Recommendation;
}
@ -304,25 +300,25 @@ export class AnnotationWrapper implements IListable, Record<string, unknown> {
annotationWrapper.rectangle = redactionLogEntry.rectangle;
annotationWrapper.hintDictionary = redactionLogEntry.hintDictionary;
annotationWrapper.hasBeenResized = !!redactionLogEntry.manualChanges?.find(
c => c.manualRedactionType === ManualRedactionType.RESIZE && c.annotationStatus === LogEntryStatus.APPROVED,
c => c.manualRedactionType === ManualRedactionTypes.RESIZE && c.annotationStatus === LogEntryStatuses.APPROVED,
);
annotationWrapper.hasBeenRecategorized = !!redactionLogEntry.manualChanges?.find(
c => c.manualRedactionType === ManualRedactionType.RECATEGORIZE && c.annotationStatus === LogEntryStatus.APPROVED,
c => c.manualRedactionType === ManualRedactionTypes.RECATEGORIZE && c.annotationStatus === LogEntryStatuses.APPROVED,
);
annotationWrapper.hasLegalBasisChanged = !!redactionLogEntry.manualChanges?.find(
c => c.manualRedactionType === ManualRedactionType.LEGAL_BASIS_CHANGE && c.annotationStatus === LogEntryStatus.APPROVED,
c => c.manualRedactionType === ManualRedactionTypes.LEGAL_BASIS_CHANGE && c.annotationStatus === LogEntryStatuses.APPROVED,
);
annotationWrapper.hasBeenForcedHint = !!redactionLogEntry.manualChanges?.find(
c => c.manualRedactionType === ManualRedactionType.FORCE_HINT && c.annotationStatus === LogEntryStatus.APPROVED,
c => c.manualRedactionType === ManualRedactionTypes.FORCE_HINT && c.annotationStatus === LogEntryStatuses.APPROVED,
);
annotationWrapper.hasBeenForcedRedaction = !!redactionLogEntry.manualChanges?.find(
c => c.manualRedactionType === ManualRedactionType.FORCE_REDACT && c.annotationStatus === LogEntryStatus.APPROVED,
c => c.manualRedactionType === ManualRedactionTypes.FORCE_REDACT && c.annotationStatus === LogEntryStatuses.APPROVED,
);
annotationWrapper.hasBeenRemovedByManualOverride = !!redactionLogEntry.manualChanges?.find(
c => c.manualRedactionType === ManualRedactionType.REMOVE_LOCALLY && c.annotationStatus === LogEntryStatus.APPROVED,
c => c.manualRedactionType === ManualRedactionTypes.REMOVE_LOCALLY && c.annotationStatus === LogEntryStatuses.APPROVED,
);
annotationWrapper.legalBasisChangeValue = redactionLogEntry.manualChanges?.find(
c => c.manualRedactionType === ManualRedactionType.LEGAL_BASIS_CHANGE && c.annotationStatus === LogEntryStatus.REQUESTED,
c => c.manualRedactionType === ManualRedactionTypes.LEGAL_BASIS_CHANGE && c.annotationStatus === LogEntryStatuses.REQUESTED,
)?.propertyChanges.legalBasis;
this._createContent(annotationWrapper, redactionLogEntry);
@ -370,7 +366,7 @@ export class AnnotationWrapper implements IListable, Record<string, unknown> {
annotationWrapper.hintDictionary,
);
if (lastRelevantManualChange.annotationStatus === LogEntryStatus.REQUESTED) {
if (lastRelevantManualChange.annotationStatus === LogEntryStatuses.REQUESTED) {
annotationWrapper.recategorizationType = lastRelevantManualChange.propertyChanges.type;
}
} else {
@ -436,31 +432,31 @@ export class AnnotationWrapper implements IListable, Record<string, unknown> {
isHintDictionary: boolean,
): SuperType {
switch (lastManualChange.manualRedactionType) {
case ManualRedactionType.ADD_LOCALLY:
case ManualRedactionTypes.ADD_LOCALLY:
switch (lastManualChange.annotationStatus) {
case LogEntryStatus.APPROVED:
case LogEntryStatuses.APPROVED:
return SuperTypes.ManualRedaction;
case LogEntryStatus.DECLINED:
case LogEntryStatuses.DECLINED:
return SuperTypes.DeclinedSuggestion;
case LogEntryStatus.REQUESTED:
case LogEntryStatuses.REQUESTED:
return SuperTypes.SuggestionAdd;
}
break;
case ManualRedactionType.ADD_TO_DICTIONARY:
case ManualRedactionTypes.ADD_TO_DICTIONARY:
switch (lastManualChange.annotationStatus) {
case LogEntryStatus.APPROVED:
case LogEntryStatuses.APPROVED:
return isHintDictionary ? SuperTypes.Hint : SuperTypes.Redaction;
case LogEntryStatus.DECLINED:
case LogEntryStatuses.DECLINED:
return SuperTypes.DeclinedSuggestion;
case LogEntryStatus.REQUESTED:
case LogEntryStatuses.REQUESTED:
return SuperTypes.SuggestionAddDictionary;
}
break;
case ManualRedactionType.REMOVE_LOCALLY:
case ManualRedactionTypes.REMOVE_LOCALLY:
switch (lastManualChange.annotationStatus) {
case LogEntryStatus.APPROVED:
case LogEntryStatuses.APPROVED:
return isHintDictionary ? SuperTypes.IgnoredHint : SuperTypes.Skipped;
case LogEntryStatus.DECLINED: {
case LogEntryStatuses.DECLINED: {
if (isHintDictionary) {
return SuperTypes.Hint;
}
@ -471,55 +467,55 @@ export class AnnotationWrapper implements IListable, Record<string, unknown> {
return SuperTypes.Skipped;
}
case LogEntryStatus.REQUESTED:
case LogEntryStatuses.REQUESTED:
return SuperTypes.SuggestionRemove;
}
break;
case ManualRedactionType.REMOVE_FROM_DICTIONARY:
case ManualRedactionTypes.REMOVE_FROM_DICTIONARY:
if (redactionLogEntry.redacted) {
switch (lastManualChange.annotationStatus) {
case LogEntryStatus.APPROVED:
case LogEntryStatuses.APPROVED:
return SuperTypes.Skipped;
case LogEntryStatus.DECLINED:
case LogEntryStatuses.DECLINED:
return SuperTypes.Redaction;
case LogEntryStatus.REQUESTED:
case LogEntryStatuses.REQUESTED:
return SuperTypes.SuggestionRemoveDictionary;
}
} else {
switch (lastManualChange.annotationStatus) {
case LogEntryStatus.APPROVED:
case LogEntryStatuses.APPROVED:
return SuperTypes.Redaction;
case LogEntryStatus.DECLINED:
case LogEntryStatuses.DECLINED:
return isHintDictionary ? SuperTypes.Hint : SuperTypes.Skipped;
case LogEntryStatus.REQUESTED:
case LogEntryStatuses.REQUESTED:
return SuperTypes.SuggestionRemoveDictionary;
}
}
break;
case ManualRedactionType.FORCE_REDACT:
case ManualRedactionTypes.FORCE_REDACT:
switch (lastManualChange.annotationStatus) {
case LogEntryStatus.APPROVED:
case LogEntryStatuses.APPROVED:
return SuperTypes.Redaction;
case LogEntryStatus.DECLINED:
case LogEntryStatuses.DECLINED:
return isHintDictionary ? SuperTypes.IgnoredHint : SuperTypes.Skipped;
case LogEntryStatus.REQUESTED:
case LogEntryStatuses.REQUESTED:
return SuperTypes.SuggestionForceRedaction;
}
break;
case ManualRedactionType.FORCE_HINT:
case ManualRedactionTypes.FORCE_HINT:
switch (lastManualChange.annotationStatus) {
case LogEntryStatus.APPROVED:
case LogEntryStatuses.APPROVED:
return SuperTypes.Hint;
case LogEntryStatus.DECLINED:
case LogEntryStatuses.DECLINED:
return SuperTypes.IgnoredHint;
case LogEntryStatus.REQUESTED:
case LogEntryStatuses.REQUESTED:
return SuperTypes.SuggestionForceHint;
}
break;
case ManualRedactionType.RECATEGORIZE:
case ManualRedactionTypes.RECATEGORIZE:
switch (lastManualChange.annotationStatus) {
case LogEntryStatus.APPROVED:
case LogEntryStatus.DECLINED: {
case LogEntryStatuses.APPROVED:
case LogEntryStatuses.DECLINED: {
if (redactionLogEntry.recommendation) {
return SuperTypes.Recommendation;
} else if (redactionLogEntry.redacted) {
@ -529,23 +525,23 @@ export class AnnotationWrapper implements IListable, Record<string, unknown> {
}
return isHintDictionary ? SuperTypes.IgnoredHint : SuperTypes.Skipped;
}
case LogEntryStatus.REQUESTED:
case LogEntryStatuses.REQUESTED:
return SuperTypes.SuggestionRecategorizeImage;
}
break;
case ManualRedactionType.LEGAL_BASIS_CHANGE:
case ManualRedactionTypes.LEGAL_BASIS_CHANGE:
switch (lastManualChange.annotationStatus) {
case LogEntryStatus.APPROVED:
case LogEntryStatus.DECLINED:
case LogEntryStatuses.APPROVED:
case LogEntryStatuses.DECLINED:
return redactionLogEntry.type === SuperTypes.ManualRedaction ? SuperTypes.ManualRedaction : SuperTypes.Redaction;
case LogEntryStatus.REQUESTED:
case LogEntryStatuses.REQUESTED:
return SuperTypes.SuggestionChangeLegalBasis;
}
break;
case ManualRedactionType.RESIZE:
case ManualRedactionTypes.RESIZE:
switch (lastManualChange.annotationStatus) {
case LogEntryStatus.APPROVED:
case LogEntryStatus.DECLINED:
case LogEntryStatuses.APPROVED:
case LogEntryStatuses.DECLINED:
if (redactionLogEntry.recommendation) {
return SuperTypes.Recommendation;
} else if (redactionLogEntry.redacted) {
@ -557,7 +553,7 @@ export class AnnotationWrapper implements IListable, Record<string, unknown> {
}
return isHintDictionary ? SuperTypes.IgnoredHint : SuperTypes.Skipped;
case LogEntryStatus.REQUESTED:
case LogEntryStatuses.REQUESTED:
return SuperTypes.SuggestionResize;
}
break;

View File

@ -30,13 +30,13 @@ export class RedactionLogEntry implements IRedactionLogEntry {
readonly type?: string;
readonly value?: string;
readonly sourceId?: string;
readonly isChangeLogEntry: boolean;
reason?: string;
constructor(
redactionLogEntry: IRedactionLogEntry,
readonly changeLogType: 'ADDED' | 'REMOVED' | 'CHANGED',
readonly isChangeLogEntry: boolean,
readonly changeLogType: 'ADDED' | 'REMOVED' | 'CHANGED' | null,
readonly legalBasisList: ILegalBasis[],
readonly hintDictionary: boolean,
) {
@ -70,5 +70,6 @@ export class RedactionLogEntry implements IRedactionLogEntry {
this.value = redactionLogEntry.value;
this.imported = redactionLogEntry.imported;
this.sourceId = redactionLogEntry.sourceId;
this.isChangeLogEntry = !!this.changeLogType;
}
}

View File

@ -33,7 +33,8 @@ export class ViewSwitchComponent {
private readonly _dialogService: FilePreviewDialogService,
private readonly _toaster: Toaster,
) {
this.canSwitchToDeltaView$ = combineLatest([_fileDataService.hasChangeLog$, _stateService.file$]).pipe(
const hasChangeLog$ = this._fileDataService.annotations$.pipe(map(annotations => annotations.some(a => a.isChangeLogEntry)));
this.canSwitchToDeltaView$ = combineLatest([hasChangeLog$, _stateService.file$]).pipe(
map(([hasChangeLog, file]) => hasChangeLog && !file.isApproved),
);

View File

@ -1,4 +1,15 @@
import { ChangeType, File, IRedactionLog, IRedactionLogEntry, ManualRedactionType, ViewMode, ViewModes } from '@red/domain';
import {
ChangeType,
ChangeTypes,
File,
IRedactionLog,
IRedactionLogEntry,
LogEntryStatuses,
ManualRedactionType,
ViewedPage,
ViewMode,
ViewModes,
} from '@red/domain';
import { AnnotationWrapper } from '@models/file/annotation.wrapper';
import { BehaviorSubject, firstValueFrom, iif, Observable, Subject } from 'rxjs';
import { RedactionLogEntry } from '@models/file/redaction-log.entry';
@ -24,15 +35,23 @@ import { ViewedPagesMapService } from '@services/files/viewed-pages-map.service'
const DELTA_VIEW_TIME = 10 * 60 * 1000; // 10 minutes;
function timestampOf(value: string) {
return dayjs(value).valueOf();
}
function chronologicallyBy<T>(property: (x: T) => string) {
return (a: T, b: T) => timestampOf(property(a)) - timestampOf(property(b));
}
@Injectable()
export class FileDataService extends EntitiesService<AnnotationWrapper, AnnotationWrapper> {
missingTypes = new Set<string>();
readonly hasChangeLog$ = new BehaviorSubject<boolean>(false);
readonly annotations$: Observable<AnnotationWrapper[]>;
readonly earmarks$: Observable<Map<number, AnnotationWrapper[]>>;
protected readonly _entityClass = AnnotationWrapper;
readonly #redactionLog$ = new Subject<IRedactionLog>();
readonly #earmarks$ = new BehaviorSubject<Map<number, AnnotationWrapper[]>>(new Map());
#originalViewedPages: ViewedPage[] = [];
constructor(
private readonly _state: FilePreviewStateService,
@ -153,8 +172,8 @@ export class FileDataService extends EntitiesService<AnnotationWrapper, Annotati
return;
}
const viewedPages = await this._viewedPagesService.load(file.dossierId, file.fileId);
this._viewedPagesMapService.set(file.fileId, viewedPages);
this.#originalViewedPages = await this._viewedPagesService.load(file.dossierId, file.fileId);
this._viewedPagesMapService.set(file.fileId, this.#originalViewedPages);
}
#getVisibleAnnotations(annotations: AnnotationWrapper[], viewMode: ViewMode) {
@ -228,7 +247,6 @@ export class FileDataService extends EntitiesService<AnnotationWrapper, Annotati
const redactionLogEntryWrapper: RedactionLogEntry = new RedactionLogEntry(
redactionLogEntry,
changeLogValues.changeLogType,
changeLogValues.isChangeLogEntry,
redactionLog.legalBasis,
!!dictionary?.hint,
);
@ -254,58 +272,66 @@ export class FileDataService extends EntitiesService<AnnotationWrapper, Annotati
): {
hidden: boolean;
changeLogType: ChangeType;
isChangeLogEntry: boolean;
} {
const hasManualChanges = redactionLogEntry.manualChanges.length > 0;
if (file.numberOfAnalyses <= 1 && !file.hasUpdates && !hasManualChanges) {
return {
changeLogType: null,
isChangeLogEntry: false,
hidden: false,
};
}
const viableChanges = redactionLogEntry.changes.filter(c => c.analysisNumber > 1);
viableChanges.sort((a, b) => dayjs(a.dateTime).valueOf() - dayjs(b.dateTime).valueOf());
const lastChange = viableChanges.sort(chronologicallyBy(x => x.dateTime)).at(-1);
const lastChange = viableChanges.length >= 1 ? viableChanges[viableChanges.length - 1] : undefined;
const page = redactionLogEntry.positions?.[0].page;
const viewedPage = this._viewedPagesMapService.get(file.fileId, page);
const viewedPage = this.#originalViewedPages.find(p => p.page === page);
// page has been seen -> let's see if it's a change
if (viewedPage) {
const viewTime = dayjs(viewedPage.viewedTime).valueOf() - DELTA_VIEW_TIME;
// these are all unseen changes
const relevantChanges = viableChanges.filter(change => dayjs(change.dateTime).valueOf() > viewTime);
// at least one unseen change
if (relevantChanges.length > 0) {
// at least 1 relevant change
const showAsUnseen = dayjs(viewedPage.viewedTime).valueOf() < dayjs(lastChange.dateTime).valueOf();
if (showAsUnseen) {
this._viewedPagesMapService.delete(this._state.fileId, viewedPage);
}
this.hasChangeLog$.next(true);
return {
changeLogType: relevantChanges[relevantChanges.length - 1].type,
isChangeLogEntry: true,
hidden: false,
};
}
// no relevant changes - hide removed anyway
if (!viewedPage) {
return {
changeLogType: null,
isChangeLogEntry: false,
hidden: lastChange && lastChange.type === 'REMOVED',
hidden: lastChange && lastChange.type === ChangeTypes.REMOVED,
};
}
// Page doesn't have a view-time
// page has been seen -> let's see if it's a change
const viewTime = timestampOf(viewedPage.viewedTime) - DELTA_VIEW_TIME;
let changeOccurredAfterPageIsViewed = lastChange && timestampOf(lastChange.dateTime) > viewTime;
if (changeOccurredAfterPageIsViewed) {
this.#markPageAsUnseenIfNeeded(viewedPage, lastChange.dateTime);
return {
changeLogType: lastChange.type,
hidden: false,
};
}
const viableManualChanges = redactionLogEntry.manualChanges.filter(
change => change.processed && change.annotationStatus === LogEntryStatuses.APPROVED,
);
viableManualChanges.sort(chronologicallyBy(x => x.processedDate));
const lastManualChange = viableManualChanges.at(-1);
changeOccurredAfterPageIsViewed = lastManualChange && timestampOf(lastManualChange.processedDate) > viewTime;
if (changeOccurredAfterPageIsViewed) {
this.#markPageAsUnseenIfNeeded(viewedPage, lastManualChange.processedDate);
return {
changeLogType: ChangeTypes.CHANGED,
hidden: false,
};
}
// Page doesn't have a view-time or no relevant changes - hide removed anyway
return {
changeLogType: null,
isChangeLogEntry: false,
hidden: lastChange && lastChange.type === 'REMOVED',
hidden: lastChange && lastChange.type === ChangeTypes.REMOVED,
};
}
#markPageAsUnseenIfNeeded(viewedPage: ViewedPage, dateTime: string) {
const showAsUnseen = timestampOf(viewedPage.viewedTime) < timestampOf(dateTime);
if (showAsUnseen) {
this._viewedPagesMapService.delete(this._state.fileId, viewedPage);
}
}
}

View File

@ -1,11 +1,15 @@
import { ValuesOf } from '@iqser/common-ui';
export interface IChange {
dateTime?: string;
analysisNumber?: number;
type?: ChangeType;
}
export enum ChangeType {
ADDED = 'ADDED',
CHANGED = 'CHANGED',
REMOVED = 'REMOVED',
}
export const ChangeTypes = {
ADDED: 'ADDED',
CHANGED: 'CHANGED',
REMOVED: 'REMOVED',
} as const;
export type ChangeType = ValuesOf<typeof ChangeTypes>;

View File

@ -6,6 +6,5 @@ export interface IManualChange {
annotationStatus: LogEntryStatus;
manualRedactionType: ManualRedactionType;
propertyChanges: { [key: string]: any };
processed: boolean;
}

View File

@ -1,23 +1,31 @@
export enum LogEntryEngine {
DICTIONARY = 'DICTIONARY',
NER = 'NER',
RULE = 'RULE',
}
import { ValuesOf } from '@iqser/common-ui';
export enum ManualRedactionType {
ADD_LOCALLY = 'ADD_LOCALLY',
ADD_TO_DICTIONARY = 'ADD_TO_DICTIONARY',
REMOVE_LOCALLY = 'REMOVE_LOCALLY',
REMOVE_FROM_DICTIONARY = 'REMOVE_FROM_DICTIONARY',
FORCE_REDACT = 'FORCE_REDACT',
FORCE_HINT = 'FORCE_HINT',
RECATEGORIZE = 'RECATEGORIZE',
LEGAL_BASIS_CHANGE = 'LEGAL_BASIS_CHANGE',
RESIZE = 'RESIZE',
}
export const LogEntryEngines = {
DICTIONARY: 'DICTIONARY',
NER: 'NER',
RULE: 'RULE',
} as const;
export enum LogEntryStatus {
APPROVED = 'APPROVED',
DECLINED = 'DECLINED',
REQUESTED = 'REQUESTED',
}
export type LogEntryEngine = ValuesOf<typeof LogEntryEngines>;
export const ManualRedactionTypes = {
ADD_LOCALLY: 'ADD_LOCALLY',
ADD_TO_DICTIONARY: 'ADD_TO_DICTIONARY',
REMOVE_LOCALLY: 'REMOVE_LOCALLY',
REMOVE_FROM_DICTIONARY: 'REMOVE_FROM_DICTIONARY',
FORCE_REDACT: 'FORCE_REDACT',
FORCE_HINT: 'FORCE_HINT',
RECATEGORIZE: 'RECATEGORIZE',
LEGAL_BASIS_CHANGE: 'LEGAL_BASIS_CHANGE',
RESIZE: 'RESIZE',
} as const;
export type ManualRedactionType = ValuesOf<typeof ManualRedactionTypes>;
export const LogEntryStatuses = {
APPROVED: 'APPROVED',
DECLINED: 'DECLINED',
REQUESTED: 'REQUESTED',
} as const;
export type LogEntryStatus = ValuesOf<typeof LogEntryStatuses>;