diff --git a/apps/red-ui/src/app/modules/file-preview/components/annotation-actions/annotation-actions.component.html b/apps/red-ui/src/app/modules/file-preview/components/annotation-actions/annotation-actions.component.html index a6a68b376..3a79d9f8e 100644 --- a/apps/red-ui/src/app/modules/file-preview/components/annotation-actions/annotation-actions.component.html +++ b/apps/red-ui/src/app/modules/file-preview/components/annotation-actions/annotation-actions.component.html @@ -1,161 +1,165 @@ -
- - - +@if (canPerformAnnotationActions && annotationPermissions()) { +
+ + + - - + + - - - + + + - + - + - + - + - + - + - + - + - + - + - - -
+ +
+
+} diff --git a/apps/red-ui/src/app/modules/file-preview/components/annotation-actions/annotation-actions.component.ts b/apps/red-ui/src/app/modules/file-preview/components/annotation-actions/annotation-actions.component.ts index 3e2b3f822..ae64768f4 100644 --- a/apps/red-ui/src/app/modules/file-preview/components/annotation-actions/annotation-actions.component.ts +++ b/apps/red-ui/src/app/modules/file-preview/components/annotation-actions/annotation-actions.component.ts @@ -1,4 +1,4 @@ -import { Component, computed, Input, OnChanges } from '@angular/core'; +import { Component, computed, input, Input, untracked } from '@angular/core'; import { CircleButtonComponent, getConfig, HelpModeService, IqserAllowDirective, IqserPermissionsService } from '@iqser/common-ui'; import { AnnotationPermissions } from '@models/file/annotation.permissions'; import { AnnotationWrapper } from '@models/file/annotation.wrapper'; @@ -28,21 +28,77 @@ export type AnnotationButtonType = keyof typeof AnnotationButtonTypes; standalone: true, imports: [CircleButtonComponent, NgIf, TranslateModule, AsyncPipe, IqserAllowDirective], }) -export class AnnotationActionsComponent implements OnChanges { - #annotations: AnnotationWrapper[] = []; - readonly #isDocumine = getConfig().IS_DOCUMINE; - protected _annotationId = ''; +export class AnnotationActionsComponent { @Input() buttonType: AnnotationButtonType = AnnotationButtonTypes.default; @Input() tooltipPosition: 'before' | 'above' = 'before'; @Input() canPerformAnnotationActions: boolean; @Input() alwaysVisible: boolean; @Input() actionsHelpModeKey: string; readonly roles = Roles; - annotationPermissions: AnnotationPermissions; - isImage = true; + readonly annotations = input.required({ + transform: value => value.filter(a => a !== undefined), + }); readonly isVisible = computed(() => { const hidden = this._annotationManager.hidden(); - return this.#annotations.reduce((acc, annotation) => !hidden.has(annotation.id) && acc, true); + return this.annotations().reduce((acc, annotation) => !hidden.has(annotation.id) && acc, true); + }); + readonly somePending = computed(() => { + return this.annotations().some(a => a.pending); + }); + readonly sameType = computed(() => { + const annotations = this.annotations(); + const type = annotations[0].superType; + return annotations.every(a => a.superType === type); + }); + readonly resizing = computed(() => { + return this.annotations().length === 1 && this.annotations()[0].id === this._annotationManager.resizingAnnotationId; + }); + readonly viewerAnnotations = computed(() => { + return this._annotationManager.get(this.annotations()); + }); + readonly annotationPermissions = computed(() => + AnnotationPermissions.forUser( + this._permissionsService.isApprover(this._state.dossier()), + this.annotations(), + this._state.dictionaries, + this._iqserPermissionsService, + this._state.file().excludedFromAutomaticAnalysis, + ), + ); + readonly hideSkipped = computed(() => this.skippedService.hideSkipped() && this.annotations().some(a => a.isSkipped)); + readonly isImageHint = computed(() => this.annotations().every(a => a.IMAGE_HINT)); + readonly isImage = computed(() => this.annotations().reduce((acc, a) => acc && a.isImage, true)); + readonly canRemoveRedaction = computed( + () => this.annotationChangesAllowed() && this.annotationPermissions().canRemoveRedaction && this.sameType(), + ); + readonly canForceRedaction = computed(() => this.annotationChangesAllowed() && this.annotationPermissions().canForceRedaction); + readonly canForceHint = computed(() => this.annotationChangesAllowed() && this.annotationPermissions().canForceHint); + readonly canAcceptRecommendation = computed( + () => this.annotationChangesAllowed() && this.annotationPermissions().canAcceptRecommendation, + ); + readonly #isDocumine = getConfig().IS_DOCUMINE; + readonly annotationChangesAllowed = computed( + () => (!this.#isDocumine || !this._state.file().excludedFromAutomaticAnalysis) && !this.somePending(), + ); + readonly canResize = computed( + () => this.annotationChangesAllowed() && this.annotationPermissions().canResizeAnnotation && this.annotations().length === 1, + ); + readonly canEdit = computed(() => { + const canEditRedactions = + this.annotationPermissions().canChangeLegalBasis || + this.annotationPermissions().canRecategorizeAnnotation || + this.annotationPermissions().canForceHint || + this.annotationPermissions().canForceRedaction; + return ( + this.annotationChangesAllowed() && + (this.annotations().length > 1 + ? this.#isDocumine + ? this.annotationPermissions().canEditAnnotations + : this.annotationPermissions().canEditHints || + this.annotationPermissions().canEditImages || + this.annotationPermissions().canEditAnnotations + : canEditRedactions) + ); }); constructor( @@ -58,137 +114,54 @@ export class AnnotationActionsComponent implements OnChanges { readonly annotationReferencesService: AnnotationReferencesService, ) {} - get annotations(): AnnotationWrapper[] { - return this.#annotations; - } - - get isImageHint(): boolean { - return this.annotations.every(annotation => annotation.IMAGE_HINT); - } - - @Input() - set annotations(annotations: AnnotationWrapper[]) { - this.#annotations = annotations.filter(a => a !== undefined); - this.isImage = this.#annotations?.reduce((accumulator, annotation) => annotation.isImage && accumulator, true); - this._annotationId = this.#annotations[0]?.id; - } - - get canEdit(): boolean { - const canEditRedactions = - this.annotationPermissions.canChangeLegalBasis || - this.annotationPermissions.canRecategorizeAnnotation || - this.annotationPermissions.canForceHint || - this.annotationPermissions.canForceRedaction; - return ( - this.#annotationChangesAllowed && - (this.annotations.length > 1 - ? this.#isDocumine - ? this.annotationPermissions.canEditAnnotations - : this.annotationPermissions.canEditHints || - this.annotationPermissions.canEditImages || - this.annotationPermissions.canEditAnnotations - : canEditRedactions) - ); - } - - get canResize(): boolean { - return this.#annotationChangesAllowed && this.annotationPermissions.canResizeAnnotation && this.annotations.length === 1; - } - - get canRemoveRedaction(): boolean { - return this.#annotationChangesAllowed && this.annotationPermissions.canRemoveRedaction && this.#sameType; - } - - get canForceRedaction() { - return this.#annotationChangesAllowed && this.annotationPermissions.canForceRedaction; - } - - get canForceHint() { - return this.#annotationChangesAllowed && this.annotationPermissions.canForceHint; - } - - get canAcceptRecommendation() { - return this.#annotationChangesAllowed && this.annotationPermissions.canAcceptRecommendation; - } - - get viewerAnnotations() { - return this._annotationManager.get(this.#annotations); - } - - get resizing() { - return this.#annotations?.length === 1 && this.#annotations?.[0].id === this._annotationManager.resizingAnnotationId; - } - - get resized() { + get resized(): boolean { return this._annotationManager.annotationHasBeenResized; } - get hideSkipped() { - return this.skippedService.hideSkipped() && this.annotations.some(a => a.isSkipped); + async removeRedaction(): Promise { + const annotations = untracked(this.annotations); + const permissions = untracked(this.annotationPermissions); + await this.annotationActionsService.removeRedaction(annotations, permissions); } - get #sameType() { - const type = this.annotations[0].superType; - return this.annotations.every(a => a.superType === type); - } - - ngOnChanges(): void { - this.#setPermissions(); - } - - removeRedaction() { - this.annotationActionsService.removeRedaction(this.annotations, this.annotationPermissions); - } - - acceptRecommendation() { - return this.annotationActionsService.convertRecommendationToAnnotation(this.annotations); + async acceptRecommendation(): Promise { + const annotations = untracked(this.annotations); + await this.annotationActionsService.convertRecommendationToAnnotation(annotations); } hideAnnotation() { - this._annotationManager.hide(this.viewerAnnotations); + const viewerAnnotations = untracked(this.viewerAnnotations); + this._annotationManager.hide(viewerAnnotations); this._annotationManager.deselect(); - this._annotationManager.addToHidden(this.viewerAnnotations[0].Id); + this._annotationManager.addToHidden(viewerAnnotations[0].Id); } showAnnotation() { - this._annotationManager.show(this.viewerAnnotations); + const viewerAnnotations = untracked(this.viewerAnnotations); + this._annotationManager.show(viewerAnnotations); this._annotationManager.deselect(); - this._annotationManager.removeFromHidden(this.viewerAnnotations[0].Id); + this._annotationManager.removeFromHidden(viewerAnnotations[0].Id); } resize() { - return this.annotationActionsService.resize(this.#annotations[0]); + const annotations = untracked(this.annotations); + return this.annotationActionsService.resize(annotations[0]); } acceptResize() { if (this.resized) { - return this.annotationActionsService.acceptResize(this.#annotations[0], this.annotationPermissions); + const annotations = untracked(this.annotations); + const permissions = untracked(this.annotationPermissions); + return this.annotationActionsService.acceptResize(annotations[0], permissions); } } cancelResize() { - return this.annotationActionsService.cancelResize(this.#annotations[0]); + const annotations = untracked(this.annotations); + return this.annotationActionsService.cancelResize(annotations[0]); } helpModeKey(action: string) { return this.#isDocumine ? `${action}_annotation` : `${this.actionsHelpModeKey}_${action}`; } - - #setPermissions() { - this.annotationPermissions = AnnotationPermissions.forUser( - this._permissionsService.isApprover(this._state.dossier()), - this.#annotations, - this._state.dictionaries, - this._iqserPermissionsService, - this._state.file().excludedFromAutomaticAnalysis, - ); - } - - get #annotationChangesAllowed() { - return (!this.#isDocumine || !this._state.file().excludedFromAutomaticAnalysis) && !this.#somePending; - } - - get #somePending() { - return this.#annotations.some(a => a.pending); - } } diff --git a/apps/red-ui/src/app/modules/file-preview/components/annotation-wrapper/annotation-wrapper.component.html b/apps/red-ui/src/app/modules/file-preview/components/annotation-wrapper/annotation-wrapper.component.html index 7297036a0..e3eeb5b22 100644 --- a/apps/red-ui/src/app/modules/file-preview/components/annotation-wrapper/annotation-wrapper.component.html +++ b/apps/red-ui/src/app/modules/file-preview/components/annotation-wrapper/annotation-wrapper.component.html @@ -1,37 +1,42 @@
-
+
-
-
- - {{ annotation.item.numberOfComments }} -
+ @if (!annotation().item.isEarmark) { +
+ @if (!annotation().item.pending) { +
+ + {{ annotation().item.numberOfComments }} +
+ } -
- + @if (multiSelectService.inactive()) { +
+ +
+ }
-
+ } - - + @if (showComments) { +
-
+ }
- + diff --git a/apps/red-ui/src/app/modules/file-preview/components/annotation-wrapper/annotation-wrapper.component.ts b/apps/red-ui/src/app/modules/file-preview/components/annotation-wrapper/annotation-wrapper.component.ts index b7ce777e0..ccf5bc79e 100644 --- a/apps/red-ui/src/app/modules/file-preview/components/annotation-wrapper/annotation-wrapper.component.ts +++ b/apps/red-ui/src/app/modules/file-preview/components/annotation-wrapper/annotation-wrapper.component.ts @@ -1,4 +1,4 @@ -import { Component, HostBinding, inject, Input, OnChanges } from '@angular/core'; +import { Component, computed, effect, HostBinding, inject, input } from '@angular/core'; import { getConfig, StopPropagationDirective } from '@iqser/common-ui'; import { AnnotationWrapper } from '@models/file/annotation.wrapper'; import { ListItem } from '@models/file/list-item'; @@ -6,7 +6,6 @@ import { MultiSelectService } from '../../services/multi-select.service'; import { PdfProxyService } from '../../services/pdf-proxy.service'; import { ActionsHelpModeKeys } from '../../utils/constants'; import { AnnotationCardComponent } from '../annotation-card/annotation-card.component'; -import { NgIf } from '@angular/common'; import { MatTooltip } from '@angular/material/tooltip'; import { TranslateModule } from '@ngx-translate/core'; import { MatIcon } from '@angular/material/icon'; @@ -22,7 +21,6 @@ import { ReplaceNbspPipe } from '@common-ui/pipes/replace-nbsp.pipe'; standalone: true, imports: [ AnnotationCardComponent, - NgIf, MatTooltip, TranslateModule, StopPropagationDirective, @@ -33,26 +31,28 @@ import { ReplaceNbspPipe } from '@common-ui/pipes/replace-nbsp.pipe'; ReplaceNbspPipe, ], }) -export class AnnotationWrapperComponent implements OnChanges { - readonly #isDocumine = getConfig().IS_DOCUMINE; +export class AnnotationWrapperComponent { + readonly annotation = input.required>(); + @HostBinding('attr.annotation-id') annotationId: string; + @HostBinding('class.active') active: boolean; + + readonly actionsHelpModeKey = computed(() => this.#getActionsHelpModeKey(this.annotation().item)); + showComments = false; protected readonly pdfProxyService = inject(PdfProxyService); protected readonly multiSelectService = inject(MultiSelectService); - @Input({ required: true }) annotation!: ListItem; - @HostBinding('attr.annotation-id') annotationId: string; - @HostBinding('class.active') active = false; - actionsHelpModeKey: string; - showComments = false; + readonly #isDocumine = getConfig().IS_DOCUMINE; - ngOnChanges() { - this.annotationId = this.annotation.item.id; - this.active = this.annotation.isSelected; - this.actionsHelpModeKey = this.#getActionsHelpModeKey(); + constructor() { + effect(() => { + this.active = this.annotation().isSelected; + this.annotationId = this.annotation().item.id; + }); } - #getActionsHelpModeKey(): string { + #getActionsHelpModeKey(item: AnnotationWrapper): string { if (!this.#isDocumine) { - const superType = this.annotation.item.superTypeLabel?.split('.')[1]; - const type = this.annotation.item.type; + const superType = item.superTypeLabel?.split('.')[1]; + const type = item.type; if (superType === 'hint' && (type === 'ocr' || type === 'formula' || type === 'image')) { return ActionsHelpModeKeys[`${superType}-${type}`]; } diff --git a/apps/red-ui/src/app/modules/file-preview/components/annotations-list/annotations-list.component.html b/apps/red-ui/src/app/modules/file-preview/components/annotations-list/annotations-list.component.html index c62243644..97d2ea0c0 100644 --- a/apps/red-ui/src/app/modules/file-preview/components/annotations-list/annotations-list.component.html +++ b/apps/red-ui/src/app/modules/file-preview/components/annotations-list/annotations-list.component.html @@ -1,16 +1,16 @@ - -
+@for (annotation of annotations(); track annotation.item.id) { + @if (displayedHighlightGroups()[$index]; as highlightGroup) { -
+ } -
+} []; - @Output() readonly pagesPanelActive = new EventEmitter(); - readonly #earmarkGroups = computed(() => { - if (this._viewModeService.isEarmarks()) { - return this.#getEarmarksGroups(); + readonly annotations = input.required[]>(); + readonly pagesPanelActive = output(); + readonly displayedHighlightGroups = computed(() => { + const isEarMarks = this._viewModeService.isEarmarks(); + let result: Record = {}; + + if (isEarMarks) { + const annotations = this.annotations(); + const earmarkGroups = this.#getEarmarksGroups(annotations); + for (let idx = 0; idx < annotations.length; ++idx) { + const group = earmarkGroups.find(h => h.startIdx === idx); + if (group) { + result[idx] = group; + } + } } - return [] as EarmarkGroup[]; + + return result; }); protected readonly isDocumine = getConfig().IS_DOCUMINE; @@ -77,26 +88,20 @@ export class AnnotationsListComponent extends HasScrollbarDirective { } } - showHighlightGroup(idx: number): EarmarkGroup { - return this._viewModeService.isEarmarks() && this.#earmarkGroups().find(h => h.startIdx === idx); - } - - protected readonly _trackBy = (index: number, listItem: ListItem) => listItem.item.id; - - #getEarmarksGroups() { + #getEarmarksGroups(annotations: ListItem[]) { const earmarksGroups: EarmarkGroup[] = []; - if (!this.annotations?.length) { + if (!annotations.length) { return earmarksGroups; } let lastGroup: EarmarkGroup; - for (let idx = 0; idx < this.annotations.length; ++idx) { - if (idx === 0 || this.annotations[idx].item.color !== this.annotations[idx - 1].item.color) { + for (let idx = 0; idx < annotations.length; ++idx) { + if (idx === 0 || annotations[idx].item.color !== annotations[idx - 1].item.color) { if (lastGroup) { earmarksGroups.push(lastGroup); } - lastGroup = { startIdx: idx, length: 1, color: this.annotations[idx].item.color }; + lastGroup = { startIdx: idx, length: 1, color: annotations[idx].item.color }; } else { lastGroup.length += 1; } diff --git a/apps/red-ui/src/app/modules/file-preview/file-preview-screen.component.ts b/apps/red-ui/src/app/modules/file-preview/file-preview-screen.component.ts index 7701d687c..7aab6a810 100644 --- a/apps/red-ui/src/app/modules/file-preview/file-preview-screen.component.ts +++ b/apps/red-ui/src/app/modules/file-preview/file-preview-screen.component.ts @@ -1,5 +1,5 @@ import { ActivatedRouteSnapshot, NavigationExtras, Router, RouterLink } from '@angular/router'; -import { ChangeDetectorRef, Component, effect, NgZone, OnDestroy, OnInit, TemplateRef, ViewChild } from '@angular/core'; +import { ChangeDetectorRef, Component, effect, NgZone, OnDestroy, OnInit, TemplateRef, untracked, ViewChild } from '@angular/core'; import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; import { ComponentCanDeactivate } from '@guards/can-deactivate.guard'; import { @@ -100,13 +100,13 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni readonly roles = Roles; readonly fileId = this.state.fileId; readonly dossierId = this.state.dossierId; + protected readonly isDocumine = getConfig().IS_DOCUMINE; @ViewChild('annotationFilterTemplate', { read: TemplateRef, static: false, }) private readonly _filterTemplate: TemplateRef; #loadAllAnnotationsEnabled = false; - protected readonly isDocumine = getConfig().IS_DOCUMINE; constructor( readonly pdf: PdfViewer, @@ -179,7 +179,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni effect(() => { this._viewModeService.viewMode(); - this.updateViewMode().then(); + this.#updateViewMode().then(); }); effect(() => { @@ -207,7 +207,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni const earmarks$ = isEarmarksViewMode$.pipe( tap(() => this._loadingService.start()), switchMap(() => this._fileDataService.loadEarmarks()), - tap(() => this.updateViewMode().then(() => this._loadingService.stop())), + tap(() => this.#updateViewMode().then(() => this._loadingService.stop())), ); const currentPageIfEarmarksView$ = combineLatest([this.pdf.currentPage$, this._viewModeService.viewMode$]).pipe( @@ -233,7 +233,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni return isChangingFromEarmarksViewMode$.pipe( map(() => this._fileDataService.earmarks().get(this.pdf.currentPage()) ?? []), - map(earmarks => this.deleteAnnotations(earmarks, [])), + map(earmarks => this.#deleteAnnotations(earmarks, [])), ); } @@ -242,61 +242,6 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni this._viewerHeaderService.disable(ROTATION_ACTION_BUTTONS); } - async updateViewMode(): Promise { - this._logger.info(`[PDF] Update ${this._viewModeService.viewMode()} view mode`); - - const annotations = this._annotationManager.get(a => bool(a.getCustomData('redact-manager'))); - const redactions = annotations.filter(a => bool(a.getCustomData('redaction'))); - - switch (this._viewModeService.viewMode()) { - case ViewModes.STANDARD: { - const wrappers = this._fileDataService.annotations(); - const ocrAnnotationIds = wrappers.filter(a => a.isOCR).map(a => a.id); - const standardEntries = annotations - .filter(a => !bool(a.getCustomData('changeLogRemoved')) && !this._annotationManager.isHidden(a.Id)) - .filter(a => !ocrAnnotationIds.includes(a.Id)); - const nonStandardEntries = annotations.filter( - a => - bool(a.getCustomData('changeLogRemoved')) || - this._annotationManager.isHidden(a.Id) || - (this._skippedService.hideSkipped() && bool(a.getCustomData('skipped'))), - ); - this._readableRedactionsService.setAnnotationsColor(standardEntries, 'annotationColor'); - this._readableRedactionsService.setAnnotationsOpacity(standardEntries, true); - this._annotationManager.show(standardEntries); - this._annotationManager.hide(nonStandardEntries); - break; - } - case ViewModes.DELTA: { - const changeLogEntries = annotations.filter(a => bool(a.getCustomData('changeLog'))); - const nonChangeLogEntries = annotations.filter(a => !bool(a.getCustomData('changeLog'))); - this._readableRedactionsService.setAnnotationsColor(redactions, 'annotationColor'); - this._readableRedactionsService.setAnnotationsOpacity(changeLogEntries, true); - this._annotationManager.show(changeLogEntries); - this._annotationManager.hide(nonChangeLogEntries); - break; - } - case ViewModes.REDACTED: { - const nonRedactionEntries = annotations.filter( - a => !bool(a.getCustomData('redaction')) || bool(a.getCustomData('changeLogRemoved')), - ); - this._readableRedactionsService.setAnnotationsColor(redactions, 'redactionColor'); - this._readableRedactionsService.setAnnotationsOpacity(redactions); - this._annotationManager.show(redactions); - this._annotationManager.hide(nonRedactionEntries); - - break; - } - case ViewModes.TEXT_HIGHLIGHTS: { - this._annotationManager.hide(annotations); - } - } - - this._logger.info('[PDF] Rebuild filters'); - this.#rebuildFilters(); - this._logger.info('[PDF] Update done'); - } - ngOnDetach() { this._viewerHeaderService.resetCompareButtons(); this._viewerHeaderService.enableLoadAllAnnotations(); // Reset the button state (since the viewer is reused between files) @@ -413,7 +358,9 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni const annotations$ = this._fileDataService.annotations$.pipe( startWith([] as AnnotationWrapper[]), pairwise(), - tap(annotations => this.deleteAnnotations(...annotations)), + tap(annotations => this.#deleteAnnotations(...annotations)), + tap(() => this.#updateFiltersAfterAnnotationsChanged()), + tap(() => this.#updateViewMode()), ); const currentPageIfNotHighlightsView$ = combineLatest([this.pdf.currentPage$, this._viewModeService.viewMode$]).pipe( @@ -432,12 +379,72 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni return combineLatest([currentPageAnnotations$, this._documentViewer.loaded$]).pipe( filter(([, loaded]) => loaded), map(([annotations]) => annotations), - tap(([oldA, newA]) => this.drawChangedAnnotations(oldA, newA)?.then(() => this.updateViewMode())), - tap(([, newAnnotations]) => this.#highlightSelectedAnnotations(newAnnotations)), + switchMap(async ([oldAnnotations, newAnnotations]) => { + await this.#drawChangedAnnotations(oldAnnotations, newAnnotations); + return newAnnotations; + }), + tap(newAnnotations => this.#highlightSelectedAnnotations(newAnnotations)), ); } - deleteAnnotations(oldAnnotations: AnnotationWrapper[], newAnnotations: AnnotationWrapper[]) { + async #updateViewMode(): Promise { + const viewMode = untracked(this._viewModeService.viewMode); + this._logger.info(`[PDF] Update ${viewMode} view mode`); + + const annotations = this._annotationManager.get(a => bool(a.getCustomData('redact-manager'))); + const redactions = annotations.filter(a => bool(a.getCustomData('redaction'))); + + switch (viewMode) { + case ViewModes.STANDARD: { + const wrappers = this._fileDataService.annotations(); + // TODO: const wrappers = untracked(this._fileDataService.annotations); + const ocrAnnotationIds = wrappers.filter(a => a.isOCR).map(a => a.id); + const standardEntries = annotations + .filter(a => !bool(a.getCustomData('changeLogRemoved')) && !this._annotationManager.isHidden(a.Id)) + .filter(a => !ocrAnnotationIds.includes(a.Id)); + const nonStandardEntries = annotations.filter( + a => + bool(a.getCustomData('changeLogRemoved')) || + this._annotationManager.isHidden(a.Id) || + (untracked(this._skippedService.hideSkipped) && bool(a.getCustomData('skipped'))), + ); + this._readableRedactionsService.setAnnotationsColor(standardEntries, 'annotationColor'); + this._readableRedactionsService.setAnnotationsOpacity(standardEntries, true); + this._annotationManager.show(standardEntries); + this._annotationManager.hide(nonStandardEntries); + break; + } + case ViewModes.DELTA: { + const changeLogEntries = annotations.filter(a => bool(a.getCustomData('changeLog'))); + const nonChangeLogEntries = annotations.filter(a => !bool(a.getCustomData('changeLog'))); + this._readableRedactionsService.setAnnotationsColor(redactions, 'annotationColor'); + this._readableRedactionsService.setAnnotationsOpacity(changeLogEntries, true); + this._annotationManager.show(changeLogEntries); + this._annotationManager.hide(nonChangeLogEntries); + break; + } + case ViewModes.REDACTED: { + const nonRedactionEntries = annotations.filter( + a => !bool(a.getCustomData('redaction')) || bool(a.getCustomData('changeLogRemoved')), + ); + this._readableRedactionsService.setAnnotationsColor(redactions, 'redactionColor'); + this._readableRedactionsService.setAnnotationsOpacity(redactions); + this._annotationManager.show(redactions); + this._annotationManager.hide(nonRedactionEntries); + + break; + } + case ViewModes.TEXT_HIGHLIGHTS: { + this._annotationManager.hide(annotations); + } + } + + this._logger.info('[PDF] Rebuild filters'); + this.#rebuildFilters(); + this._logger.info('[PDF] Update done'); + } + + #deleteAnnotations(oldAnnotations: AnnotationWrapper[], newAnnotations: AnnotationWrapper[]) { const annotationsToDelete = oldAnnotations.filter(oldAnnotation => { const newAnnotation = newAnnotations.find(byId(oldAnnotation.id)); return newAnnotation ? hasChanges(oldAnnotation, newAnnotation) : true; @@ -447,11 +454,11 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni this._annotationManager.delete(annotationsToDelete); } - drawChangedAnnotations(oldAnnotations: AnnotationWrapper[], newAnnotations: AnnotationWrapper[]) { + async #drawChangedAnnotations(oldAnnotations: AnnotationWrapper[], newAnnotations: AnnotationWrapper[]): Promise { const annotationsToDraw = this.#getAnnotationsToDraw(oldAnnotations, newAnnotations); this._logger.info('[ANNOTATIONS] To draw: ', annotationsToDraw); this._annotationManager.delete(annotationsToDraw); - return this.#cleanupAndRedrawAnnotations(annotationsToDraw); + await this.#cleanupAndRedrawAnnotations(annotationsToDraw); } async #openRedactTextDialog(manualRedactionEntryWrapper: ManualRedactionEntryWrapper) { @@ -642,7 +649,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni filter(event => event.type === ViewerEvents.LOAD_ALL_ANNOTATIONS), switchMap(() => { // TODO: this switchMap is ugly, to be refactored - const annotations = this._fileDataService.annotations(); + const annotations = untracked(this._fileDataService.annotations); const showWarning = !this.userPreferenceService.getBool(PreferencesKeys.loadAllAnnotationsWarning); const annotationsExceedThreshold = annotations.length >= this.configService.values.ANNOTATIONS_THRESHOLD; @@ -655,7 +662,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni filter(([confirmed]) => confirmed), map(([, annotations]) => { this.#loadAllAnnotationsEnabled = true; - this.drawChangedAnnotations([], annotations).then(() => { + this.#drawChangedAnnotations([], annotations).then(() => { this._toaster.success(_('load-all-annotations-success')); this._viewerHeaderService.disableLoadAllAnnotations(); }); @@ -663,7 +670,9 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni ) .subscribe(); - this.addActiveScreenSubscription = this._readableRedactionsService.active$.pipe(map(() => this.updateViewMode())).subscribe(); + this.addActiveScreenSubscription = this._readableRedactionsService.active$ + .pipe(switchMap(() => this.#updateViewMode())) + .subscribe(); this.addActiveScreenSubscription = combineLatest([this._viewModeService.viewMode$, this.state.file$, this._documentViewer.loaded$]) .pipe( @@ -702,11 +711,15 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni this._errorService.set(error); } - #cleanupAndRedrawAnnotations(newAnnotations: List) { + async #cleanupAndRedrawAnnotations(newAnnotations: List): Promise { if (!newAnnotations.length) { return undefined; } + await this._annotationDrawService.draw(newAnnotations, this._skippedService.hideSkipped(), this.state.dossierTemplateId); + } + + #updateFiltersAfterAnnotationsChanged(): void { const currentFilters = this._filterService.getGroup('primaryFilters')?.filters || []; this.#rebuildFilters(); @@ -715,8 +728,6 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni this.#handleDeltaAnnotationFilters(currentFilters); }, 100); } - - return this._annotationDrawService.draw(newAnnotations, this._skippedService.hideSkipped(), this.state.dossierTemplateId); } #handleDeltaAnnotationFilters(currentFilters: NestedFilter[]) { diff --git a/apps/red-ui/src/app/modules/file-preview/services/annotation-references.service.ts b/apps/red-ui/src/app/modules/file-preview/services/annotation-references.service.ts index 914376c97..789d38d83 100644 --- a/apps/red-ui/src/app/modules/file-preview/services/annotation-references.service.ts +++ b/apps/red-ui/src/app/modules/file-preview/services/annotation-references.service.ts @@ -1,5 +1,4 @@ import { computed, Injectable, Signal, signal } from '@angular/core'; -import { BehaviorSubject } from 'rxjs'; import { AnnotationWrapper } from '@models/file/annotation.wrapper'; import { FileDataService } from './file-data.service'; @@ -7,7 +6,6 @@ import { FileDataService } from './file-data.service'; export class AnnotationReferencesService { readonly references: Signal; readonly annotation: Signal; - private readonly _annotation$ = new BehaviorSubject(undefined); readonly #annotation = signal(undefined); constructor(private readonly _fileDataService: FileDataService) { diff --git a/apps/red-ui/src/app/modules/file-preview/services/pdf-proxy.service.ts b/apps/red-ui/src/app/modules/file-preview/services/pdf-proxy.service.ts index 976be8626..7b7560376 100644 --- a/apps/red-ui/src/app/modules/file-preview/services/pdf-proxy.service.ts +++ b/apps/red-ui/src/app/modules/file-preview/services/pdf-proxy.service.ts @@ -1,4 +1,4 @@ -import { computed, effect, inject, Injectable, NgZone } from '@angular/core'; +import { computed, effect, inject, Injectable, NgZone, untracked } from '@angular/core'; import { getConfig, IqserPermissionsService } from '@iqser/common-ui'; import { getCurrentUser } from '@iqser/common-ui/lib/users'; import { isJustOne, shareDistinctLast, UI_ROOT_PATH_FN } from '@iqser/common-ui/lib/utils'; @@ -49,7 +49,7 @@ export class PdfProxyService { readonly redactTextRequested$ = new Subject(); readonly currentUser = getCurrentUser(); readonly pageChanged$ = this._pdf.pageChanged$.pipe( - tap(() => this.#handleExcludedPageActions()), + tap(pageNumber => this.#handleExcludedPageActions(pageNumber)), tap(() => { if (this._multiSelectService.inactive()) { this._annotationManager.deselect(); @@ -98,7 +98,7 @@ export class PdfProxyService { effect( () => { const canPerformActions = this.canPerformActions(); - this._pdf.isCompareMode(); + const isCompareMode = this._pdf.isCompareMode(); this.#configureTextPopup(); @@ -109,7 +109,8 @@ export class PdfProxyService { this.#deactivateMultiSelect(); } - this.#handleExcludedPageActions(); + const currentPage = untracked(this._pdf.currentPage); + this.#handleExcludedPageActions(currentPage); }, { allowSignalWrites: true }, ); @@ -227,8 +228,9 @@ export class PdfProxyService { this.redactTextRequested$.next({ manualRedactionEntry, type }); } - #handleExcludedPageActions() { - const isCurrentPageExcluded = this._state.file().isPageExcluded(this._pdf.currentPage()); + #handleExcludedPageActions(currentPage: number) { + const isCurrentPageExcluded = this._state.file().isPageExcluded(currentPage); + if (!isCurrentPageExcluded) { return; } diff --git a/apps/red-ui/src/app/modules/pdf-viewer/services/annotation-draw.service.ts b/apps/red-ui/src/app/modules/pdf-viewer/services/annotation-draw.service.ts index 444eadf90..b575b2cb3 100644 --- a/apps/red-ui/src/app/modules/pdf-viewer/services/annotation-draw.service.ts +++ b/apps/red-ui/src/app/modules/pdf-viewer/services/annotation-draw.service.ts @@ -2,7 +2,7 @@ import { Injectable } from '@angular/core'; import { List } from '@iqser/common-ui/lib/utils'; import { AnnotationWrapper } from '@models/file/annotation.wrapper'; import { Core } from '@pdftron/webviewer'; -import { IRectangle, ISectionRectangle, IPoint, SuperTypes } from '@red/domain'; +import { IPoint, IRectangle, ISectionRectangle, SuperTypes } from '@red/domain'; import { DefaultColorsService } from '@services/entity-services/default-colors.service'; import { hexToRgb } from '@utils/functions'; import { BoundingBox, Table } from '../../file-preview/services/tables.service'; diff --git a/apps/red-ui/src/app/modules/pdf-viewer/services/annotation-manager.service.ts b/apps/red-ui/src/app/modules/pdf-viewer/services/annotation-manager.service.ts index a2457e199..201d3560c 100644 --- a/apps/red-ui/src/app/modules/pdf-viewer/services/annotation-manager.service.ts +++ b/apps/red-ui/src/app/modules/pdf-viewer/services/annotation-manager.service.ts @@ -19,14 +19,14 @@ const MOVE_OPTION = 'move'; @Injectable() export class REDAnnotationManager { + resizingAnnotationId?: string = undefined; + annotationHasBeenResized?: boolean = false; readonly #hidden = signal(new Set()); + readonly hidden = this.#hidden.asReadonly(); #manager: AnnotationManager; readonly #logger = inject(NGXLogger); readonly #annotationSelected$ = new Subject<[Annotation[], string]>(); readonly annotationSelected$ = this.#annotationSelected$.asObservable(); - resizingAnnotationId?: string = undefined; - annotationHasBeenResized?: boolean = false; - readonly hidden = this.#hidden.asReadonly(); get selected() { return this.#manager.getSelectedAnnotations(); @@ -128,21 +128,15 @@ export class REDAnnotationManager { annotations.forEach(annotation => this.#manager.redrawAnnotation(annotation)); } - add(annotations: Annotation | Annotation[]) { + async add(annotations: Annotation | Annotation[]): Promise { try { annotations = asList(annotations); this.#manager.addAnnotations(annotations, { imported: true }); - return this.#manager.drawAnnotationsFromList(annotations); } catch (e) { console.log(e); } } - showHidden() { - const hidden = this.#getByIds([...this.hidden().values()]); - this.show(hidden); - } - #listenForAnnotationSelected() { this.#manager.addEventListener('annotationSelected', async (annotations: Annotation[], action: string) => { this.#logger.info('[PDF] Annotation selected: ', annotations, action); diff --git a/apps/red-ui/src/app/services/files/entity-log.service.ts b/apps/red-ui/src/app/services/files/entity-log.service.ts index dbc05ea4e..d7287d40c 100644 --- a/apps/red-ui/src/app/services/files/entity-log.service.ts +++ b/apps/red-ui/src/app/services/files/entity-log.service.ts @@ -1,6 +1,6 @@ import { inject, Injectable } from '@angular/core'; -import { GenericService, isIqserDevMode, Toaster } from '@iqser/common-ui'; -import { EntryStates, IEntityLog, IEntityLogEntry, ISectionGrid } from '@red/domain'; +import { GenericService, Toaster } from '@iqser/common-ui'; +import { EntryStates, IEntityLog, IEntityLogEntry } from '@red/domain'; import { firstValueFrom, of } from 'rxjs'; import { catchError } from 'rxjs/operators'; @@ -20,10 +20,6 @@ export class EntityLogService extends GenericService { return entityLog; } - getSectionGrid(dossierId: string, fileId: string) { - return this._getOne([dossierId, fileId], 'sectionGrid'); - } - #filterInvalidEntries(entityLogEntry: IEntityLogEntry[]) { return entityLogEntry.filter(entry => { entry.positions = entry.positions?.filter(p => !!p.rectangle?.length);