RED-6829: remove redactionLogEntry class

This commit is contained in:
Dan Percic 2023-06-09 20:13:12 +03:00
parent 2601822a32
commit e083fa5eb4
12 changed files with 148 additions and 246 deletions

View File

@ -4,15 +4,18 @@ import {
annotationDefaultColorConfig,
annotationEntityColorConfig,
AnnotationIconType,
ChangeType,
ChangeTypes,
DefaultColors,
Dictionary,
Earmark,
FalsePositiveSuperTypes,
IComment,
ILegalBasis,
IManualChange,
IPoint,
IRectangle,
IRedactionLogEntry,
LogEntryEngine,
LogEntryStatuses,
LowLevelFilterTypes,
@ -23,11 +26,10 @@ import {
SuperType,
SuperTypes,
} from '@red/domain';
import { RedactionLogEntry } from '@models/file/redaction-log.entry';
import { IListable } from '@iqser/common-ui';
import { chronologicallyBy, timestampOf } from '../../modules/file-preview/services/file-data.service';
export class AnnotationWrapper implements IListable, Record<string, unknown> {
export class AnnotationWrapper implements IListable {
[x: string]: unknown;
superType: SuperType;
@ -37,7 +39,7 @@ export class AnnotationWrapper implements IListable, Record<string, unknown> {
entity: Dictionary;
comments: IComment[] = [];
firstTopLeftPoint: IPoint;
annotationId: string;
id: string;
shortContent: string;
content: string;
value: string;
@ -70,6 +72,7 @@ export class AnnotationWrapper implements IListable, Record<string, unknown> {
hasBeenForcedHint: boolean;
hasBeenForcedRedaction: boolean;
hasBeenRemovedByManualOverride: boolean;
legalBasisList: ILegalBasis[] = [];
get searchKey(): string {
return this.id;
@ -240,10 +243,6 @@ export class AnnotationWrapper implements IListable, Record<string, unknown> {
return this.superType === SuperTypes.Recommendation;
}
get id() {
return this.annotationId;
}
get x() {
return this.firstTopLeftPoint.x;
}
@ -278,7 +277,7 @@ export class AnnotationWrapper implements IListable, Record<string, unknown> {
static fromEarmark(earmark: Earmark) {
const annotationWrapper = new AnnotationWrapper();
annotationWrapper.annotationId = earmark.id;
annotationWrapper.id = earmark.id;
annotationWrapper.pageNumber = earmark.positions[0].page;
annotationWrapper.superType = SuperTypes.TextHighlight;
annotationWrapper.typeValue = SuperTypes.TextHighlight;
@ -291,12 +290,20 @@ export class AnnotationWrapper implements IListable, Record<string, unknown> {
return annotationWrapper;
}
static fromData(redactionLogEntry: RedactionLogEntry, dictionaries: Dictionary[], defaultColors: DefaultColors) {
static fromData(
redactionLogEntry: IRedactionLogEntry,
dictionaries: Dictionary[],
defaultColors: DefaultColors,
changeLogType: ChangeType,
legalBasisList: ILegalBasis[],
hintDictionary: boolean,
) {
const annotationWrapper = new AnnotationWrapper();
annotationWrapper.annotationId = redactionLogEntry.id;
annotationWrapper.isChangeLogEntry = redactionLogEntry.isChangeLogEntry;
annotationWrapper.changeLogType = redactionLogEntry.changeLogType;
annotationWrapper.id = redactionLogEntry.id;
annotationWrapper.isChangeLogEntry = !!changeLogType;
annotationWrapper.changeLogType = changeLogType;
annotationWrapper.legalBasisList = legalBasisList;
annotationWrapper.redaction = redactionLogEntry.redacted;
annotationWrapper.hint = redactionLogEntry.hint;
annotationWrapper.typeValue = redactionLogEntry.type;
@ -316,7 +323,7 @@ export class AnnotationWrapper implements IListable, Record<string, unknown> {
annotationWrapper.section = redactionLogEntry.section;
annotationWrapper.reference = redactionLogEntry.reference || [];
annotationWrapper.rectangle = redactionLogEntry.rectangle;
annotationWrapper.hintDictionary = redactionLogEntry.hintDictionary;
annotationWrapper.hintDictionary = hintDictionary;
annotationWrapper.hasBeenResized = !!redactionLogEntry.manualChanges?.find(
c => c.manualRedactionType === ManualRedactionTypes.RESIZE && c.annotationStatus === LogEntryStatuses.APPROVED,
);
@ -360,53 +367,45 @@ export class AnnotationWrapper implements IListable, Record<string, unknown> {
return annotationWrapper;
}
static #getTypeLabel(redactionLogEntry: RedactionLogEntry, annotation: AnnotationWrapper): string {
static #getTypeLabel(redactionLogEntry: IRedactionLogEntry, annotation: AnnotationWrapper): string {
if (redactionLogEntry.reason?.toLowerCase() === 'false positive') {
return annotationTypesTranslations[SuggestionAddFalsePositive];
}
return annotationTypesTranslations[annotation.superType];
}
static #handleRecommendations(annotationWrapper: AnnotationWrapper, redactionLogEntry: RedactionLogEntry) {
static #handleRecommendations(annotationWrapper: AnnotationWrapper, redactionLogEntry: IRedactionLogEntry) {
if (annotationWrapper.superType === SuperTypes.Recommendation) {
annotationWrapper.recommendationType = redactionLogEntry.type;
}
}
static #getLastRelevantManualChange(manualChanges: IManualChange[]) {
return manualChanges[manualChanges.length - 1];
}
static #setSuperType(annotationWrapper: AnnotationWrapper, redactionLogEntryWrapper: RedactionLogEntry) {
if (redactionLogEntryWrapper.manualChanges?.length) {
const lastRelevantManualChange = this.#getLastRelevantManualChange(redactionLogEntryWrapper.manualChanges);
const viableChanges = redactionLogEntryWrapper.changes.filter(c => c.analysisNumber > 1);
static #setSuperType(annotationWrapper: AnnotationWrapper, entry: IRedactionLogEntry) {
if (entry.manualChanges?.length) {
const lastRelevantManualChange = entry.manualChanges?.at(-1);
const viableChanges = entry.changes.filter(c => c.analysisNumber > 1);
const lastChange = viableChanges.sort(chronologicallyBy(x => x.dateTime)).at(-1);
const lastChangeOccurredAfterLastManualChange =
lastChange && timestampOf(lastChange.dateTime) > timestampOf(lastRelevantManualChange.processedDate);
if (lastChangeOccurredAfterLastManualChange && lastChange.type === ChangeTypes.ADDED && redactionLogEntryWrapper.redacted) {
if (lastChangeOccurredAfterLastManualChange && lastChange.type === ChangeTypes.ADDED && entry.redacted) {
annotationWrapper.superType = SuperTypes.Redaction;
return;
}
annotationWrapper.pending = !lastRelevantManualChange.processed;
annotationWrapper.superType = AnnotationWrapper.#selectSuperType(
redactionLogEntryWrapper,
lastRelevantManualChange,
annotationWrapper.hintDictionary,
);
annotationWrapper.superType = this.#selectSuperType(entry, lastRelevantManualChange, annotationWrapper.hintDictionary);
if (lastRelevantManualChange.annotationStatus === LogEntryStatuses.REQUESTED) {
annotationWrapper.recategorizationType = lastRelevantManualChange.propertyChanges.type;
}
} else {
if (redactionLogEntryWrapper.recommendation) {
if (entry.recommendation) {
annotationWrapper.superType = SuperTypes.Recommendation;
} else if (redactionLogEntryWrapper.redacted) {
} else if (entry.redacted) {
annotationWrapper.superType = SuperTypes.Redaction;
} else if (redactionLogEntryWrapper.hint) {
} else if (entry.hint) {
annotationWrapper.superType = SuperTypes.Hint;
} else {
annotationWrapper.superType = SuperTypes.Skipped;
@ -414,7 +413,7 @@ export class AnnotationWrapper implements IListable, Record<string, unknown> {
}
}
static #createContent(annotationWrapper: AnnotationWrapper, entry: RedactionLogEntry) {
static #createContent(annotationWrapper: AnnotationWrapper, entry: IRedactionLogEntry) {
let content = '';
if (entry.matchedRule) {
content += `Rule ${entry.matchedRule} matched \n\n`;
@ -443,13 +442,15 @@ export class AnnotationWrapper implements IListable, Record<string, unknown> {
content += `${prefix} "${entry.section}"`;
}
annotationWrapper.shortContent = this.#getShortContent(annotationWrapper, entry) || content;
annotationWrapper.shortContent = this.#getShortContent(annotationWrapper) || content;
annotationWrapper.content = content;
}
static #getShortContent(annotationWrapper: AnnotationWrapper, entry: RedactionLogEntry) {
static #getShortContent(annotationWrapper: AnnotationWrapper) {
if (annotationWrapper.legalBasis) {
const lb = entry.legalBasisList?.find(lbm => lbm.reason?.toLowerCase().includes(annotationWrapper.legalBasis.toLowerCase()));
const lb = annotationWrapper.legalBasisList.find(lbm =>
lbm.reason?.toLowerCase().includes(annotationWrapper.legalBasis.toLowerCase()),
);
if (lb) {
return lb.name;
}
@ -458,7 +459,7 @@ export class AnnotationWrapper implements IListable, Record<string, unknown> {
return annotationWrapper.legalBasis;
}
static #selectSuperType(redactionLogEntry: RedactionLogEntry, lastManualChange: IManualChange, isHintDictionary: boolean): SuperType {
static #selectSuperType(redactionLogEntry: IRedactionLogEntry, lastManualChange: IManualChange, isHintDictionary: boolean): SuperType {
switch (lastManualChange.manualRedactionType) {
case ManualRedactionTypes.ADD_LOCALLY:
switch (lastManualChange.annotationStatus) {

View File

@ -1,76 +0,0 @@
import { IChange, IComment, ILegalBasis, IManualChange, IRectangle, IRedactionLogEntry, LogEntryEngine } from '@red/domain';
// TODO: this should be removed. Use only AnnotationWrapper class
export class RedactionLogEntry implements IRedactionLogEntry {
readonly changes: IChange[];
readonly imported: boolean;
readonly manualChanges: IManualChange[];
readonly color: number[];
readonly comments: IComment[];
readonly dictionaryEntry: boolean;
readonly dossierDictionaryEntry: boolean;
readonly endOffset?: number;
readonly engines: LogEntryEngine[];
readonly excluded: boolean;
readonly hint: boolean;
readonly rectangle: boolean;
readonly id: string;
readonly image: boolean;
readonly imageHasTransparency: boolean;
readonly legalBasis?: string;
readonly matchedRule?: number;
readonly positions: IRectangle[];
readonly recommendation: boolean;
readonly redacted: boolean;
readonly reference: string[];
readonly section?: string;
readonly sectionNumber?: number;
readonly startOffset?: number;
readonly textAfter?: string;
readonly textBefore?: string;
readonly type?: string;
readonly value?: string;
readonly sourceId?: string;
readonly isChangeLogEntry: boolean;
reason?: string;
constructor(
redactionLogEntry: IRedactionLogEntry,
readonly changeLogType: 'ADDED' | 'REMOVED' | 'CHANGED' | undefined,
readonly legalBasisList: ILegalBasis[],
readonly hintDictionary: boolean,
) {
this.changes = redactionLogEntry.changes ?? [];
this.manualChanges = redactionLogEntry.manualChanges ?? [];
this.color = redactionLogEntry.color ?? [];
this.comments = redactionLogEntry.comments ?? [];
this.dictionaryEntry = !!redactionLogEntry.dictionaryEntry;
this.dossierDictionaryEntry = !!redactionLogEntry.dossierDictionaryEntry;
this.endOffset = redactionLogEntry.endOffset;
this.engines = redactionLogEntry.engines ?? [];
this.excluded = !!redactionLogEntry.excluded;
this.hint = !!redactionLogEntry.hint;
this.rectangle = !!redactionLogEntry.rectangle;
this.id = redactionLogEntry.id;
this.image = !!redactionLogEntry.image;
this.imageHasTransparency = !!redactionLogEntry.imageHasTransparency;
this.legalBasis = redactionLogEntry.legalBasis;
this.matchedRule = redactionLogEntry.matchedRule;
this.positions = redactionLogEntry.positions ?? [];
this.reason = redactionLogEntry.reason;
this.recommendation = !!redactionLogEntry.recommendation;
this.redacted = !!redactionLogEntry.redacted;
this.reference = redactionLogEntry.reference ?? [];
this.section = redactionLogEntry.section;
this.sectionNumber = redactionLogEntry.sectionNumber;
this.startOffset = redactionLogEntry.startOffset;
this.textAfter = redactionLogEntry.textAfter;
this.textBefore = redactionLogEntry.textBefore;
this.type = redactionLogEntry.type;
this.value = redactionLogEntry.value;
this.imported = !!redactionLogEntry.imported;
this.sourceId = redactionLogEntry.sourceId;
this.isChangeLogEntry = !!this.changeLogType;
}
}

View File

@ -3,7 +3,7 @@ import { ActivatedRoute } from '@angular/router';
import { DictionaryManagerComponent } from '@shared/components/dictionary-manager/dictionary-manager.component';
import { DictionaryService } from '@services/entity-services/dictionary.service';
import { getCurrentUser, getParam, IqserPermissionsService, List, LoadingService } from '@iqser/common-ui';
import { BehaviorSubject, firstValueFrom } from 'rxjs';
import { BehaviorSubject } from 'rxjs';
import { DICTIONARY_TO_ENTRY_TYPE_MAP, DICTIONARY_TYPE_KEY_MAP, DictionaryType, DOSSIER_TEMPLATE_ID, ENTITY_TYPE, User } from '@red/domain';
import { PermissionsService } from '@services/permissions.service';
import { ROLES } from '@users/roles';
@ -26,7 +26,7 @@ export class DictionaryScreenComponent implements OnInit {
private readonly _dictionaryManager: DictionaryManagerComponent;
constructor(
readonly route: ActivatedRoute,
route: ActivatedRoute,
readonly permissionsService: PermissionsService,
readonly iqserPermissionsService: IqserPermissionsService,
private readonly _loadingService: LoadingService,
@ -40,8 +40,8 @@ export class DictionaryScreenComponent implements OnInit {
return this._dictionaryManager.editor.hasChanges;
}
async ngOnInit(): Promise<void> {
await this._loadEntries();
async ngOnInit() {
await this.#loadEntries();
}
async save() {
@ -60,22 +60,22 @@ export class DictionaryScreenComponent implements OnInit {
DICTIONARY_TO_ENTRY_TYPE_MAP[this.type],
false,
);
await this._loadEntries();
await this.#loadEntries();
} catch (e) {
this._loadingService.stop();
}
}
private async _loadEntries() {
async #loadEntries() {
this._loadingService.start();
try {
const data = await firstValueFrom(this._dictionaryService.getForType(this.#dossierTemplateId, this.entityType));
const data = await this._dictionaryService.getForType(this.#dossierTemplateId, this.entityType);
const entries: List = data[DICTIONARY_TYPE_KEY_MAP[this.type]];
this.initialEntries$.next([...entries].sort((str1, str2) => str1.localeCompare(str2, undefined, { sensitivity: 'accent' })));
this._loadingService.stop();
} catch (e) {
this._loadingService.stop();
this.initialEntries$.next([]);
} finally {
this._loadingService.stop();
}
}
}

View File

@ -133,8 +133,8 @@ export class AnnotationActionsService {
const data = { annotations, dossier: this._state.dossier };
const { dossierId, fileId } = this._state;
this._dialogService.openDialog('recategorizeImage', data, ({ comment, type }: { type: string; comment: string }) => {
const body: List<IRecategorizationRequest> = annotations.map(({ annotationId }) => ({
annotationId,
const body: List<IRecategorizationRequest> = annotations.map(({ id }) => ({
id: id,
type,
comment,
}));

View File

@ -20,7 +20,7 @@ export class AnnotationReferencesService {
const annotations$ = this._fileDataService.annotations$.pipe(map(dict => Object.values(dict)));
return this.annotation$.pipe(
filter(annotation => !!annotation),
switchMap(({ reference }) => annotations$.pipe(filterEach(a => reference.includes(a.annotationId)))),
switchMap(({ reference }) => annotations$.pipe(filterEach(a => reference.includes(a.id)))),
);
}

View File

@ -10,9 +10,8 @@ import {
ViewModes,
} from '@red/domain';
import { AnnotationWrapper } from '@models/file/annotation.wrapper';
import { BehaviorSubject, firstValueFrom, Observable, Subject, Subscription } from 'rxjs';
import { RedactionLogEntry } from '@models/file/redaction-log.entry';
import { Injectable, OnDestroy } from '@angular/core';
import { firstValueFrom, Observable, Subject } from 'rxjs';
import { Injectable, signal } from '@angular/core';
import { FilePreviewStateService } from './file-preview-state.service';
import { ViewedPagesService } from '@services/files/viewed-pages.service';
import { UserPreferenceService } from '@users/user-preference.service';
@ -31,6 +30,7 @@ import { DefaultColorsService } from '@services/entity-services/default-colors.s
import { DictionaryService } from '@services/entity-services/dictionary.service';
import { SuggestionsService } from './suggestions.service';
import { ViewedPagesMapService } from '@services/files/viewed-pages-map.service';
import { takeUntilDestroyed, toObservable } from '@angular/core/rxjs-interop';
const DELTA_VIEW_TIME = 10 * 60 * 1000; // 10 minutes;
@ -43,15 +43,14 @@ export function chronologicallyBy<T>(property: (x: T) => string) {
}
@Injectable()
export class FileDataService extends EntitiesService<AnnotationWrapper, AnnotationWrapper> implements OnDestroy {
export class FileDataService extends EntitiesService<AnnotationWrapper, AnnotationWrapper> {
missingTypes = new Set<string>();
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());
readonly #earmarks = signal<Map<number, AnnotationWrapper[]>>(new Map());
#originalViewedPages: ViewedPage[] = [];
readonly #subscription: Subscription;
constructor(
private readonly _state: FilePreviewStateService,
@ -72,15 +71,16 @@ export class FileDataService extends EntitiesService<AnnotationWrapper, Annotati
) {
super();
this.annotations$ = this.#annotations$;
this.earmarks$ = this.#earmarks$.asObservable();
this.#subscription = this._viewModeService.viewMode$
this.earmarks$ = toObservable(this.#earmarks);
this._viewModeService.viewMode$
.pipe(
switchMap(viewMode =>
viewMode === ViewModes.TEXT_HIGHLIGHTS
? this.#earmarks$.pipe(map(textHighlights => ([] as AnnotationWrapper[]).concat(...textHighlights.values())))
? this.earmarks$.pipe(map(textHighlights => ([] as AnnotationWrapper[]).concat(...textHighlights.values())))
: this.annotations$.pipe(map(annotations => this.#getVisibleAnnotations(annotations, viewMode))),
),
tap(annotations => this.setEntities(annotations)),
takeUntilDestroyed(),
)
.subscribe();
}
@ -94,7 +94,7 @@ export class FileDataService extends EntitiesService<AnnotationWrapper, Annotati
tap(() => console.time('buildAnnotations')),
withLatestFrom(this._state.file$),
tap(([redactionLog, file]) => this.#buildRemovedRedactions(redactionLog, file)),
switchMap(([redactionLog, file]) => this.#buildAnnotations(redactionLog, file)),
switchMap(([redactionLog, file]) => this.#convertData(redactionLog, file)),
tap(() => this.#checkMissingTypes()),
map(annotations =>
this._userPreferenceService.areDevFeaturesEnabled ? annotations : annotations.filter(a => !a.isFalsePositive),
@ -104,10 +104,6 @@ export class FileDataService extends EntitiesService<AnnotationWrapper, Annotati
);
}
ngOnDestroy() {
this.#subscription.unsubscribe();
}
setEntities(entities: AnnotationWrapper[]): void {
// this is a light version of setEntities to skip looping too much
// used mostly for earmarks (which are usually a lot)
@ -149,9 +145,9 @@ export class FileDataService extends EntitiesService<AnnotationWrapper, Annotati
}
async loadEarmarks() {
const rawHighlights = await firstValueFrom(this._earmarksService.getEarmarks(this._state.dossierId, this._state.fileId));
const rawHighlights = await this._earmarksService.getEarmarks(this._state.dossierId, this._state.fileId);
const earmarks = rawHighlights.groupBy(h => h.pageNumber);
this.#earmarks$.next(earmarks);
this.#earmarks.set(earmarks);
return earmarks;
}
@ -203,29 +199,11 @@ export class FileDataService extends EntitiesService<AnnotationWrapper, Annotati
});
}
async #buildAnnotations(redactionLog: IRedactionLog, file: File) {
const entries = await this.#convertData(redactionLog, file);
const dictionaries = this._state.dictionaries;
const defaultColors = this._defaultColorsService.find(this._state.dossierTemplateId);
const annotations: AnnotationWrapper[] = [];
for (const entry of entries) {
const pageNumber = entry.positions[0]?.page;
const manual = entry.manualChanges?.length > 0;
if (!manual && file.excludedPages.includes(pageNumber)) {
continue;
}
annotations.push(AnnotationWrapper.fromData(entry, dictionaries, defaultColors));
}
return annotations;
}
async #buildRemovedRedactions(redactionLog: IRedactionLog, file: File): Promise<void> {
const redactionLogCopy = JSON.parse(JSON.stringify(redactionLog));
const redactionLogCopy: IRedactionLog = JSON.parse(JSON.stringify(redactionLog));
const redactionLogEntries: IRedactionLogEntry[] = [];
redactionLogCopy.redactionLogEntry = redactionLogCopy.redactionLogEntry?.reduce((filtered, entry) => {
for (const entry of redactionLogCopy.redactionLogEntry) {
const lastChange = entry.manualChanges.at(-1);
if (
lastChange?.annotationStatus === LogEntryStatuses.REQUESTED &&
@ -234,12 +212,12 @@ export class FileDataService extends EntitiesService<AnnotationWrapper, Annotati
) {
entry.manualChanges.pop();
entry.reason = null;
filtered.push(entry);
redactionLogEntries.push(entry);
}
return filtered;
}, []);
}
const annotations = await this.#buildAnnotations(redactionLogCopy, file);
redactionLogCopy.redactionLogEntry = redactionLogEntries;
const annotations = await this.#convertData(redactionLogCopy, file);
this._suggestionsService.removedRedactions = annotations.filter(a => !a.isSkipped);
}
@ -248,49 +226,58 @@ export class FileDataService extends EntitiesService<AnnotationWrapper, Annotati
return [];
}
const result: RedactionLogEntry[] = [];
const sourceIdAnnotationIds: { [key: string]: RedactionLogEntry[] } = {};
const annotations: AnnotationWrapper[] = [];
const sourceIds: { [key: string]: AnnotationWrapper[] } = {};
const dictionaries = this._state.dictionaries;
const defaultColors = this._defaultColorsService.find(this._state.dossierTemplateId);
let checkDictionary = true;
for (const redactionLogEntry of redactionLog.redactionLogEntry) {
const changeLogValues = this.#getChangeLogValues(redactionLogEntry, file);
if (changeLogValues.hidden) {
continue;
}
let dictionary = dictionaries.find(dict => dict.type === redactionLogEntry.type);
for (const entry of redactionLog.redactionLogEntry) {
let dictionary = dictionaries.find(dict => dict.type === entry.type);
if (!dictionary && checkDictionary) {
const dictionaryRequest = this._dictionaryService.loadDictionaryDataForDossierTemplate(this._state.dossierTemplateId);
await firstValueFrom(dictionaryRequest);
checkDictionary = false;
dictionary = dictionaries.find(dict => dict.type === redactionLogEntry.type);
dictionary = dictionaries.find(dict => dict.type === entry.type);
}
if (!dictionary) {
this.missingTypes.add(redactionLogEntry.type);
this.missingTypes.add(entry.type);
continue;
}
const redactionLogEntryWrapper: RedactionLogEntry = new RedactionLogEntry(
redactionLogEntry,
const pageNumber = entry.positions[0]?.page;
const manual = entry.manualChanges?.length > 0;
if (!manual && file.excludedPages.includes(pageNumber)) {
continue;
}
const changeLogValues = this.#getChangeLogValues(entry, file);
if (changeLogValues.hidden) {
continue;
}
const annotation = AnnotationWrapper.fromData(
entry,
dictionaries,
defaultColors,
changeLogValues.changeLogType,
redactionLog.legalBasis ?? [],
!!dictionary?.hint,
);
if (redactionLogEntry.sourceId) {
if (!sourceIdAnnotationIds[redactionLogEntry.sourceId]) {
sourceIdAnnotationIds[redactionLogEntry.sourceId] = [];
if (entry.sourceId) {
if (!sourceIds[entry.sourceId]) {
sourceIds[entry.sourceId] = [];
}
sourceIdAnnotationIds[redactionLogEntry.sourceId].push(redactionLogEntryWrapper);
sourceIds[entry.sourceId].push(annotation);
}
result.push(redactionLogEntryWrapper);
annotations.push(annotation);
}
const sourceKeys = Object.keys(sourceIdAnnotationIds);
return result.filter(r => !sourceKeys.includes(r.id));
const sourceKeys = Object.keys(sourceIds);
return annotations.filter(r => !sourceKeys.includes(r.id));
}
#getChangeLogValues(
@ -309,7 +296,7 @@ export class FileDataService extends EntitiesService<AnnotationWrapper, Annotati
}
const viableChanges = redactionLogEntry.changes?.filter(c => c.analysisNumber > 1);
const lastChange = viableChanges.sort(chronologicallyBy(x => x.dateTime)).at(-1);
const lastChange = viableChanges?.sort(chronologicallyBy(x => x.dateTime)).at(-1);
const page = redactionLogEntry.positions?.[0].page;
const viewedPage = this.#originalViewedPages.find(p => p.page === page);

View File

@ -67,7 +67,7 @@ export class ManualRedactionService extends GenericService<IManualAddResponse> {
addRecommendation(annotations: AnnotationWrapper[], dossierId: string, fileId: string, comment = { text: 'Accepted Recommendation' }) {
const recommendations: List<IAddRedactionRequest> = annotations.map(annotation => ({
addToDictionary: true,
sourceId: annotation.annotationId,
sourceId: annotation.id,
value: annotation.value,
reason: annotation.legalBasis ?? 'Dictionary Request',
positions: annotation.positions,

View File

@ -146,13 +146,13 @@ export class AnnotationDrawService {
annotation.ReadOnly = true;
const isOCR = annotationWrapper.isOCR && !annotationWrapper.isSuggestionResize;
if (isOCR && !this._annotationManager.isHidden(annotationWrapper.annotationId)) {
this._annotationManager.addToHidden(annotationWrapper.annotationId);
if (isOCR && !this._annotationManager.isHidden(annotationWrapper.id)) {
this._annotationManager.addToHidden(annotationWrapper.id);
}
annotation.Hidden =
annotationWrapper.isChangeLogRemoved ||
(hideSkipped && annotationWrapper.isSkipped) ||
this._annotationManager.isHidden(annotationWrapper.annotationId);
this._annotationManager.isHidden(annotationWrapper.id);
annotation.setCustomData('redact-manager', 'true');
annotation.setCustomData('redaction', String(annotationWrapper.previewAnnotation));
annotation.setCustomData('suggestion', String(annotationWrapper.isSuggestion));

View File

@ -14,7 +14,7 @@ export function stopAndPreventIfNotAllowed($event: KeyboardEvent) {
}
export function getId(item: string | AnnotationWrapper) {
return typeof item === 'string' ? item : item.annotationId;
return typeof item === 'string' ? item : item.id;
}
export function getIds(items?: List | List<AnnotationWrapper>): List | undefined {

View File

@ -1,7 +1,5 @@
import { ChangeDetectorRef, Component, EventEmitter, Input, OnChanges, Output, SimpleChanges, ViewChild } from '@angular/core';
import { Debounce, IconButtonTypes, List, LoadingService } from '@iqser/common-ui';
import { firstValueFrom, Observable, of } from 'rxjs';
import { catchError, map, take, tap } from 'rxjs/operators';
import { Dictionary, DICTIONARY_TYPE_KEY_MAP, DictionaryType, Dossier, DossierTemplate } from '@red/domain';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { DictionaryService } from '@services/entity-services/dictionary.service';
@ -50,9 +48,10 @@ export class DictionaryManagerComponent implements OnChanges {
} as Dictionary;
selectDossierTemplate = { name: _('dictionary-overview.compare.select-dossier-template') } as DossierTemplate;
compare: false;
dictionaries: List<Dictionary> = this._dictionaries;
dictionaries: List<Dictionary> = this.#dictionaries;
private _searchDecorations: string[] = [];
readonly #currentTab: string = window.location.href.split('/').pop();
readonly #currentTab = window.location.href.split('/').pop();
#compareDictionary = this.selectDictionary;
constructor(
private readonly _dictionaryService: DictionaryService,
@ -71,8 +70,8 @@ export class DictionaryManagerComponent implements OnChanges {
set dossierTemplate(value: DossierTemplate) {
this._dossierTemplate = value;
this.dictionaries = this._dictionaries;
this._compareDictionary = this.selectDictionary;
this.dictionaries = this.#dictionaries;
this.#compareDictionary = this.selectDictionary;
this.showDiffEditor = false;
}
@ -91,24 +90,19 @@ export class DictionaryManagerComponent implements OnChanges {
return;
}
this._onDossierChanged(dossier.dossierTemplateId, dossier.id)
.pipe(take(1))
// eslint-disable-next-line rxjs/no-ignored-subscription
.subscribe(entries => {
this.diffEditorText = entries;
this.showDiffEditor = true;
});
this.#onDossierChanged(dossier.dossierTemplateId, dossier.id).then(entries => {
this.diffEditorText = entries;
this.showDiffEditor = true;
});
}
private _compareDictionary = this.selectDictionary;
get compareDictionary() {
return this._compareDictionary;
return this.#compareDictionary;
}
set compareDictionary(dictionary: Dictionary) {
this._loadingService.start();
this._compareDictionary = dictionary;
this.#compareDictionary = dictionary;
if (dictionary.label === this.selectDictionary.label) {
this.showDiffEditor = false;
@ -116,38 +110,25 @@ export class DictionaryManagerComponent implements OnChanges {
return;
}
const entries: List =
this._compareDictionary.getEntries(this.type) ??
this._dictionariesMapService.get(this._compareDictionary.dossierTemplateId, this._compareDictionary.type).getEntries(this.type);
this.#compareDictionary.getEntries(this.type) ??
this._dictionariesMapService.get(this.#compareDictionary.dossierTemplateId, this.#compareDictionary.type).getEntries(this.type);
if (entries.length) {
this.diffEditorText = this._toString([...entries]);
this.diffEditorText = this.#toString([...entries]);
this.showDiffEditor = true;
return;
}
firstValueFrom(
this._dictionaryService.getForType(this._compareDictionary.dossierTemplateId, this._compareDictionary.type).pipe(
tap(values => {
this._compareDictionary.setEntries([...values[DICTIONARY_TYPE_KEY_MAP[this.type]]] ?? [], this.type);
}),
catchError(() => {
this._compareDictionary.setEntries([], this.type);
return of({});
}),
),
).then(() => {
this.diffEditorText = this._toString([...(this._compareDictionary.getEntries(this.type) as string[])]);
this.showDiffEditor = true;
this._changeRef.markForCheck();
this._loadingService.stop();
});
}
get _dictionaries(): Dictionary[] {
if (!this._dossierTemplate || this._dossierTemplate.name === this.selectDossierTemplate.name) {
return;
}
return this._dictionariesMapService.get(this.dossierTemplate?.dossierTemplateId).filter(dict => !dict.virtual);
this._dictionaryService
.getForType(this.#compareDictionary.dossierTemplateId, this.#compareDictionary.type)
.then(values => {
this.#compareDictionary.setEntries([...values[DICTIONARY_TYPE_KEY_MAP[this.type]]] ?? [], this.type);
this.diffEditorText = this.#toString([...(this.#compareDictionary.getEntries(this.type) as string[])]);
this.showDiffEditor = true;
this._changeRef.markForCheck();
this._loadingService.stop();
})
.catch(() => this.#compareDictionary.setEntries([], this.type));
}
get dossierTemplateIsNotSelected() {
@ -156,7 +137,7 @@ export class DictionaryManagerComponent implements OnChanges {
get optionNotSelected() {
if (this.filterByDossierTemplate) {
return this.selectDictionary.label === this._compareDictionary.label;
return this.selectDictionary.label === this.#compareDictionary.label;
}
return this.dossier.dossierName === this.selectDossier.dossierName;
}
@ -165,6 +146,13 @@ export class DictionaryManagerComponent implements OnChanges {
return HELP_MODE_KEYS[this.#currentTab];
}
get #dictionaries(): Dictionary[] {
if (!this._dossierTemplate || this._dossierTemplate.name === this.selectDossierTemplate.name) {
return;
}
return this._dictionariesMapService.get(this.dossierTemplate?.dossierTemplateId).filter(dict => !dict.virtual);
}
download(): void {
const content = this.editor.currentEntries.join('\n');
const blob = new Blob([content], {
@ -236,12 +224,12 @@ export class DictionaryManagerComponent implements OnChanges {
this.editor.codeEditor.revealLineInCenter(range.startLineNumber, SMOOTH_SCROLL);
}
private _onDossierChanged(dossierTemplateId: string, dossierId?: string, type = 'dossier_redaction'): Observable<string> {
const dictionary$ = this._dictionaryService.getForType(dossierTemplateId, type, dossierId);
return dictionary$.pipe(map(data => this._toString([...data.entries])));
async #onDossierChanged(dossierTemplateId: string, dossierId?: string, type = 'dossier_redaction') {
const dictionary = await this._dictionaryService.getForType(dossierTemplateId, type, dossierId);
return this.#toString([...dictionary.entries]);
}
private _toString(entries: string[]): string {
#toString(entries: string[]): string {
entries.sort((a, b) => a.localeCompare(b, undefined, { sensitivity: 'accent' }));
return entries.join('\n');
}

View File

@ -32,9 +32,9 @@ export class DictionaryService extends EntitiesService<IDictionary, Dictionary>
* Retrieves all dictionary entries of an entry type
*/
getForType(dossierTemplateId: string, type: string, dossierId?: string): Observable<IDictionary> {
getForType(dossierTemplateId: string, type: string, dossierId?: string) {
const queryParams = dossierId ? [{ key: 'dossierId', value: dossierId }] : undefined;
return this._getOne([type, dossierTemplateId], this._defaultModelPath, queryParams);
return firstValueFrom(this._getOne([type, dossierTemplateId], this._defaultModelPath, queryParams));
}
/**
@ -181,7 +181,7 @@ export class DictionaryService extends EntitiesService<IDictionary, Dictionary>
}
async loadDossierDictionary(dossierTemplateId: string, dossierId: string): Promise<Dictionary> {
const promise = firstValueFrom(this.getForType(dossierTemplateId, 'dossier_redaction', dossierId));
const promise = this.getForType(dossierTemplateId, 'dossier_redaction', dossierId);
const dict = await promise.catch(() => undefined);
if (dict) {
const dictionary = new Dictionary({

View File

@ -2,7 +2,7 @@ import { inject, Injectable } from '@angular/core';
import { GenericService, Toaster } from '@iqser/common-ui';
import { Earmark, EarmarkOperation, EarmarkResponse } from '@red/domain';
import { catchError, map, tap } from 'rxjs/operators';
import { Observable, of } from 'rxjs';
import { firstValueFrom, of } from 'rxjs';
import { AnnotationWrapper } from '@models/file/annotation.wrapper';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
@ -13,13 +13,15 @@ export class EarmarksService extends GenericService<EarmarkResponse> {
protected readonly _defaultModelPath = '';
readonly #toaster = inject(Toaster);
getEarmarks(dossierId: string, fileId: string): Observable<AnnotationWrapper[]> {
return this._http.get<{ highlights: Earmark[] }>(`/${this.#getPath(dossierId, fileId)}`).pipe(
getEarmarks(dossierId: string, fileId: string) {
const request = this._http.get<{ highlights: Earmark[] }>(`/${this.#getPath(dossierId, fileId)}`).pipe(
map(response => response.highlights),
map(highlights => highlights.map(highlight => AnnotationWrapper.fromEarmark(highlight))),
map(highlights => highlights.sort((h1, h2) => h1.color.localeCompare(h2.color))),
catchError(() => of([])),
);
return firstValueFrom(request);
}
performHighlightsAction(ids: string[], dossierId: string, fileId: string, operation: EarmarkOperation) {