diff --git a/apps/red-ui/src/app/models/file/annotation.wrapper.ts b/apps/red-ui/src/app/models/file/annotation.wrapper.ts index 54ae0e1ed..d826475d5 100644 --- a/apps/red-ui/src/app/models/file/annotation.wrapper.ts +++ b/apps/red-ui/src/app/models/file/annotation.wrapper.ts @@ -10,13 +10,12 @@ import { SuperType, SuperTypes, } from '@models/file/super-types'; -import { List } from '@iqser/common-ui'; +import { IListable, List } from '@iqser/common-ui'; -export class AnnotationWrapper implements Record { +export class AnnotationWrapper implements IListable, Record { [x: string]: unknown; superType: SuperType; - typeValue: string; recategorizationType: string; color: string; @@ -40,21 +39,17 @@ export class AnnotationWrapper implements Record { rectangle?: boolean; section?: string; reference: List; - imported?: boolean; image?: boolean; manual?: boolean; hidden?: boolean; pending?: boolean; hintDictionary?: boolean; - textAfter?: string; textBefore?: string; - isChangeLogEntry?: boolean; changeLogType?: 'ADDED' | 'REMOVED' | 'CHANGED'; engines?: string[]; - hasBeenResized: boolean; hasBeenRecategorized: boolean; hasLegalBasisChanged: boolean; @@ -62,6 +57,10 @@ export class AnnotationWrapper implements Record { hasBeenForcedRedaction: boolean; hasBeenRemovedByManualOverride: boolean; + get searchKey(): string { + return this.id; + } + get isChangeLogRemoved() { return this.changeLogType === 'REMOVED'; } diff --git a/apps/red-ui/src/app/modules/file-preview/components/file-workload/file-workload.component.html b/apps/red-ui/src/app/modules/file-preview/components/file-workload/file-workload.component.html index d886017df..291ab2bdb 100644 --- a/apps/red-ui/src/app/modules/file-preview/components/file-workload/file-workload.component.html +++ b/apps/red-ui/src/app/modules/file-preview/components/file-workload/file-workload.component.html @@ -181,11 +181,11 @@ >. - + {{ 'file-preview.tabs.annotations.no-annotations' | translate }} - {{ 'file-preview.tabs.annotations.wrong-filters' | translate }} {{ 'file-preview.tabs.annotations.the-filters' | translate }} diff --git a/apps/red-ui/src/app/modules/file-preview/components/file-workload/file-workload.component.ts b/apps/red-ui/src/app/modules/file-preview/components/file-workload/file-workload.component.ts index a03ae6568..5153490a8 100644 --- a/apps/red-ui/src/app/modules/file-preview/components/file-workload/file-workload.component.ts +++ b/apps/red-ui/src/app/modules/file-preview/components/file-workload/file-workload.component.ts @@ -137,7 +137,7 @@ export class FileWorkloadComponent { const primary$ = this.filterService.getFilterModels$('primaryFilters'); const secondary$ = this.filterService.getFilterModels$('secondaryFilters'); - return combineLatest([this.fileDataService.visibleAnnotations$, primary$, secondary$]).pipe( + return combineLatest([this.fileDataService.all$, primary$, secondary$]).pipe( map(([annotations, primary, secondary]) => this._filterAnnotations(annotations, primary, secondary)), ); } diff --git a/apps/red-ui/src/app/modules/file-preview/components/pdf-viewer/pdf-viewer.component.ts b/apps/red-ui/src/app/modules/file-preview/components/pdf-viewer/pdf-viewer.component.ts index de786c320..19118ca77 100644 --- a/apps/red-ui/src/app/modules/file-preview/components/pdf-viewer/pdf-viewer.component.ts +++ b/apps/red-ui/src/app/modules/file-preview/components/pdf-viewer/pdf-viewer.component.ts @@ -193,7 +193,7 @@ export class PdfViewerComponent extends AutoUnsubscribe implements OnInit, OnCha this.pdfViewer.disableHotkeys(); await this._configureTextPopup(); - this.annotationManager.addEventListener('annotationSelected', async (annotations: Annotation[], action) => { + this.annotationManager.addEventListener('annotationSelected', (annotations: Annotation[], action) => { let nextAnnotations: Annotation[]; if (action === 'deselected') { @@ -213,12 +213,11 @@ export class PdfViewerComponent extends AutoUnsubscribe implements OnInit, OnCha } if (!this.multiSelectService.isEnabled) { - const visibleAnnotations = await this._fileDataService.visibleAnnotations; this.pdfViewer.deselectAnnotations( - visibleAnnotations.filter(wrapper => !nextAnnotations.find(ann => ann.Id === wrapper.id)), + this._fileDataService.all.filter(wrapper => !nextAnnotations.find(ann => ann.Id === wrapper.id)), ); } - await this.#configureAnnotationSpecificActions(annotations); + this.#configureAnnotationSpecificActions(annotations); this._toggleRectangleAnnotationAction(annotations.length === 1 && annotations[0].ReadOnly); }); @@ -350,7 +349,7 @@ export class PdfViewerComponent extends AutoUnsubscribe implements OnInit, OnCha }); } - async #configureAnnotationSpecificActions(viewerAnnotations: Annotation[]) { + #configureAnnotationSpecificActions(viewerAnnotations: Annotation[]) { if (!this.canPerformActions) { if (this.instance.UI.annotationPopup.getItems().length) { this.instance.UI.annotationPopup.update([]); @@ -358,10 +357,7 @@ export class PdfViewerComponent extends AutoUnsubscribe implements OnInit, OnCha return; } - const visibleAnnotations = await this._fileDataService.visibleAnnotations; - const annotationWrappers: AnnotationWrapper[] = viewerAnnotations - .map(va => visibleAnnotations.find(a => a.id === va.Id)) - .filter(va => !!va); + const annotationWrappers: AnnotationWrapper[] = viewerAnnotations.map(va => this._fileDataService.find(va.Id)).filter(va => !!va); this.instance.UI.annotationPopup.update([]); if (annotationWrappers.length === 0) { diff --git a/apps/red-ui/src/app/modules/file-preview/file-preview-providers.ts b/apps/red-ui/src/app/modules/file-preview/file-preview-providers.ts index c89b22949..d752ce855 100644 --- a/apps/red-ui/src/app/modules/file-preview/file-preview-providers.ts +++ b/apps/red-ui/src/app/modules/file-preview/file-preview-providers.ts @@ -8,7 +8,7 @@ import { AnnotationDrawService } from './services/annotation-draw.service'; import { AnnotationActionsService } from './services/annotation-actions.service'; import { FilePreviewStateService } from './services/file-preview-state.service'; import { AnnotationReferencesService } from './services/annotation-references.service'; -import { FilterService } from '@iqser/common-ui'; +import { EntitiesService, FilterService, ListingService, SearchService } from '@iqser/common-ui'; import { AnnotationProcessingService } from '../dossier/services/annotation-processing.service'; import { dossiersServiceProvider } from '@services/entity-services/dossiers.service.provider'; import { PageRotationService } from './services/page-rotation.service'; @@ -33,7 +33,10 @@ export const filePreviewScreenProviders = [ PdfViewer, AnnotationProcessingService, FileDataService, + { provide: EntitiesService, useExisting: FileDataService }, dossiersServiceProvider, ViewerHeaderConfigService, TooltipsService, + ListingService, + SearchService, ]; 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 8d04f6e6c..89e62289d 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 @@ -8,6 +8,7 @@ import { Debounce, ErrorService, FilterService, + ListingService, LoadingService, NestedFilter, OnAttach, @@ -88,6 +89,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni private readonly _errorService: ErrorService, readonly state: FilePreviewStateService, private readonly _filterService: FilterService, + readonly listingService: ListingService, readonly permissionsService: PermissionsService, readonly multiSelectService: MultiSelectService, private readonly _activatedRoute: ActivatedRoute, @@ -189,7 +191,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni } await this._stampPDF(); - await this.rebuildFilters(); + this.#rebuildFilters(); } ngOnDetach(): void { @@ -234,38 +236,11 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni } } - async rebuildFilters() { - const startTime = new Date().getTime(); - - const visibleAnnotations = await this._fileDataService.visibleAnnotations; - const annotationFilters = this._annotationProcessingService.getAnnotationFilter(visibleAnnotations); - const primaryFilters = this._filterService.getGroup('primaryFilters')?.filters; - this._filterService.addFilterGroup({ - slug: 'primaryFilters', - filterTemplate: this._filterTemplate, - filters: processFilters(primaryFilters, annotationFilters), - }); - const secondaryFilters = this._filterService.getGroup('secondaryFilters')?.filters; - this._filterService.addFilterGroup({ - slug: 'secondaryFilters', - filterTemplate: this._filterTemplate, - filters: processFilters( - secondaryFilters, - AnnotationProcessingService.secondaryAnnotationFilters(this._fileDataService.viewedPages), - ), - }); - - this._logger.info(`[FILTERS] Rebuild time: ${new Date().getTime() - startTime} ms`); - } - - async handleAnnotationSelected(annotationIds: string[]) { + handleAnnotationSelected(annotationIds: string[]) { if (annotationIds.length > 0) { this._workloadComponent.pagesPanelActive = false; } - const visibleAnnotations = await this._fileDataService.visibleAnnotations; - this.selectedAnnotations = annotationIds - .map(id => visibleAnnotations.find(annotation => annotation.id === id)) - .filter(ann => ann !== undefined); + this.selectedAnnotations = annotationIds.map(id => this._fileDataService.find(id)).filter(ann => ann !== undefined); if (this.selectedAnnotations.length > 1) { this.multiSelectService.activate(); } @@ -466,6 +441,29 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni return this._cleanupAndRedrawAnnotations(annotationsToDraw); } + #rebuildFilters() { + const startTime = new Date().getTime(); + + const annotationFilters = this._annotationProcessingService.getAnnotationFilter(this._fileDataService.all); + const primaryFilters = this._filterService.getGroup('primaryFilters')?.filters; + this._filterService.addFilterGroup({ + slug: 'primaryFilters', + filterTemplate: this._filterTemplate, + filters: processFilters(primaryFilters, annotationFilters), + }); + const secondaryFilters = this._filterService.getGroup('secondaryFilters')?.filters; + this._filterService.addFilterGroup({ + slug: 'secondaryFilters', + filterTemplate: this._filterTemplate, + filters: processFilters( + secondaryFilters, + AnnotationProcessingService.secondaryAnnotationFilters(this._fileDataService.viewedPages), + ), + }); + + this._logger.info(`[FILTERS] Rebuild time: ${new Date().getTime() - startTime} ms`); + } + async #updateQueryParamsPage(page: number): Promise { // Add current page in URL query params const extras: NavigationExtras = { @@ -503,7 +501,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni async #deactivateMultiSelect() { this.multiSelectService.deactivate(); this.pdf.deselectAllAnnotations(); - await this.handleAnnotationSelected([]); + this.handleAnnotationSelected([]); } private _setExcludedPageStyles() { @@ -626,13 +624,12 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni private async _cleanupAndRedrawAnnotations(newAnnotations: readonly AnnotationWrapper[]) { const currentFilters = this._filterService.getGroup('primaryFilters')?.filters || []; - await this.rebuildFilters(); + this.#rebuildFilters(); const startTime = new Date().getTime(); if (currentFilters) { - const visibleAnnotations = await this._fileDataService.visibleAnnotations; - this._handleDeltaAnnotationFilters(currentFilters, visibleAnnotations); + this._handleDeltaAnnotationFilters(currentFilters, this._fileDataService.all); } await this._annotationDrawService.drawAnnotations(newAnnotations); diff --git a/apps/red-ui/src/app/modules/file-preview/services/file-data.service.ts b/apps/red-ui/src/app/modules/file-preview/services/file-data.service.ts index 63e03e35b..1517a8f66 100644 --- a/apps/red-ui/src/app/modules/file-preview/services/file-data.service.ts +++ b/apps/red-ui/src/app/modules/file-preview/services/file-data.service.ts @@ -13,7 +13,7 @@ import { import { AnnotationWrapper } from '../../../models/file/annotation.wrapper'; import { BehaviorSubject, firstValueFrom, iif, Observable, Subject } from 'rxjs'; import { RedactionLogEntry } from '../../../models/file/redaction-log.entry'; -import { Injectable } from '@angular/core'; +import { Injectable, Injector } from '@angular/core'; import { FilePreviewStateService } from './file-preview-state.service'; import { ViewedPagesService } from '@services/entity-services/viewed-pages.service'; import { UserPreferenceService } from '@services/user-preference.service'; @@ -21,12 +21,11 @@ import { DictionariesMapService } from '@services/entity-services/dictionaries-m import { map, switchMap, tap, withLatestFrom } from 'rxjs/operators'; import { PermissionsService } from '@services/permissions.service'; import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; -import { shareDistinctLast, shareLast, Toaster } from '@iqser/common-ui'; +import { EntitiesService, shareLast, Toaster } from '@iqser/common-ui'; import { RedactionLogService } from '../../dossier/services/redaction-log.service'; import { TextHighlightService } from '../../dossier/services/text-highlight.service'; import { ViewModeService } from './view-mode.service'; import { Core } from '@pdftron/webviewer'; -import { Router } from '@angular/router'; import dayjs from 'dayjs'; import { NGXLogger } from 'ngx-logger'; import Annotation = Core.Annotations.Annotation; @@ -34,16 +33,13 @@ import Annotation = Core.Annotations.Annotation; const DELTA_VIEW_TIME = 10 * 60 * 1000; // 10 minutes; @Injectable() -export class FileDataService { +export class FileDataService extends EntitiesService { viewedPages: IViewedPage[] = []; missingTypes = new Set(); readonly hasChangeLog$ = new BehaviorSubject(false); readonly annotations$: Observable>; - readonly visibleAnnotations$: Observable; - readonly numberOfVisibleAnnotations$: Observable; readonly hiddenAnnotations = new Set(); - readonly #redactionLog$ = new Subject(); readonly #textHighlights$ = new BehaviorSubject([]); @@ -57,25 +53,23 @@ export class FileDataService { private readonly _redactionLogService: RedactionLogService, private readonly _textHighlightsService: TextHighlightService, private readonly _toaster: Toaster, - private readonly _router: Router, private readonly _logger: NGXLogger, + protected readonly _injector: Injector, ) { + super(_injector, AnnotationWrapper); this.annotations$ = this.#annotations$; - this.visibleAnnotations$ = this._viewModeService.viewMode$.pipe( - switchMap(viewMode => - iif( - () => viewMode === ViewModes.TEXT_HIGHLIGHTS, - this.#textHighlights$, - this.annotations$.pipe(map(annotations => this.getVisibleAnnotations(Object.values(annotations), viewMode))), + this._viewModeService.viewMode$ + .pipe( + switchMap(viewMode => + iif( + () => viewMode === ViewModes.TEXT_HIGHLIGHTS, + this.#textHighlights$, + this.annotations$.pipe(map(annotations => this.#getVisibleAnnotations(Object.values(annotations), viewMode))), + ), ), - ), - shareDistinctLast(), - ); - this.numberOfVisibleAnnotations$ = this.visibleAnnotations$.pipe(map(annotations => annotations.length)); - } - - get visibleAnnotations() { - return firstValueFrom(this.visibleAnnotations$); + tap(annotations => this.setEntities(annotations)), + ) + .subscribe(); } get annotations() { @@ -86,7 +80,7 @@ export class FileDataService { return this.#redactionLog$.pipe( withLatestFrom(this._state.file$), map(([redactionLog, file]) => this.#buildAnnotations(redactionLog, file)), - tap(() => this.checkMissingTypes()), + tap(() => this.#checkMissingTypes()), map(annotations => this._userPreferenceService.areDevFeaturesEnabled ? annotations : annotations.filter(a => !a.isFalsePositive), ), @@ -102,20 +96,10 @@ export class FileDataService { this._logger.info('[ANNOTATIONS] Load annotations'); - await this.loadViewedPages(file); + await this.#loadViewedPages(file); await this.loadRedactionLog(); } - checkMissingTypes() { - if (this.missingTypes.size > 0) { - this._toaster.error(_('error.missing-types'), { - disableTimeOut: true, - params: { missingTypes: Array.from(this.missingTypes).join(', ') }, - }); - this.missingTypes.clear(); - } - } - async loadTextHighlights() { const redactionPerColor = this._textHighlightsService.getTextHighlights(this._state.dossierId, this._state.fileId); const textHighlights = this.#buildTextHighlights(await firstValueFrom(redactionPerColor)); @@ -124,34 +108,11 @@ export class FileDataService { return textHighlights; } - async loadViewedPages(file: File) { - if (!this._permissionsService.canMarkPagesAsViewed(file)) { - this.viewedPages = []; - return; - } - - this.viewedPages = await firstValueFrom(this._viewedPagesService.getViewedPages(file.dossierId, file.fileId)); - } - loadRedactionLog() { const redactionLog$ = this._redactionLogService.getRedactionLog(this._state.dossierId, this._state.fileId); return firstValueFrom(redactionLog$.pipe(tap(redactionLog => this.#redactionLog$.next(redactionLog)))); } - getVisibleAnnotations(annotations: AnnotationWrapper[], viewMode: ViewMode) { - return annotations.filter(annotation => { - if (viewMode === 'STANDARD') { - return !annotation.isChangeLogRemoved; - } - - if (viewMode === 'DELTA') { - return annotation.isChangeLogEntry; - } - - return annotation.previewAnnotation; - }); - } - updateHiddenAnnotations(viewerAnnotations: Annotation[], hidden: boolean) { const annotationId = viewerAnnotations[0].Id; if (hidden) { @@ -165,6 +126,39 @@ export class FileDataService { return this.hiddenAnnotations.has(annotationId); } + #checkMissingTypes() { + if (this.missingTypes.size > 0) { + this._toaster.error(_('error.missing-types'), { + disableTimeOut: true, + params: { missingTypes: Array.from(this.missingTypes).join(', ') }, + }); + this.missingTypes.clear(); + } + } + + async #loadViewedPages(file: File) { + if (!this._permissionsService.canMarkPagesAsViewed(file)) { + this.viewedPages = []; + return; + } + + this.viewedPages = await firstValueFrom(this._viewedPagesService.getViewedPages(file.dossierId, file.fileId)); + } + + #getVisibleAnnotations(annotations: AnnotationWrapper[], viewMode: ViewMode) { + return annotations.filter(annotation => { + if (viewMode === 'STANDARD') { + return !annotation.isChangeLogRemoved; + } + + if (viewMode === 'DELTA') { + return annotation.isChangeLogEntry; + } + + return annotation.previewAnnotation; + }); + } + #buildTextHighlights(redactionPerColor: Record): AnnotationWrapper[] { if (!redactionPerColor) { return [];