From 6f7f23f97f559e43be2b227eca63b1d1eb829f96 Mon Sep 17 00:00:00 2001 From: Dan Percic Date: Wed, 18 Aug 2021 18:13:51 +0300 Subject: [PATCH] refactor annotation filters --- .../file-workload.component.html | 17 ++- .../file-workload/file-workload.component.ts | 120 ++++++++++-------- .../file-preview-screen.component.ts | 36 +++--- .../services/annotation-processing.service.ts | 24 ++-- .../simple-doughnut-chart.component.html | 4 +- .../app/services/user-preference.service.ts | 17 ++- libs/common-ui | 2 +- 7 files changed, 117 insertions(+), 103 deletions(-) diff --git a/apps/red-ui/src/app/modules/dossier/components/file-workload/file-workload.component.html b/apps/red-ui/src/app/modules/dossier/components/file-workload/file-workload.component.html index 74d473a9f..c52caa0e3 100644 --- a/apps/red-ui/src/app/modules/dossier/components/file-workload/file-workload.component.html +++ b/apps/red-ui/src/app/modules/dossier/components/file-workload/file-workload.component.html @@ -1,3 +1,6 @@ + + +
+
+
@@ -34,6 +39,7 @@
+
+
{{ activeViewerPage }} - - {{ activeAnnotationsLength || 0 }} - + {{ activeAnnotations?.length || 0 }} +
@@ -126,7 +133,7 @@ redactionHasScrollbar tabindex="1" > - + (); @Input() selectedAnnotations: AnnotationWrapper[]; @Input() activeViewerPage: number; @Input() shouldDeselectAnnotationsOnPageChange: boolean; - @Input() dialogRef: MatDialogRef; + @Input() dialogRef: MatDialogRef; @Input() fileData: FileDataModel; @Input() hideSkipped: boolean; @Input() excludePages: boolean; - @Input() annotationActionsTemplate: TemplateRef; + @Input() annotationActionsTemplate: TemplateRef; @Input() viewer: WebViewerInstance; - @Output() shouldDeselectAnnotationsOnPageChangeChange = new EventEmitter(); - @Output() selectAnnotations = new EventEmitter(); - @Output() deselectAnnotations = new EventEmitter(); - @Output() selectPage = new EventEmitter(); - @Output() toggleSkipped = new EventEmitter(); - @Output() annotationsChanged = new EventEmitter(); - @Output() actionPerformed = new EventEmitter(); + @Output() readonly shouldDeselectAnnotationsOnPageChangeChange = new EventEmitter(); + @Output() readonly selectAnnotations = new EventEmitter< + AnnotationWrapper[] | { annotations: AnnotationWrapper[]; multiSelect: boolean } + >(); + @Output() readonly deselectAnnotations = new EventEmitter(); + @Output() readonly selectPage = new EventEmitter(); + @Output() readonly toggleSkipped = new EventEmitter(); + @Output() readonly annotationsChanged = new EventEmitter(); + @Output() readonly actionPerformed = new EventEmitter(); displayedPages: number[] = []; pagesPanelActive = true; - @ViewChildren(CommentsComponent) annotationCommentsComponents: QueryList; - @ViewChild('annotationsElement') private _annotationsElement: ElementRef; - @ViewChild('quickNavigation') private _quickNavigationElement: ElementRef; + @ViewChildren(CommentsComponent) readonly annotationCommentsComponents: QueryList; + @ViewChild('annotationsElement') private readonly _annotationsElement: ElementRef; + @ViewChild('quickNavigation') private readonly _quickNavigationElement: ElementRef; constructor( private readonly _permissionsService: PermissionsService, private readonly _changeDetectorRef: ChangeDetectorRef, + private readonly _filterService: FilterService, private readonly _annotationProcessingService: AnnotationProcessingService ) {} - private _annotations: AnnotationWrapper[]; + private _annotations$ = new BehaviorSubject([]); + readonly displayedAnnotations$ = this._displayedAnnotations$; @Input() set annotations(value: AnnotationWrapper[]) { - this._annotations = value; + this._annotations$.next(value); } private _multiSelectActive = false; @@ -89,8 +95,8 @@ export class FileWorkloadComponent { return this.fileData?.fileStatus?.isProcessing; } - get activeAnnotationsLength(): number | undefined { - return this.displayedAnnotations[this.activeViewerPage]?.annotations?.length; + get activeAnnotations(): AnnotationWrapper[] | undefined { + return this.displayedAnnotations.get(this.activeViewerPage); } get isReadOnly(): boolean { @@ -130,27 +136,25 @@ export class FileWorkloadComponent { } selectAllOnActivePage() { - this.selectAnnotations.emit(this.displayedAnnotations[this.activeViewerPage].annotations); - this._changeDetectorRef.detectChanges(); + this.selectAnnotations.emit(this.activeAnnotations); } - deselectAllOnActivePage() { - this.deselectAnnotations.emit(this.displayedAnnotations[this.activeViewerPage].annotations); - this._changeDetectorRef.detectChanges(); + deselectAllOnActivePage(): void { + this.deselectAnnotations.emit(this.activeAnnotations); } - @Debounce(0) - filtersChanged(filters: { primary: NestedFilter[]; secondary?: NestedFilter[] }) { - this.displayedAnnotations = this._annotationProcessingService.filterAndGroupAnnotations( - this._annotations, - filters.primary, - filters.secondary - ); - this.displayedPages = Object.keys(this.displayedAnnotations).map(key => Number(key)); - this._changeDetectorRef.markForCheck(); + private _filterAnnotations( + annotations: AnnotationWrapper[], + primary: NestedFilter[], + secondary: NestedFilter[] = [] + ): Map { + if (!primary) return; + this.displayedAnnotations = this._annotationProcessingService.filterAndGroupAnnotations(annotations, primary, secondary); + this.displayedPages = [...this.displayedAnnotations.keys()]; + return this.displayedAnnotations; } - annotationClicked(annotation: AnnotationWrapper, $event: MouseEvent) { + annotationClicked(annotation: AnnotationWrapper, $event: MouseEvent): void { this.pagesPanelActive = false; this.logAnnotation(annotation); if (this.isSelected(annotation)) { @@ -167,7 +171,7 @@ export class FileWorkloadComponent { } @HostListener('window:keyup', ['$event']) - handleKeyEvent($event: KeyboardEvent) { + handleKeyEvent($event: KeyboardEvent): void { if ( !ALL_HOTKEY_ARRAY.includes($event.key) || this.dialogRef?.getState() === MatDialogState.OPEN || @@ -205,32 +209,32 @@ export class FileWorkloadComponent { this._changeDetectorRef.detectChanges(); } - scrollAnnotations() { + scrollAnnotations(): void { if (this._firstSelectedAnnotation?.pageNumber === this.activeViewerPage) { return; } this.scrollAnnotationsToPage(this.activeViewerPage, 'always'); } - scrollAnnotationsToPage(page: number, mode: 'always' | 'if-needed' = 'if-needed') { + scrollAnnotationsToPage(page: number, mode: 'always' | 'if-needed' = 'if-needed'): void { if (this._annotationsElement) { - const elements: any[] = this._annotationsElement.nativeElement.querySelectorAll(`div[anotation-page-header="${page}"]`); + const elements = this._annotationsElement.nativeElement.querySelectorAll(`div[anotation-page-header="${page}"]`); FileWorkloadComponent._scrollToFirstElement(elements, mode); } } @Debounce() - scrollToSelectedAnnotation() { + scrollToSelectedAnnotation(): void { if (!this.selectedAnnotations || this.selectedAnnotations.length === 0 || !this._annotationsElement) { return; } - const elements: any[] = this._annotationsElement.nativeElement.querySelectorAll( + const elements = this._annotationsElement.nativeElement.querySelectorAll( `div[annotation-id="${this._firstSelectedAnnotation?.id}"].active` ); FileWorkloadComponent._scrollToFirstElement(elements); } - scrollQuickNavigation() { + scrollQuickNavigation(): void { let quickNavPageIndex = this.displayedPages.findIndex(p => p >= this.activeViewerPage); if (quickNavPageIndex === -1 || this.displayedPages[quickNavPageIndex] !== this.activeViewerPage) { quickNavPageIndex = Math.max(0, quickNavPageIndex - 1); @@ -238,39 +242,47 @@ export class FileWorkloadComponent { this._scrollQuickNavigationToPage(this.displayedPages[quickNavPageIndex]); } - scrollQuickNavFirst() { + scrollQuickNavFirst(): void { this.selectPage.emit(1); } - scrollQuickNavLast() { + scrollQuickNavLast(): void { this.selectPage.emit(this.fileData.fileStatus.numberOfPages); } - pageSelectedByClick($event: number) { + pageSelectedByClick($event: number): void { this.pagesPanelActive = true; this.selectPage.emit($event); } - preventKeyDefault($event: KeyboardEvent) { + preventKeyDefault($event: KeyboardEvent): void { if (COMMAND_KEY_ARRAY.includes($event.key) && !(($event.target as any).localName === 'input')) { $event.preventDefault(); } } - jumpToPreviousWithAnnotations() { + jumpToPreviousWithAnnotations(): void { this.selectPage.emit(this._prevPageWithAnnotations()); } - jumpToNextWithAnnotations() { + jumpToNextWithAnnotations(): void { this.selectPage.emit(this._nextPageWithAnnotations()); } + private get _displayedAnnotations$(): Observable> { + const primary$ = this._filterService.getFilterModels$('primaryFilters'); + const secondary$ = this._filterService.getFilterModels$('secondaryFilters'); + return combineLatest([this._annotations$, primary$, secondary$]).pipe( + map(([annotations, primary, secondary]) => this._filterAnnotations(annotations, primary, secondary)) + ); + } + private _selectFirstAnnotationOnCurrentPageIfNecessary() { if ( (!this._firstSelectedAnnotation || this.activeViewerPage !== this._firstSelectedAnnotation.pageNumber) && this.displayedPages.indexOf(this.activeViewerPage) >= 0 ) { - this.selectAnnotations.emit([this.displayedAnnotations[this.activeViewerPage].annotations[0]]); + this.selectAnnotations.emit([this.activeAnnotations[0]]); } } @@ -278,28 +290,28 @@ export class FileWorkloadComponent { if (!this._firstSelectedAnnotation || this.activeViewerPage !== this._firstSelectedAnnotation.pageNumber) { if (this.displayedPages.indexOf(this.activeViewerPage) !== -1) { // Displayed page has annotations - return this.selectAnnotations.emit([this.displayedAnnotations[this.activeViewerPage].annotations[0]]); + return this.selectAnnotations.emit([this.activeAnnotations[0]]); } // Displayed page doesn't have annotations if ($event.key === 'ArrowDown') { const nextPage = this._nextPageWithAnnotations(); this.shouldDeselectAnnotationsOnPageChange = false; this.shouldDeselectAnnotationsOnPageChangeChange.emit(false); - this.selectAnnotations.emit([this.displayedAnnotations[nextPage].annotations[0]]); + this.selectAnnotations.emit([this.displayedAnnotations.get(nextPage)[0]]); return; } const prevPage = this._prevPageWithAnnotations(); this.shouldDeselectAnnotationsOnPageChange = false; this.shouldDeselectAnnotationsOnPageChangeChange.emit(false); - const prevPageAnnotations = this.displayedAnnotations[prevPage].annotations; + const prevPageAnnotations = this.displayedAnnotations.get(prevPage); this.selectAnnotations.emit([prevPageAnnotations[prevPageAnnotations.length - 1]]); return; } const page = this._firstSelectedAnnotation.pageNumber; const pageIdx = this.displayedPages.indexOf(page); - const annotationsOnPage = this.displayedAnnotations[page].annotations; + const annotationsOnPage = this.displayedAnnotations.get(page); const idx = annotationsOnPage.findIndex(a => a.id === this._firstSelectedAnnotation.id); if ($event.key === 'ArrowDown') { @@ -308,7 +320,7 @@ export class FileWorkloadComponent { this.selectAnnotations.emit([annotationsOnPage[idx + 1]]); } else if (pageIdx + 1 < this.displayedPages.length) { // If not last page - const nextPageAnnotations = this.displayedAnnotations[this.displayedPages[pageIdx + 1]].annotations; + const nextPageAnnotations = this.displayedAnnotations.get(this.displayedPages[pageIdx + 1]); this.shouldDeselectAnnotationsOnPageChange = false; this.shouldDeselectAnnotationsOnPageChangeChange.emit(false); this.selectAnnotations.emit([nextPageAnnotations[0]]); @@ -318,7 +330,7 @@ export class FileWorkloadComponent { this.selectAnnotations.emit([annotationsOnPage[idx - 1]]); } else if (pageIdx) { // If not first page - const prevPageAnnotations = this.displayedAnnotations[this.displayedPages[pageIdx - 1]].annotations; + const prevPageAnnotations = this.displayedAnnotations.get(this.displayedPages[pageIdx - 1]); this.shouldDeselectAnnotationsOnPageChange = false; this.shouldDeselectAnnotationsOnPageChangeChange.emit(false); this.selectAnnotations.emit([prevPageAnnotations[prevPageAnnotations.length - 1]]); @@ -382,7 +394,7 @@ export class FileWorkloadComponent { private _scrollQuickNavigationToPage(page: number) { if (this._quickNavigationElement) { - const elements: any[] = this._quickNavigationElement.nativeElement.querySelectorAll(`#quick-nav-page-${page}`); + const elements = this._quickNavigationElement.nativeElement.querySelectorAll(`#quick-nav-page-${page}`); FileWorkloadComponent._scrollToFirstElement(elements); } } diff --git a/apps/red-ui/src/app/modules/dossier/screens/file-preview-screen/file-preview-screen.component.ts b/apps/red-ui/src/app/modules/dossier/screens/file-preview-screen/file-preview-screen.component.ts index 639c7e0c9..f462b5161 100644 --- a/apps/red-ui/src/app/modules/dossier/screens/file-preview-screen/file-preview-screen.component.ts +++ b/apps/red-ui/src/app/modules/dossier/screens/file-preview-screen/file-preview-screen.component.ts @@ -48,7 +48,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni readonly circleButtonTypes = CircleButtonTypes; readonly translations = fileStatusTranslations; - dialogRef: MatDialogRef; + dialogRef: MatDialogRef; viewMode: ViewMode = 'STANDARD'; fullScreen = false; editingReviewer = false; @@ -61,11 +61,11 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni displayPDFViewer = false; viewDocumentInfo = false; excludePages = false; - @ViewChild(PdfViewerComponent) viewerComponent: PdfViewerComponent; private _instance: WebViewerInstance; private _lastPage: string; private _reloadFileOnReanalysis = false; - @ViewChild('fileWorkloadComponent') private _workloadComponent: FileWorkloadComponent; + @ViewChild(PdfViewerComponent) readonly viewerComponent: PdfViewerComponent; + @ViewChild('fileWorkloadComponent') private readonly _workloadComponent: FileWorkloadComponent; @ViewChild('annotationFilterTemplate', { read: TemplateRef, static: true @@ -119,11 +119,11 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni return this.annotationData ? this.annotationData.visibleAnnotations : []; } - get activeViewer() { + get activeViewer(): WebViewerInstance { return this._instance; } - get activeViewerPage() { + get activeViewerPage(): number { const currentPage = this._instance?.docViewer?.getCurrentPage(); if (!currentPage) { return 0; @@ -135,11 +135,11 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni : (currentPage + 1) / 2; } - get canSwitchToRedactedView() { + get canSwitchToRedactedView(): boolean { return this.fileData && !this.fileData.fileStatus.analysisRequired && !this.fileData.fileStatus.excluded; } - get canSwitchToDeltaView() { + get canSwitchToDeltaView(): boolean { return this.fileData?.redactionChangeLog?.redactionLogEntry?.length > 0 && !this.fileData.fileStatus.excluded; } @@ -147,15 +147,15 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni return !this.editingReviewer && (this.permissionsService.canAssignUser() || this.permissionsService.canAssignToSelf()); } - get displayData() { + get displayData(): Blob { return this.fileData?.fileData; } - get dossierId() { + get dossierId(): string { return this.appStateService.activeDossierId; } - get fileId() { + get fileId(): string { return this.appStateService.activeFileId; } @@ -167,7 +167,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni return this.appStateService.activeFile.fileStatus.lastReviewer; } - get assignOrChangeReviewerTooltip() { + get assignOrChangeReviewerTooltip(): string { return this.currentReviewer ? this._translateService.instant('file-preview.change-reviewer') : this._translateService.instant('file-preview.assign-reviewer'); @@ -195,7 +195,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni ); } - updateViewMode() { + updateViewMode(): void { const annotations = this._getAnnotations(a => a.getCustomData('redacto-manager')); const redactions = annotations.filter(a => a.getCustomData('redaction')); @@ -230,12 +230,12 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni this._updateCanPerformActions(); } - ngOnDetach() { + ngOnDetach(): void { this.displayPDFViewer = false; super.ngOnDestroy(); } - async ngOnAttach(previousRoute: ActivatedRouteSnapshot) { + async ngOnAttach(previousRoute: ActivatedRouteSnapshot): Promise { if (!this.appStateService.activeFile.canBeOpened) { await this._router.navigate(['/main/dossiers/' + this.dossierId]); return; @@ -245,14 +245,14 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni this._lastPage = previousRoute.queryParams.page; } - async ngOnInit() { - await this._loadFileData(false); + async ngOnInit(): Promise { + await this._loadFileData(); this.displayPDFViewer = true; this._updateCanPerformActions(); this._subscribeToFileUpdates(); } - rebuildFilters(deletePreviousAnnotations: boolean = false) { + rebuildFilters(deletePreviousAnnotations = false): void { const startTime = new Date().getTime(); if (deletePreviousAnnotations) { const annotationsToDelete = this._instance?.annotManager?.getAnnotationsList() || []; @@ -571,7 +571,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni !this.viewerComponent?.utils.isCompareMode; } - private async _loadFileData(performUpdate: boolean = false): Promise { + private async _loadFileData(performUpdate = false): Promise { const fileData = await this._fileDownloadService.loadActiveFileData().toPromise(); if (!fileData.fileStatus?.isPending && !fileData.fileStatus?.isError) { if (performUpdate) { diff --git a/apps/red-ui/src/app/modules/dossier/services/annotation-processing.service.ts b/apps/red-ui/src/app/modules/dossier/services/annotation-processing.service.ts index 0d8f9e6db..ee64fb5a7 100644 --- a/apps/red-ui/src/app/modules/dossier/services/annotation-processing.service.ts +++ b/apps/red-ui/src/app/modules/dossier/services/annotation-processing.service.ts @@ -1,10 +1,9 @@ import { Injectable } from '@angular/core'; import { AnnotationWrapper } from '@models/file/annotation.wrapper'; import { SuperTypeSorter } from '@utils/sorters/super-type-sorter'; -import { handleCheckedValue } from '@iqser/common-ui'; +import { handleCheckedValue, NestedFilter } from '@iqser/common-ui'; import { annotationTypesTranslations } from '../../../translations/annotation-types-translations'; import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; -import { NestedFilter } from '@iqser/common-ui'; @Injectable() export class AnnotationProcessingService { @@ -81,8 +80,8 @@ export class AnnotationProcessingService { annotations: AnnotationWrapper[], primaryFilters: NestedFilter[], secondaryFilters?: NestedFilter[] - ): { [key: number]: { annotations: AnnotationWrapper[] } } { - const obj = {}; + ): Map { + const obj = new Map(); const primaryFlatFilters = this._getFlatFilters(primaryFilters, f => f.checked); const secondaryFlatFilters = this._getFlatFilters(secondaryFilters, f => f.checked); @@ -103,22 +102,15 @@ export class AnnotationProcessingService { const pageNumber = annotation.pageNumber; - if (!obj[pageNumber]) { - obj[pageNumber] = { - annotations: [], - hint: 0, - redaction: 0, - request: 0, - skipped: 0 - }; + if (!obj.has(pageNumber)) { + obj.set(pageNumber, []); } - obj[pageNumber].annotations.push(annotation); - obj[pageNumber][annotation.superType]++; + obj.get(pageNumber).push(annotation); } - Object.keys(obj).map(page => { - obj[page].annotations = this._sortAnnotations(obj[page].annotations); + obj.forEach((values, page) => { + obj.set(page, this._sortAnnotations(values)); }); return obj; diff --git a/apps/red-ui/src/app/modules/shared/components/simple-doughnut-chart/simple-doughnut-chart.component.html b/apps/red-ui/src/app/modules/shared/components/simple-doughnut-chart/simple-doughnut-chart.component.html index 5c0ed8df1..dbe733b5e 100644 --- a/apps/red-ui/src/app/modules/shared/components/simple-doughnut-chart/simple-doughnut-chart.component.html +++ b/apps/red-ui/src/app/modules/shared/components/simple-doughnut-chart/simple-doughnut-chart.component.html @@ -25,10 +25,10 @@