RED-4640: optimize earmarks
This commit is contained in:
parent
c9ac77e195
commit
56956ab50e
@ -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;
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 }"
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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),
|
||||
);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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));
|
||||
}
|
||||
});
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -44,7 +44,7 @@ export class ViewModeService {
|
||||
return this.#viewMode$.value === 'REDACTED';
|
||||
}
|
||||
|
||||
get isTextHighlights() {
|
||||
get isEarmarks() {
|
||||
return this.#viewMode$.value === 'TEXT_HIGHLIGHTS';
|
||||
}
|
||||
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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];
|
||||
}
|
||||
|
||||
@ -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 } })),
|
||||
@ -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'),
|
||||
},
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
export interface TextHighlightsGroup {
|
||||
export interface EarmarkGroup {
|
||||
startIdx: number;
|
||||
color: string;
|
||||
length: number;
|
||||
@ -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',
|
||||
}
|
||||
8
libs/red-domain/src/lib/earmarks/earmark.request.ts
Normal file
8
libs/red-domain/src/lib/earmarks/earmark.request.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { EarmarkOperation } from './earmark-operation';
|
||||
|
||||
export interface EarmarkRequest {
|
||||
dossierId: string;
|
||||
fileId: string;
|
||||
operation: EarmarkOperation;
|
||||
colors?: string[];
|
||||
}
|
||||
9
libs/red-domain/src/lib/earmarks/earmark.response.ts
Normal file
9
libs/red-domain/src/lib/earmarks/earmark.response.ts
Normal 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[]>;
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
import { IRectangle } from '../geometry';
|
||||
|
||||
export interface Highlight {
|
||||
export interface Earmark {
|
||||
readonly id: string;
|
||||
readonly positions: IRectangle[];
|
||||
readonly hexColor: string;
|
||||
5
libs/red-domain/src/lib/earmarks/index.ts
Normal file
5
libs/red-domain/src/lib/earmarks/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export * from './earmark';
|
||||
export * from './earmark-operation';
|
||||
export * from './earmark.response';
|
||||
export * from './earmark.request';
|
||||
export * from './earmark-group';
|
||||
@ -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';
|
||||
@ -1,8 +0,0 @@
|
||||
import { TextHighlightOperation } from './text-highlight-operation';
|
||||
|
||||
export interface TextHighlightRequest {
|
||||
dossierId: string;
|
||||
fileId: string;
|
||||
operation: TextHighlightOperation;
|
||||
colors?: string[];
|
||||
}
|
||||
@ -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[]>;
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user