RED-4640: optimize earmarks

This commit is contained in:
Dan Percic 2022-08-16 19:15:32 +03:00
parent c9ac77e195
commit 56956ab50e
29 changed files with 183 additions and 133 deletions

View File

@ -6,8 +6,8 @@ import {
AnnotationIconType,
DefaultColors,
Dictionary,
Earmark,
FalsePositiveSuperTypes,
Highlight,
IComment,
IManualChange,
IPoint,
@ -124,7 +124,7 @@ export class AnnotationWrapper implements IListable, Record<string, unknown> {
}
get filterKey() {
if (this.isHighlight) {
if (this.isEarmark) {
return this.color;
}
@ -147,7 +147,7 @@ export class AnnotationWrapper implements IListable, Record<string, unknown> {
return this.superType === SuperTypes.Hint;
}
get isHighlight() {
get isEarmark() {
return this.superType === SuperTypes.TextHighlight;
}
@ -256,17 +256,17 @@ export class AnnotationWrapper implements IListable, Record<string, unknown> {
);
}
static fromHighlight(highlight: Highlight) {
static fromEarmark(earmark: Earmark) {
const annotationWrapper = new AnnotationWrapper();
annotationWrapper.annotationId = highlight.id;
annotationWrapper.pageNumber = highlight.positions[0].page;
annotationWrapper.annotationId = earmark.id;
annotationWrapper.pageNumber = earmark.positions[0].page;
annotationWrapper.superType = SuperTypes.TextHighlight;
annotationWrapper.typeValue = SuperTypes.TextHighlight;
annotationWrapper.value = 'Imported';
annotationWrapper.color = highlight.hexColor;
annotationWrapper.positions = highlight.positions;
annotationWrapper.firstTopLeftPoint = highlight.positions[0]?.topLeft;
annotationWrapper.color = earmark.hexColor;
annotationWrapper.positions = earmark.positions;
annotationWrapper.firstTopLeftPoint = earmark.positions[0]?.topLeft;
annotationWrapper.typeLabel = annotationTypesTranslations[annotationWrapper.superType];
return annotationWrapper;

View File

@ -1,7 +1,7 @@
<div class="details">
<redaction-annotation-icon
[color]="annotation.color"
[label]="annotation.isHighlight ? '' : annotation.superType[0].toUpperCase()"
[label]="annotation.isEarmark ? '' : annotation.superType[0].toUpperCase()"
[type]="annotation.iconShape"
class="mt-6 mr-10"
></redaction-annotation-icon>
@ -24,10 +24,10 @@
<div *ngIf="annotation.shortContent && !annotation.isHint">
<strong><span translate="content"></span>: </strong>{{ annotation.shortContent }}
</div>
<div *ngIf="annotation.isHighlight">
<div *ngIf="annotation.isEarmark">
<strong><span translate="color"></span>: </strong>{{ annotation.color }}
</div>
<div *ngIf="annotation.isHighlight">
<div *ngIf="annotation.isEarmark">
<strong><span translate="size"></span>: </strong>{{ annotation.width }}x{{ annotation.height }} px
</div>
</div>

View File

@ -8,7 +8,7 @@
matTooltipPosition="above"
></redaction-annotation-card>
<div *ngIf="!annotation.isHighlight" class="actions-wrapper">
<div *ngIf="!annotation.isEarmark" class="actions-wrapper">
<div
(click)="comments.toggleExpandComments($event)"
[matTooltip]="'comments.comments' | translate: { count: annotation.comments?.length }"

View File

@ -16,7 +16,7 @@ import { AnnotationReferencesService } from '../../services/annotation-reference
import { UserPreferenceService } from '@users/user-preference.service';
import { ViewModeService } from '../../services/view-mode.service';
import { BehaviorSubject } from 'rxjs';
import { TextHighlightsGroup } from '@red/domain';
import { EarmarkGroup } from '@red/domain';
import { REDAnnotationManager } from '../../../pdf-viewer/services/annotation-manager.service';
import { AnnotationsListingService } from '../../services/annotations-listing.service';
@ -32,7 +32,7 @@ export class AnnotationsListComponent extends HasScrollbarDirective implements O
@Output() readonly pagesPanelActive = new EventEmitter<boolean>();
readonly highlightGroups$ = new BehaviorSubject<TextHighlightsGroup[]>([]);
readonly earmarkGroups$ = new BehaviorSubject<EarmarkGroup[]>([]);
constructor(
protected readonly _elementRef: ElementRef,
@ -49,8 +49,8 @@ export class AnnotationsListComponent extends HasScrollbarDirective implements O
}
ngOnChanges(): void {
if (this._viewModeService.isTextHighlights) {
this._updateHighlightGroups();
if (this._viewModeService.isEarmarks) {
this._updateEarmarksGroups();
}
setTimeout(() => {
@ -88,27 +88,27 @@ export class AnnotationsListComponent extends HasScrollbarDirective implements O
}
}
showHighlightGroup(idx: number): TextHighlightsGroup {
return this._viewModeService.isTextHighlights && this.highlightGroups$.value.find(h => h.startIdx === idx);
showHighlightGroup(idx: number): EarmarkGroup {
return this._viewModeService.isEarmarks && this.earmarkGroups$.value.find(h => h.startIdx === idx);
}
private _updateHighlightGroups(): void {
private _updateEarmarksGroups(): void {
if (!this.annotations?.length) {
return;
}
const highlightGroups: TextHighlightsGroup[] = [];
let lastGroup: TextHighlightsGroup;
const earmarksGroups: EarmarkGroup[] = [];
let lastGroup: EarmarkGroup;
for (let idx = 0; idx < this.annotations.length; ++idx) {
if (idx === 0 || this.annotations[idx].color !== this.annotations[idx - 1].color) {
if (lastGroup) {
highlightGroups.push(lastGroup);
earmarksGroups.push(lastGroup);
}
lastGroup = { startIdx: idx, length: 1, color: this.annotations[idx].color };
} else {
lastGroup.length += 1;
}
}
highlightGroups.push(lastGroup);
this.highlightGroups$.next(highlightGroups);
earmarksGroups.push(lastGroup);
this.earmarkGroups$.next(earmarksGroups);
}
}

View File

@ -116,7 +116,7 @@
<div class="content">
<div
*ngIf="(isHighlights$ | async) === false"
*ngIf="(isEarmarks$ | async) === false"
[attr.anotation-page-header]="activeViewerPage"
[hidden]="excludedPagesService.shown$ | async"
class="workload-separator"

View File

@ -64,7 +64,7 @@ export class FileWorkloadComponent extends AutoUnsubscribe implements OnDestroy
readonly multiSelectInactive$: Observable<boolean>;
readonly showExcludedPages$: Observable<boolean>;
readonly title$: Observable<string>;
readonly isHighlights$: Observable<boolean>;
readonly isEarmarks$: Observable<boolean>;
@ViewChild('annotationsElement') private readonly _annotationsElement: ElementRef;
@ViewChild('quickNavigation') private readonly _quickNavigationElement: ElementRef;
@ -105,7 +105,7 @@ export class FileWorkloadComponent extends AutoUnsubscribe implements OnDestroy
this.displayedAnnotations$ = this._displayedAnnotations$;
this.multiSelectInactive$ = this._multiSelectInactive$;
this.showExcludedPages$ = this._showExcludedPages$;
this.isHighlights$ = this._isHighlights$;
this.isEarmarks$ = this._isHighlights$;
this.title$ = this._title$;
}
@ -129,7 +129,7 @@ export class FileWorkloadComponent extends AutoUnsubscribe implements OnDestroy
}
private get _title$(): Observable<string> {
return this.isHighlights$.pipe(
return this.isEarmarks$.pipe(
map(isHighlights => (isHighlights ? _('file-preview.tabs.highlights.label') : _('file-preview.tabs.annotations.label'))),
);
}
@ -137,7 +137,7 @@ export class FileWorkloadComponent extends AutoUnsubscribe implements OnDestroy
private get _isHighlights$(): Observable<boolean> {
return this.viewModeService.viewMode$.pipe(
tap(() => this._scrollViews()),
map(() => this.viewModeService.isTextHighlights),
map(() => this.viewModeService.isEarmarks),
);
}

View File

@ -1,6 +1,6 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { CircleButtonTypes } from '@iqser/common-ui';
import { TextHighlightOperation, TextHighlightsGroup } from '@red/domain';
import { EarmarkGroup, EarmarkOperation } from '@red/domain';
import { FilePreviewStateService } from '../../services/file-preview-state.service';
import { AnnotationWrapper } from '@models/file/annotation.wrapper';
import { FilePreviewDialogService } from '../../services/file-preview-dialog.service';
@ -14,7 +14,7 @@ import { MultiSelectService } from '../../services/multi-select.service';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class HighlightsSeparatorComponent {
@Input() highlightGroup: TextHighlightsGroup;
@Input() highlightGroup: EarmarkGroup;
@Input() annotation: AnnotationWrapper;
readonly circleButtonTypes = CircleButtonTypes;
@ -28,17 +28,17 @@ export class HighlightsSeparatorComponent {
private readonly _multiSelectService: MultiSelectService,
) {}
convertHighlights(highlightGroup: TextHighlightsGroup): void {
const data = this._getActionData(highlightGroup, TextHighlightOperation.CONVERT);
convertHighlights(highlightGroup: EarmarkGroup): void {
const data = this._getActionData(highlightGroup, EarmarkOperation.CONVERT);
this._dialogService.openDialog('highlightAction', null, data);
}
removeHighlights(highlightGroup: TextHighlightsGroup): void {
const data = this._getActionData(highlightGroup, TextHighlightOperation.REMOVE);
removeHighlights(highlightGroup: EarmarkGroup): void {
const data = this._getActionData(highlightGroup, EarmarkOperation.REMOVE);
this._dialogService.openDialog('highlightAction', null, data);
}
private _getActionData(highlightGroup: TextHighlightsGroup, operation: TextHighlightOperation) {
private _getActionData(highlightGroup: EarmarkGroup, operation: EarmarkOperation) {
const highlights = this._fileDataService.all.filter(a => a.color === highlightGroup.color);
return {
dossierId: this._state.dossierId,

View File

@ -34,7 +34,7 @@
<button
(click)="switchView.emit(viewModes.TEXT_HIGHLIGHTS)"
[class.active]="viewMode === viewModes.TEXT_HIGHLIGHTS"
[disabled]="(canSwitchToHighlightsView$ | async) === false"
[disabled]="(canSwitchToEarmarksView$ | async) === false"
[iqserHelpMode]="'views'"
[matTooltip]="'file-preview.text-highlights-tooltip' | translate"
class="red-tab"

View File

@ -18,7 +18,7 @@ export class ViewSwitchComponent {
readonly canSwitchToDeltaView$: Observable<boolean>;
readonly canSwitchToRedactedView$: Observable<boolean>;
readonly canSwitchToHighlightsView$: Observable<boolean>;
readonly canSwitchToEarmarksView$: Observable<boolean>;
constructor(
readonly viewModeService: ViewModeService,
@ -31,7 +31,7 @@ export class ViewSwitchComponent {
this.canSwitchToRedactedView$ = _stateService.file$.pipe(map(file => !file.analysisRequired && !file.excluded));
this.canSwitchToHighlightsView$ = _stateService.file$.pipe(
this.canSwitchToEarmarksView$ = _stateService.file$.pipe(
map(file => file.hasHighlights && !file.analysisRequired && !file.excluded),
);
}

View File

@ -1,16 +1,15 @@
import { Component, Inject } from '@angular/core';
import { UntypedFormGroup, Validators } from '@angular/forms';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { TextHighlightOperation, TextHighlightOperationPages } from '@red/domain';
import { EarmarkOperation, EarmarkOperationPages } from '@red/domain';
import { BaseDialogComponent, DetailsRadioOption } from '@iqser/common-ui';
import { TextHighlightService } from '@services/files/text-highlight.service';
import { EarmarksService } from '@services/files/earmarks.service';
import { firstValueFrom } from 'rxjs';
import { AnnotationWrapper } from '@models/file/annotation.wrapper';
import { highlightsTranslations } from '@translations/highlights-translations';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
export interface HighlightActionData {
readonly operation: TextHighlightOperation;
readonly operation: EarmarkOperation;
readonly color: string;
readonly dossierId: string;
readonly fileId: string;
@ -26,22 +25,22 @@ export class HighlightActionDialogComponent extends BaseDialogComponent {
readonly translations = highlightsTranslations;
readonly #operation = this.data.operation;
readonly options: DetailsRadioOption<TextHighlightOperationPages>[] = [
readonly options: DetailsRadioOption<EarmarkOperationPages>[] = [
{
label: highlightsTranslations[this.#operation].options[TextHighlightOperationPages.THIS_PAGE].label,
value: TextHighlightOperationPages.THIS_PAGE,
description: highlightsTranslations[this.#operation].options[TextHighlightOperationPages.THIS_PAGE].description,
label: highlightsTranslations[this.#operation].options[EarmarkOperationPages.THIS_PAGE].label,
value: EarmarkOperationPages.THIS_PAGE,
description: highlightsTranslations[this.#operation].options[EarmarkOperationPages.THIS_PAGE].description,
},
{
label: highlightsTranslations[this.#operation].options[TextHighlightOperationPages.ALL_PAGES].label,
value: TextHighlightOperationPages.ALL_PAGES,
description: highlightsTranslations[this.#operation].options[TextHighlightOperationPages.ALL_PAGES].description,
label: highlightsTranslations[this.#operation].options[EarmarkOperationPages.ALL_PAGES].label,
value: EarmarkOperationPages.ALL_PAGES,
description: highlightsTranslations[this.#operation].options[EarmarkOperationPages.ALL_PAGES].description,
},
];
constructor(
protected readonly _dialogRef: MatDialogRef<HighlightActionDialogComponent>,
private readonly _textHighlightService: TextHighlightService,
private readonly _textHighlightService: EarmarksService,
@Inject(MAT_DIALOG_DATA) readonly data: HighlightActionData,
) {
super(_dialogRef);
@ -55,7 +54,7 @@ export class HighlightActionDialogComponent extends BaseDialogComponent {
// !color means we are in bulk select mode, so we don't need to apply additional page filters
const filteredHighlights =
!color || this.form.get('option').value.value === TextHighlightOperationPages.ALL_PAGES
!color || this.form.get('option').value.value === EarmarkOperationPages.ALL_PAGES
? highlights
: highlights.filter(h => h.pageNumber === pageNumber);

View File

@ -27,7 +27,7 @@ import { UserPreferenceService } from '@users/user-preference.service';
import { byId, byPage, download, handleFilterDelta, hasChanges } from '../../utils';
import { FilesService } from '@services/files/files.service';
import { FileManagementService } from '@services/files/file-management.service';
import { catchError, filter, map, startWith, switchMap, tap } from 'rxjs/operators';
import { catchError, filter, map, startWith, switchMap, tap, withLatestFrom } from 'rxjs/operators';
import { FilesMapService } from '@services/files/files-map.service';
import { ViewModeService } from './services/view-mode.service';
import { ReanalysisService } from '@services/reanalysis.service';
@ -135,6 +135,42 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
);
}
get #earmarks$() {
const isEarmarksViewMode$ = this._viewModeService.viewMode$.pipe(filter(() => this._viewModeService.isEarmarks));
const earmarks$ = isEarmarksViewMode$.pipe(
tap(() => this._loadingService.start()),
switchMap(() => this._fileDataService.loadEarmarks()),
tap(() => this.updateViewMode().then(() => this._loadingService.stop())),
);
const currentPageIfEarmarksView$ = combineLatest([this.pdf.currentPage$, this._viewModeService.viewMode$]).pipe(
filter(() => this._viewModeService.isEarmarks),
map(([page]) => page),
);
const currentPageEarmarks$ = combineLatest([currentPageIfEarmarksView$, earmarks$]).pipe(
map(([page, earmarks]) => earmarks.get(page)),
);
return currentPageEarmarks$.pipe(
map(earmarks => [earmarks, this._skippedService.hideSkipped, this.state.dossierTemplateId] as const),
switchMap(args => this._annotationDrawService.draw(...args)),
);
}
deleteEarmarksOnViewChange$() {
const isChangingFromEarmarksViewMode$ = this._viewModeService.viewMode$.pipe(
pairwise(),
filter(([oldViewMode]) => oldViewMode === ViewModes.TEXT_HIGHLIGHTS),
);
return isChangingFromEarmarksViewMode$.pipe(
withLatestFrom(this._fileDataService.earmarks$),
map(([, earmarks]) => this.deleteAnnotations(earmarks.get(this.pdf.currentPage) ?? [], [])),
);
}
async save() {
await this._pageRotationService.applyRotation();
this._viewerHeaderService.disable(ROTATION_ACTION_BUTTONS);
@ -147,7 +183,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
const redactions = annotations.filter(a => bool(a.getCustomData('redaction')));
switch (this._viewModeService.viewMode) {
case 'STANDARD': {
case ViewModes.STANDARD: {
this._setAnnotationsColor(redactions, 'annotationColor');
const wrappers = await this._fileDataService.annotations;
const ocrAnnotationIds = wrappers.filter(a => a.isOCR).map(a => a.id);
@ -160,7 +196,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
this._annotationManager.hide(nonStandardEntries);
break;
}
case 'DELTA': {
case ViewModes.DELTA: {
const changeLogEntries = annotations.filter(a => bool(a.getCustomData('changeLog')));
const nonChangeLogEntries = annotations.filter(a => !bool(a.getCustomData('changeLog')));
this._setAnnotationsColor(redactions, 'annotationColor');
@ -169,7 +205,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
this._annotationManager.hide(nonChangeLogEntries);
break;
}
case 'REDACTED': {
case ViewModes.REDACTED: {
const nonRedactionEntries = annotations.filter(a => !bool(a.getCustomData('redaction')));
this._setAnnotationsOpacity(redactions);
this._setAnnotationsColor(redactions, 'redactionColor');
@ -177,13 +213,6 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
this._annotationManager.hide(nonRedactionEntries);
break;
}
case 'TEXT_HIGHLIGHTS': {
this._loadingService.start();
this._annotationManager.hide(annotations);
const highlights = await this._fileDataService.loadTextHighlights();
await this._annotationDrawService.draw(highlights, this._skippedService.hideSkipped, this.state.dossierTemplateId);
this._loadingService.stop();
}
}
await this._stampService.stampPDF();
@ -426,7 +455,6 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
}
async #updateQueryParamsPage(page: number): Promise<void> {
console.log('updateQueryParamsPage: ', page);
const extras: NavigationExtras = {
queryParams: { page },
queryParamsHandling: 'merge',
@ -502,11 +530,8 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
this.addActiveScreenSubscription = combineLatest([this._viewModeService.viewMode$, this.state.file$])
.pipe(
tap(([viewMode, file]) => {
if (viewMode === ViewModes.TEXT_HIGHLIGHTS && !file.hasHighlights) {
this._viewModeService.switchToStandard();
}
}),
filter(([viewMode, file]) => viewMode === ViewModes.TEXT_HIGHLIGHTS && !file.hasHighlights),
tap(() => this._viewModeService.switchToStandard()),
)
.subscribe();
@ -519,6 +544,10 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
});
this.addActiveScreenSubscription = this.#textSelected$.subscribe();
this.addActiveScreenSubscription = this.#earmarks$.subscribe();
this.addActiveScreenSubscription = this.deleteEarmarksOnViewChange$().subscribe();
this.addActiveScreenSubscription = this.state.dossierFileChange$.subscribe();
this.addActiveScreenSubscription = this.state.blob$
@ -534,10 +563,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
});
this.addActiveScreenSubscription = this.pdfProxyService.pageChanged$.subscribe(page =>
this._ngZone.run(() => {
console.log('viewerPageChanged', page);
return this.#updateQueryParamsPage(page);
}),
this._ngZone.run(() => this.#updateQueryParamsPage(page)),
);
this.addActiveScreenSubscription = this.pdfProxyService.annotationSelected$.subscribe();
}

View File

@ -7,12 +7,12 @@ import { Core } from '@pdftron/webviewer';
import {
DictionaryEntryTypes,
Dossier,
EarmarkOperation,
IAddRedactionRequest,
ILegalBasisChangeRequest,
IRecategorizationRequest,
IRectangle,
IResizeRequest,
TextHighlightOperation,
} from '@red/domain';
import { toPosition } from '../utils/pdf-calculation.utils';
import { AnnotationDrawService } from '../../pdf-viewer/services/annotation-draw.service';
@ -62,12 +62,12 @@ export class AnnotationActionsService {
}
removeHighlights(highlights: AnnotationWrapper[]): void {
const data = this.#getHighlightOperationData(TextHighlightOperation.REMOVE, highlights);
const data = this.#getHighlightOperationData(EarmarkOperation.REMOVE, highlights);
this._dialogService.openDialog('highlightAction', null, data);
}
convertHighlights(highlights: AnnotationWrapper[]): void {
const data = this.#getHighlightOperationData(TextHighlightOperation.CONVERT, highlights);
const data = this.#getHighlightOperationData(EarmarkOperation.CONVERT, highlights);
this._dialogService.openDialog('highlightAction', null, data);
}
@ -280,7 +280,7 @@ export class AnnotationActionsService {
return annotation;
}
#getHighlightOperationData(operation: TextHighlightOperation, highlights: AnnotationWrapper[]) {
#getHighlightOperationData(operation: EarmarkOperation, highlights: AnnotationWrapper[]) {
return {
dossierId: this._state.dossierId,
fileId: this._state.fileId,

View File

@ -68,9 +68,9 @@ export class AnnotationProcessingService {
} else {
// top level filter
if (topLevelFilter) {
this._createParentFilter(a.isHighlight ? a.filterKey : a.superType, filterMap, filters, a.isHighlight, {
this._createParentFilter(a.isEarmark ? a.filterKey : a.superType, filterMap, filters, a.isEarmark, {
color$: of(a.color),
shortLabel: a.isHighlight ? '' : null,
shortLabel: a.isEarmark ? '' : null,
shape: a.iconShape,
});
} else {
@ -150,7 +150,7 @@ export class AnnotationProcessingService {
}
obj.forEach((values, page) => {
if (!values[0].isHighlight) {
if (!values[0].isEarmark) {
obj.set(page, this._sortAnnotations(values));
}
});

View File

@ -12,7 +12,7 @@ import { PermissionsService } from '@services/permissions.service';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { EntitiesService, shareLast, Toaster } from '@iqser/common-ui';
import { RedactionLogService } from '@services/files/redaction-log.service';
import { TextHighlightService } from '@services/files/text-highlight.service';
import { EarmarksService } from '@services/files/earmarks.service';
import { ViewModeService } from './view-mode.service';
import dayjs from 'dayjs';
import { NGXLogger } from 'ngx-logger';
@ -29,9 +29,10 @@ export class FileDataService extends EntitiesService<AnnotationWrapper, Annotati
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 #textHighlights$ = new BehaviorSubject<AnnotationWrapper[]>([]);
readonly #earmarks$ = new BehaviorSubject<Map<number, AnnotationWrapper[]>>(new Map());
constructor(
private readonly _state: FilePreviewStateService,
@ -42,7 +43,7 @@ export class FileDataService extends EntitiesService<AnnotationWrapper, Annotati
private readonly _dossierDictionariesMapService: DossierDictionariesMapService,
private readonly _permissionsService: PermissionsService,
private readonly _redactionLogService: RedactionLogService,
private readonly _textHighlightsService: TextHighlightService,
private readonly _earmarksService: EarmarksService,
private readonly _multiSelectService: MultiSelectService,
private readonly _filesService: FilesService,
private readonly _toaster: Toaster,
@ -51,12 +52,13 @@ export class FileDataService extends EntitiesService<AnnotationWrapper, Annotati
) {
super();
this.annotations$ = this.#annotations$;
this.earmarks$ = this.#earmarks$.asObservable();
this._viewModeService.viewMode$
.pipe(
switchMap(viewMode =>
iif(
() => viewMode === ViewModes.TEXT_HIGHLIGHTS,
this.#textHighlights$,
this.#earmarks$.pipe(map(textHighlights => ([] as AnnotationWrapper[]).concat(...textHighlights.values()))),
this.annotations$.pipe(map(annotations => this.#getVisibleAnnotations(annotations, viewMode))),
),
),
@ -85,6 +87,25 @@ export class FileDataService extends EntitiesService<AnnotationWrapper, Annotati
);
}
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)
const all = new Map(this.all.map(annotation => [annotation.id, annotation]));
const newEntities = [];
for (const entity of entities) {
const oldEntity = all.get(entity.id);
if (oldEntity && JSON.stringify(oldEntity) === JSON.stringify(entity)) {
newEntities.push(oldEntity);
} else {
newEntities.push(entity);
}
}
this._all$.next(newEntities);
}
async loadAnnotations(file: File) {
if (!file || file.isUnprocessed) {
return;
@ -105,11 +126,12 @@ export class FileDataService extends EntitiesService<AnnotationWrapper, Annotati
}
}
async loadTextHighlights(): Promise<AnnotationWrapper[]> {
const highlights = await firstValueFrom(this._textHighlightsService.getTextHighlights(this._state.dossierId, this._state.fileId));
this.#textHighlights$.next(highlights);
async loadEarmarks() {
const rawHighlights = await firstValueFrom(this._earmarksService.getEarmarks(this._state.dossierId, this._state.fileId));
const earmarks = rawHighlights.groupBy(h => h.pageNumber);
this.#earmarks$.next(earmarks);
return highlights;
return earmarks;
}
loadRedactionLog() {

View File

@ -44,7 +44,7 @@ export class ViewModeService {
return this.#viewMode$.value === 'REDACTED';
}
get isTextHighlights() {
get isEarmarks() {
return this.#viewMode$.value === 'TEXT_HIGHLIGHTS';
}

View File

@ -5,10 +5,9 @@ import { AnnotationWrapper } from '@models/file/annotation.wrapper';
import { UserPreferenceService } from '@users/user-preference.service';
import { RedactionLogService } from '@services/files/redaction-log.service';
import { IRectangle, ISectionGrid, ISectionRectangle } from '@red/domain';
import { IRectangle, ISectionGrid, ISectionRectangle, SuperTypes } from '@red/domain';
import { firstValueFrom } from 'rxjs';
import { DictionariesMapService } from '@services/entity-services/dictionaries-map.service';
import { SuperTypes } from '@red/domain';
import { PdfViewer } from './pdf-viewer.service';
import { ActivatedRoute } from '@angular/router';
import { REDAnnotationManager } from './annotation-manager.service';
@ -65,7 +64,7 @@ export class AnnotationDrawService {
private async _draw(annotationWrappers: List<AnnotationWrapper>, hideSkipped: boolean, dossierTemplateId: string) {
const totalPages = await firstValueFrom(this._pdf.totalPages$);
const annotations = annotationWrappers
.map(annotation => this._computeAnnotation(annotation, hideSkipped, totalPages, dossierTemplateId))
?.map(annotation => this._computeAnnotation(annotation, hideSkipped, totalPages, dossierTemplateId))
.filterTruthy();
const documentLoaded = await firstValueFrom(this._documentViewer.loaded$);
if (!documentLoaded) {

View File

@ -31,6 +31,10 @@ export function asList(items: AnnotationWrapper[] | AnnotationWrapper): Annotati
export function asList<T>(
items: string[] | string | AnnotationWrapper[] | AnnotationWrapper | T | T[],
): string[] | AnnotationWrapper[] | T[] {
if (!items) {
return [];
}
if (typeof items === 'string') {
return [items];
}

View File

@ -1,6 +1,6 @@
import { inject, Injectable } from '@angular/core';
import { GenericService, RequiredParam, Toaster, Validate } from '@iqser/common-ui';
import { Highlight, TextHighlightOperation, TextHighlightResponse } from '@red/domain';
import { Earmark, EarmarkOperation, EarmarkResponse } from '@red/domain';
import { catchError, map, tap } from 'rxjs/operators';
import { Observable, of } from 'rxjs';
import { AnnotationWrapper } from '@models/file/annotation.wrapper';
@ -9,15 +9,15 @@ import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
@Injectable({
providedIn: 'root',
})
export class TextHighlightService extends GenericService<TextHighlightResponse> {
export class EarmarksService extends GenericService<EarmarkResponse> {
protected readonly _defaultModelPath = '';
readonly #toaster = inject(Toaster);
@Validate()
getTextHighlights(@RequiredParam() dossierId: string, @RequiredParam() fileId: string): Observable<AnnotationWrapper[]> {
return this._http.get<{ highlights: Highlight[] }>(`/${this.#getPath(dossierId, fileId)}`).pipe(
getEarmarks(@RequiredParam() dossierId: string, @RequiredParam() fileId: string): Observable<AnnotationWrapper[]> {
return this._http.get<{ highlights: Earmark[] }>(`/${this.#getPath(dossierId, fileId)}`).pipe(
map(response => response.highlights),
map(highlights => highlights.map(highlight => AnnotationWrapper.fromHighlight(highlight))),
map(highlights => highlights.map(highlight => AnnotationWrapper.fromEarmark(highlight))),
map(highlights => highlights.sort((h1, h2) => h1.color.localeCompare(h2.color))),
catchError(() => of([])),
);
@ -28,7 +28,7 @@ export class TextHighlightService extends GenericService<TextHighlightResponse>
@RequiredParam() ids: string[],
@RequiredParam() dossierId: string,
@RequiredParam() fileId: string,
@RequiredParam() operation: TextHighlightOperation,
@RequiredParam() operation: EarmarkOperation,
) {
return this._post({ ids }, `${this.#getPath(dossierId, fileId)}/${operation}`).pipe(
tap(() => this.#toaster.success(_('highlight-action-dialog.success'), { params: { operation } })),

View File

@ -1,34 +1,34 @@
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { TextHighlightOperation, TextHighlightOperationPages } from '@red/domain';
import { EarmarkOperation, EarmarkOperationPages } from '@red/domain';
export const highlightsTranslations = {
[TextHighlightOperation.CONVERT]: {
[EarmarkOperation.CONVERT]: {
title: _('highlight-action-dialog.convert.title'),
details: _('highlight-action-dialog.convert.details'),
save: _('highlight-action-dialog.convert.save'),
confirmation: _('highlight-action-dialog.convert.confirmation'),
options: {
[TextHighlightOperationPages.ALL_PAGES]: {
[EarmarkOperationPages.ALL_PAGES]: {
description: _('highlight-action-dialog.convert.options.all-pages.description'),
label: _('highlight-action-dialog.convert.options.all-pages.label'),
},
[TextHighlightOperationPages.THIS_PAGE]: {
[EarmarkOperationPages.THIS_PAGE]: {
description: _('highlight-action-dialog.convert.options.this-page.description'),
label: _('highlight-action-dialog.convert.options.this-page.label'),
},
},
},
[TextHighlightOperation.REMOVE]: {
[EarmarkOperation.REMOVE]: {
title: _('highlight-action-dialog.remove.title'),
details: _('highlight-action-dialog.remove.details'),
save: _('highlight-action-dialog.remove.save'),
confirmation: _('highlight-action-dialog.remove.confirmation'),
options: {
[TextHighlightOperationPages.ALL_PAGES]: {
[EarmarkOperationPages.ALL_PAGES]: {
description: _('highlight-action-dialog.remove.options.all-pages.description'),
label: _('highlight-action-dialog.remove.options.all-pages.label'),
},
[TextHighlightOperationPages.THIS_PAGE]: {
[EarmarkOperationPages.THIS_PAGE]: {
description: _('highlight-action-dialog.remove.options.this-page.description'),
label: _('highlight-action-dialog.remove.options.this-page.label'),
},

View File

@ -22,7 +22,7 @@ export * from './lib/legal-basis';
export * from './lib/dossier-stats';
export * from './lib/dossier-state';
export * from './lib/trash';
export * from './lib/text-highlight';
export * from './lib/earmarks';
export * from './lib/permissions';
export * from './lib/license';
export * from './lib/digital-signature';

View File

@ -1,4 +1,4 @@
export interface TextHighlightsGroup {
export interface EarmarkGroup {
startIdx: number;
color: string;
length: number;

View File

@ -1,9 +1,9 @@
export enum TextHighlightOperation {
export enum EarmarkOperation {
REMOVE = 'delete',
CONVERT = 'convert',
}
export enum TextHighlightOperationPages {
export enum EarmarkOperationPages {
ALL_PAGES = 'ALL_PAGES',
THIS_PAGE = 'THIS_PAGE',
}

View File

@ -0,0 +1,8 @@
import { EarmarkOperation } from './earmark-operation';
export interface EarmarkRequest {
dossierId: string;
fileId: string;
operation: EarmarkOperation;
colors?: string[];
}

View File

@ -0,0 +1,9 @@
import { Earmark } from './earmark';
import { EarmarkOperation } from './earmark-operation';
export interface EarmarkResponse {
dossierId?: string;
fileId?: string;
operation?: EarmarkOperation;
redactionPerColor?: Record<string, Earmark[]>;
}

View File

@ -1,6 +1,6 @@
import { IRectangle } from '../geometry';
export interface Highlight {
export interface Earmark {
readonly id: string;
readonly positions: IRectangle[];
readonly hexColor: string;

View File

@ -0,0 +1,5 @@
export * from './earmark';
export * from './earmark-operation';
export * from './earmark.response';
export * from './earmark.request';
export * from './earmark-group';

View File

@ -1,5 +0,0 @@
export * from './highlight';
export * from './text-highlight-operation';
export * from './text-highlight.response';
export * from './text-highlight.request';
export * from './text-highlights-group';

View File

@ -1,8 +0,0 @@
import { TextHighlightOperation } from './text-highlight-operation';
export interface TextHighlightRequest {
dossierId: string;
fileId: string;
operation: TextHighlightOperation;
colors?: string[];
}

View File

@ -1,9 +0,0 @@
import { Highlight } from './highlight';
import { TextHighlightOperation } from './text-highlight-operation';
export interface TextHighlightResponse {
dossierId?: string;
fileId?: string;
operation?: TextHighlightOperation;
redactionPerColor?: Record<string, Highlight[]>;
}