From ee812ee5398608fb517dab7e16ef66f0cf5ba534 Mon Sep 17 00:00:00 2001 From: Dan Percic Date: Wed, 11 Jan 2023 16:15:38 +0200 Subject: [PATCH] RED-5910: use manualChanges property to show delta changes --- .../src/app/models/file/annotation.wrapper.ts | 102 +++++++++--------- .../app/models/file/redaction-log.entry.ts | 5 +- .../view-switch/view-switch.component.ts | 3 +- .../services/file-data.service.ts | 100 ++++++++++------- .../src/lib/redaction-log/change.ts | 14 ++- .../src/lib/redaction-log/manual-change.ts | 1 - .../red-domain/src/lib/redaction-log/types.ts | 50 +++++---- 7 files changed, 155 insertions(+), 120 deletions(-) diff --git a/apps/red-ui/src/app/models/file/annotation.wrapper.ts b/apps/red-ui/src/app/models/file/annotation.wrapper.ts index 2af861b76..860c633e0 100644 --- a/apps/red-ui/src/app/models/file/annotation.wrapper.ts +++ b/apps/red-ui/src/app/models/file/annotation.wrapper.ts @@ -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 { ); } - 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 { 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 { 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 { 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 { 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 { } 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 { } return isHintDictionary ? SuperTypes.IgnoredHint : SuperTypes.Skipped; - case LogEntryStatus.REQUESTED: + case LogEntryStatuses.REQUESTED: return SuperTypes.SuggestionResize; } break; diff --git a/apps/red-ui/src/app/models/file/redaction-log.entry.ts b/apps/red-ui/src/app/models/file/redaction-log.entry.ts index 43c92cc74..091bb012f 100644 --- a/apps/red-ui/src/app/models/file/redaction-log.entry.ts +++ b/apps/red-ui/src/app/models/file/redaction-log.entry.ts @@ -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; } } diff --git a/apps/red-ui/src/app/modules/file-preview/components/view-switch/view-switch.component.ts b/apps/red-ui/src/app/modules/file-preview/components/view-switch/view-switch.component.ts index 31113312d..866bc3c9a 100644 --- a/apps/red-ui/src/app/modules/file-preview/components/view-switch/view-switch.component.ts +++ b/apps/red-ui/src/app/modules/file-preview/components/view-switch/view-switch.component.ts @@ -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), ); diff --git a/apps/red-ui/src/app/modules/file-preview/services/file-data.service.ts b/apps/red-ui/src/app/modules/file-preview/services/file-data.service.ts index 9b49a34c6..4ffda0c28 100644 --- a/apps/red-ui/src/app/modules/file-preview/services/file-data.service.ts +++ b/apps/red-ui/src/app/modules/file-preview/services/file-data.service.ts @@ -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(property: (x: T) => string) { + return (a: T, b: T) => timestampOf(property(a)) - timestampOf(property(b)); +} + @Injectable() export class FileDataService extends EntitiesService { missingTypes = new Set(); - readonly hasChangeLog$ = new BehaviorSubject(false); readonly annotations$: Observable; readonly earmarks$: Observable>; protected readonly _entityClass = AnnotationWrapper; readonly #redactionLog$ = new Subject(); readonly #earmarks$ = new BehaviorSubject>(new Map()); + #originalViewedPages: ViewedPage[] = []; constructor( private readonly _state: FilePreviewStateService, @@ -153,8 +172,8 @@ export class FileDataService extends EntitiesService 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); + } + } } diff --git a/libs/red-domain/src/lib/redaction-log/change.ts b/libs/red-domain/src/lib/redaction-log/change.ts index b0cf47a38..25d88fcd1 100644 --- a/libs/red-domain/src/lib/redaction-log/change.ts +++ b/libs/red-domain/src/lib/redaction-log/change.ts @@ -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; diff --git a/libs/red-domain/src/lib/redaction-log/manual-change.ts b/libs/red-domain/src/lib/redaction-log/manual-change.ts index 923a39d5d..fdf26f57c 100644 --- a/libs/red-domain/src/lib/redaction-log/manual-change.ts +++ b/libs/red-domain/src/lib/redaction-log/manual-change.ts @@ -6,6 +6,5 @@ export interface IManualChange { annotationStatus: LogEntryStatus; manualRedactionType: ManualRedactionType; propertyChanges: { [key: string]: any }; - processed: boolean; } diff --git a/libs/red-domain/src/lib/redaction-log/types.ts b/libs/red-domain/src/lib/redaction-log/types.ts index 6f8477830..7e8965ff2 100644 --- a/libs/red-domain/src/lib/redaction-log/types.ts +++ b/libs/red-domain/src/lib/redaction-log/types.ts @@ -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; + +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; + +export const LogEntryStatuses = { + APPROVED: 'APPROVED', + DECLINED: 'DECLINED', + REQUESTED: 'REQUESTED', +} as const; + +export type LogEntryStatus = ValuesOf;