From c9016f0cccf9de521d456b41bb914ea9bd09e5f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adina=20=C8=9Aeudan?= Date: Thu, 15 Apr 2021 19:27:35 +0300 Subject: [PATCH] Multi annotation select --- .../annotation-actions.component.html | 64 +----- .../annotation-actions.component.ts | 17 -- .../annotation-remove-actions.component.html | 37 ++++ .../annotation-remove-actions.component.scss | 0 .../annotation-remove-actions.component.ts | 98 +++++++++ .../file-workload.component.html | 204 ++++++++++-------- .../file-workload.component.scss | 63 +++++- .../file-workload/file-workload.component.ts | 84 ++++++-- .../page-indicator.component.html | 1 + .../page-indicator.component.scss | 10 + .../page-indicator.component.ts | 1 + .../pdf-viewer/pdf-viewer.component.ts | 40 +++- .../app/modules/projects/projects.module.ts | 2 + .../file-preview-screen.component.html | 7 +- .../file-preview-screen.component.scss | 140 +----------- .../file-preview-screen.component.ts | 32 ++- .../services/annotation-actions.service.ts | 30 +-- apps/red-ui/src/assets/i18n/en.json | 1 + apps/red-ui/src/assets/styles/red-button.scss | 4 + .../src/assets/styles/red-components.scss | 4 + 20 files changed, 490 insertions(+), 349 deletions(-) create mode 100644 apps/red-ui/src/app/modules/projects/components/annotation-remove-actions/annotation-remove-actions.component.html create mode 100644 apps/red-ui/src/app/modules/projects/components/annotation-remove-actions/annotation-remove-actions.component.scss create mode 100644 apps/red-ui/src/app/modules/projects/components/annotation-remove-actions/annotation-remove-actions.component.ts diff --git a/apps/red-ui/src/app/modules/projects/components/annotation-actions/annotation-actions.component.html b/apps/red-ui/src/app/modules/projects/components/annotation-actions/annotation-actions.component.html index 842d6efe9..aebc24edf 100644 --- a/apps/red-ui/src/app/modules/projects/components/annotation-actions/annotation-actions.component.html +++ b/apps/red-ui/src/app/modules/projects/components/annotation-actions/annotation-actions.component.html @@ -20,7 +20,7 @@ - - - - - - - -
- -
-
-
- -
-
- -
- -
-
-
- -
-
-
+ diff --git a/apps/red-ui/src/app/modules/projects/components/annotation-actions/annotation-actions.component.ts b/apps/red-ui/src/app/modules/projects/components/annotation-actions/annotation-actions.component.ts index 89d1faaf8..3a0b77169 100644 --- a/apps/red-ui/src/app/modules/projects/components/annotation-actions/annotation-actions.component.ts +++ b/apps/red-ui/src/app/modules/projects/components/annotation-actions/annotation-actions.component.ts @@ -48,21 +48,4 @@ export class AnnotationActionsComponent implements OnInit { $event.stopPropagation(); this.viewer.annotManager.showAnnotation(this.viewerAnnotation); } - - public openMenu($event: MouseEvent) { - $event.preventDefault(); - this.menuOpen = true; - } - - public onMenuClosed() { - this.menuOpen = false; - } - - get suggestionColor() { - return this.appStateService.getDictionaryColor('suggestion'); - } - - get dictionaryColor() { - return this.appStateService.getDictionaryColor('suggestion-add-dictionary'); - } } diff --git a/apps/red-ui/src/app/modules/projects/components/annotation-remove-actions/annotation-remove-actions.component.html b/apps/red-ui/src/app/modules/projects/components/annotation-remove-actions/annotation-remove-actions.component.html new file mode 100644 index 000000000..13a9889e0 --- /dev/null +++ b/apps/red-ui/src/app/modules/projects/components/annotation-remove-actions/annotation-remove-actions.component.html @@ -0,0 +1,37 @@ + + + + + + + +
+ +
+
+
+ +
+
+ +
+ +
+
+
diff --git a/apps/red-ui/src/app/modules/projects/components/annotation-remove-actions/annotation-remove-actions.component.scss b/apps/red-ui/src/app/modules/projects/components/annotation-remove-actions/annotation-remove-actions.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/apps/red-ui/src/app/modules/projects/components/annotation-remove-actions/annotation-remove-actions.component.ts b/apps/red-ui/src/app/modules/projects/components/annotation-remove-actions/annotation-remove-actions.component.ts new file mode 100644 index 000000000..d35c30b10 --- /dev/null +++ b/apps/red-ui/src/app/modules/projects/components/annotation-remove-actions/annotation-remove-actions.component.ts @@ -0,0 +1,98 @@ +import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'; +import { AppStateService } from '../../../../state/app-state.service'; +import { AnnotationWrapper } from '../../../../models/file/annotation.wrapper'; +import { AnnotationActionsService } from '../../services/annotation-actions.service'; +import { AnnotationPermissions } from '../../../../models/file/annotation.permissions'; +import { PermissionsService } from '../../../../services/permissions.service'; +import { MatMenuTrigger } from '@angular/material/menu'; + +@Component({ + selector: 'redaction-annotation-remove-actions', + templateUrl: './annotation-remove-actions.component.html', + styleUrls: ['./annotation-remove-actions.component.scss'] +}) +export class AnnotationRemoveActionsComponent implements OnInit { + @Output() menuOpenChange = new EventEmitter(); + @Input() annotationsChanged: EventEmitter; + @Input() menuOpen: boolean; + @Input() btnType: 'dark-bg' | 'primary' = 'dark-bg'; + @Input() tooltipPosition: 'before' | 'above' = 'before'; + + @ViewChild(MatMenuTrigger) matMenuTrigger: MatMenuTrigger; + public permissions: { + canRemoveOrSuggestToRemoveOnlyHere: boolean; + canPerformMultipleRemoveActions: boolean; + canNotPerformMultipleRemoveActions: boolean; + canRemoveOrSuggestToRemoveFromDictionary: boolean; + canMarkAsFalsePositive: boolean; + }; + + constructor( + public readonly appStateService: AppStateService, + private readonly _annotationActionsService: AnnotationActionsService, + private readonly _permissionsService: PermissionsService + ) {} + + private _annotations: AnnotationWrapper[]; + + public get annotations(): AnnotationWrapper[] { + return this._annotations; + } + + @Input() + public set annotations(value: AnnotationWrapper[]) { + this._annotations = value.filter((a) => a !== undefined); + this._setPermissions(); + } + + public get dictionaryColor() { + return this.appStateService.getDictionaryColor('suggestion-add-dictionary'); + } + + public get suggestionColor() { + return this.appStateService.getDictionaryColor('suggestion'); + } + + ngOnInit(): void {} + + public openMenu($event: MouseEvent) { + $event.stopPropagation(); + this.matMenuTrigger.openMenu(); + this.menuOpenChange.emit(true); + } + + public onMenuClosed() { + this.menuOpenChange.emit(false); + } + + public suggestRemoveAnnotations($event, removeFromDict: boolean) { + $event.stopPropagation(); + this._annotationActionsService.suggestRemoveAnnotation($event, this.annotations, removeFromDict, this.annotationsChanged); + } + + public markAsFalsePositive($event) { + this._annotationActionsService.markAsFalsePositive($event, this.annotations, this.annotationsChanged); + } + + private _setPermissions() { + this.permissions = { + canRemoveOrSuggestToRemoveOnlyHere: this._annotationsPermissions(['canRemoveOrSuggestToRemoveOnlyHere'], true), + canPerformMultipleRemoveActions: this._annotationsPermissions(['canPerformMultipleRemoveActions'], true), + canNotPerformMultipleRemoveActions: this._annotationsPermissions(['canPerformMultipleRemoveActions'], false), + canRemoveOrSuggestToRemoveFromDictionary: this._annotationsPermissions(['canRemoveOrSuggestToRemoveFromDictionary'], true), + canMarkAsFalsePositive: this._annotationsPermissions(['canMarkAsFalsePositive', 'canMarkTextOnlyAsFalsePositive'], true) + }; + } + + private _annotationsPermissions(keys: string[], expectedValue: boolean): boolean { + return this.annotations.reduce((prevValue, annotation) => { + const annotationPermissions = AnnotationPermissions.forUser( + this._permissionsService.isManagerAndOwner(), + this._permissionsService.currentUser, + annotation + ); + const hasAtLeastOnePermission = keys.reduce((acc, key) => acc || annotationPermissions[key] === expectedValue, false); + return prevValue && hasAtLeastOnePermission; + }, true); + } +} diff --git a/apps/red-ui/src/app/modules/projects/components/file-workload/file-workload.component.html b/apps/red-ui/src/app/modules/projects/components/file-workload/file-workload.component.html index bafbba232..fab9a8319 100644 --- a/apps/red-ui/src/app/modules/projects/components/file-workload/file-workload.component.html +++ b/apps/red-ui/src/app/modules/projects/components/file-workload/file-workload.component.html @@ -1,5 +1,6 @@
+
SELECT
-
-
- -
-
- - -
-
- +
+
+
+ + {{ selectedAnnotations?.length || 0 }} selected +
+
- -
-
- {{ activeViewerPage }} - {{ displayedAnnotations[activeViewerPage]?.annotations?.length || 0 }} - -
- +
-
- {{ 'file-preview.no-annotations-for-page' | translate }} +
+ +
+
+ + +
+
+ +
+
+ +
+
+ {{ activeViewerPage }} - {{ displayedAnnotations[activeViewerPage]?.annotations?.length || 0 }} + + +
+
All
+
None
+
-
-
- -
- -
-
- {{ annotation.typeLabel | translate }} +
+ {{ 'file-preview.no-annotations-for-page' | translate }} +
+ +
+
+
+ +
+ +
+
+ {{ annotation.typeLabel | translate }} +
+
+ {{ annotation.descriptor | translate }}: {{ annotation.dictionary | humanize: false }} +
+
+ : {{ annotation.content }} +
+ {{ annotation.id }}
-
- {{ annotation.descriptor | translate }}: {{ annotation.dictionary | humanize: false }} -
-
- : {{ annotation.content }} + + +
+
- - - - - - - - -
-
- + + +
diff --git a/apps/red-ui/src/app/modules/projects/components/file-workload/file-workload.component.scss b/apps/red-ui/src/app/modules/projects/components/file-workload/file-workload.component.scss index 1ead7e879..91d22cdea 100644 --- a/apps/red-ui/src/app/modules/projects/components/file-workload/file-workload.component.scss +++ b/apps/red-ui/src/app/modules/projects/components/file-workload/file-workload.component.scss @@ -2,11 +2,51 @@ @import '../../../../../assets/styles/red-mixins'; .right-content { + flex-direction: column; + .no-annotations { padding: 24px; text-align: center; } + .multi-select { + min-height: 40px; + background: $primary; + display: flex; + justify-content: space-between; + align-items: center; + padding: 0 8px 0 16px; + color: $white; + + .selected-wrapper { + display: flex; + align-items: center; + + .select-oval, + .selection-icon { + margin-right: 8px; + color: inherit; + } + + .all-caps-label { + opacity: 1; + } + + redaction-annotation-remove-actions { + margin-left: 16px; + } + } + } + + .annotations-wrapper { + display: flex; + height: 100%; + + &.multi-select-active { + height: calc(100% - 40px); + } + } + .quick-navigation, .annotations { overflow-y: scroll; @@ -64,7 +104,16 @@ padding: 0 10px; display: flex; align-items: center; + justify-content: space-between; background-color: $grey-6; + + > div { + display: flex; + + > div:not(:last-child) { + margin-right: 8px; + } + } } .annotations { @@ -76,13 +125,23 @@ display: flex; border-bottom: 1px solid $separator; - .active-marker { + .active-bar-marker { min-width: 4px; min-height: 100%; } + .active-icon-marker-container { + min-width: 20px; + + .active-icon-marker { + color: $primary; + width: 20px; + height: 20px; + } + } + &.active { - .active-marker { + &:not(.multi-select-active) .active-bar-marker { background-color: $primary; } } diff --git a/apps/red-ui/src/app/modules/projects/components/file-workload/file-workload.component.ts b/apps/red-ui/src/app/modules/projects/components/file-workload/file-workload.component.ts index a56d1d7f9..53af97aa6 100644 --- a/apps/red-ui/src/app/modules/projects/components/file-workload/file-workload.component.ts +++ b/apps/red-ui/src/app/modules/projects/components/file-workload/file-workload.component.ts @@ -6,6 +6,8 @@ import { MatDialogRef, MatDialogState } from '@angular/material/dialog'; import scrollIntoView from 'scroll-into-view-if-needed'; import { debounce } from '../../../../utils/debounce'; import { FileDataModel } from '../../../../models/file/file-data.model'; +import { AnnotationPermissions } from '../../../../models/file/annotation.permissions'; +import { PermissionsService } from '../../../../services/permissions.service'; const COMMAND_KEY_ARRAY = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Escape']; const ALL_HOTKEY_ARRAY = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown']; @@ -22,21 +24,23 @@ export class FileWorkloadComponent { @Input() set annotations(value: AnnotationWrapper[]) { this._annotations = value; - // this.computeQuickNavButtonsState(); } @Input() selectedAnnotations: AnnotationWrapper[]; @Input() activeViewerPage: number; @Input() shouldDeselectAnnotationsOnPageChange: boolean; + @Output() shouldDeselectAnnotationsOnPageChangeChange = new EventEmitter(); @Input() dialogRef: MatDialogRef; @Input() annotationFilters: FilterModel[]; @Input() fileData: FileDataModel; @Input() hideSkipped: boolean; @Input() annotationActionsTemplate: TemplateRef; - @Output() selectAnnotation = new EventEmitter(); + @Output() selectAnnotations = new EventEmitter(); + @Output() deselectAnnotations = new EventEmitter(); @Output() selectPage = new EventEmitter(); @Output() toggleSkipped = new EventEmitter(); + @Output() annotationsChanged = new EventEmitter(); public quickScrollFirstEnabled = false; public quickScrollLastEnabled = false; @@ -46,7 +50,27 @@ export class FileWorkloadComponent { @ViewChild('annotationsElement') private _annotationsElement: ElementRef; @ViewChild('quickNavigation') private _quickNavigationElement: ElementRef; - constructor(private _changeDetectorRef: ChangeDetectorRef, private _annotationProcessingService: AnnotationProcessingService) {} + private _multiSelectActive = false; + + public get multiSelectActive(): boolean { + return this._multiSelectActive; + } + + public set multiSelectActive(value: boolean) { + this._multiSelectActive = value; + if (!value) { + this.selectAnnotations.emit(); + } else { + this.shouldDeselectAnnotationsOnPageChange = false; + this.shouldDeselectAnnotationsOnPageChangeChange.emit(false); + } + } + + constructor( + private _changeDetectorRef: ChangeDetectorRef, + private _annotationProcessingService: AnnotationProcessingService, + private readonly _permissionsService: PermissionsService + ) {} private get firstSelectedAnnotation() { return this.selectedAnnotations?.length ? this.selectedAnnotations[0] : null; @@ -71,6 +95,20 @@ export class FileWorkloadComponent { console.log(annotation); } + public pageHasSelection(page: number) { + return this.multiSelectActive && !!this.selectedAnnotations?.find((a) => a.pageNumber === page); + } + + public selectAllOnActivePage() { + this.selectAnnotations.emit(this.displayedAnnotations[this.activeViewerPage].annotations); + this._changeDetectorRef.detectChanges(); + } + + public deselectAllOnActivePage() { + this.deselectAnnotations.emit(this.displayedAnnotations[this.activeViewerPage].annotations); + this._changeDetectorRef.detectChanges(); + } + @debounce(0) public filtersChanged(filters: FilterModel[]) { this.displayedAnnotations = this._annotationProcessingService.filterAndGroupAnnotations(this._annotations, filters); @@ -88,9 +126,16 @@ export class FileWorkloadComponent { }, 0); } - public annotationClicked(annotation: AnnotationWrapper) { + public annotationClicked(annotation: AnnotationWrapper, $event: MouseEvent) { this.pagesPanelActive = false; - this.selectAnnotation.emit(annotation); + if (this.annotationIsSelected(annotation)) { + this.deselectAnnotations.emit([annotation]); + } else { + if (($event.ctrlKey || $event.metaKey) && this.selectedAnnotations.length > 0) { + this.multiSelectActive = true; + } + this.selectAnnotations.emit({ annotations: [annotation], multiSelect: true }); + } } @HostListener('window:keyup', ['$event']) @@ -107,15 +152,18 @@ export class FileWorkloadComponent { if ($event.key === 'ArrowRight') { this.pagesPanelActive = false; // if we activated annotationsPanel - select first annotation from this page in case there is no - // selected annotation on this page - if (!this.pagesPanelActive) { + // selected annotation on this page and not in multi select mode + if (!this.pagesPanelActive && !this.multiSelectActive) { this._selectFirstAnnotationOnCurrentPageIfNecessary(); } return; } if (!this.pagesPanelActive) { - this._navigateAnnotations($event); + // Disable annotation navigation in multi select mode => TODO: maybe implement selection on enter? + if (!this.multiSelectActive) { + this._navigateAnnotations($event); + } } else { this._navigatePages($event); } @@ -180,7 +228,7 @@ export class FileWorkloadComponent { (!this.firstSelectedAnnotation || this.activeViewerPage !== this.firstSelectedAnnotation.pageNumber) && this.displayedPages.indexOf(this.activeViewerPage) >= 0 ) { - this.selectAnnotation.emit(this.displayedAnnotations[this.activeViewerPage].annotations[0]); + this.selectAnnotations.emit([this.displayedAnnotations[this.activeViewerPage].annotations[0]]); } } @@ -189,18 +237,20 @@ export class FileWorkloadComponent { const pageIdx = this.displayedPages.indexOf(this.activeViewerPage); if (pageIdx !== -1) { // Displayed page has annotations - this.selectAnnotation.emit(this.displayedAnnotations[this.activeViewerPage].annotations[0]); + this.selectAnnotations.emit([this.displayedAnnotations[this.activeViewerPage].annotations[0]]); } else { // Displayed page doesn't have annotations if ($event.key === 'ArrowDown') { const nextPage = this._nextPageWithAnnotations(); this.shouldDeselectAnnotationsOnPageChange = false; - this.selectAnnotation.emit(this.displayedAnnotations[nextPage].annotations[0]); + this.shouldDeselectAnnotationsOnPageChangeChange.emit(false); + this.selectAnnotations.emit([this.displayedAnnotations[nextPage].annotations[0]]); } else { const prevPage = this._prevPageWithAnnotations(); this.shouldDeselectAnnotationsOnPageChange = false; + this.shouldDeselectAnnotationsOnPageChangeChange.emit(false); const prevPageAnnotations = this.displayedAnnotations[prevPage].annotations; - this.selectAnnotation.emit(prevPageAnnotations[prevPageAnnotations.length - 1]); + this.selectAnnotations.emit([prevPageAnnotations[prevPageAnnotations.length - 1]]); } } } else { @@ -212,22 +262,24 @@ export class FileWorkloadComponent { if ($event.key === 'ArrowDown') { if (idx + 1 !== annotationsOnPage.length) { // If not last item in page - this.selectAnnotation.emit(annotationsOnPage[idx + 1]); + 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; this.shouldDeselectAnnotationsOnPageChange = false; - this.selectAnnotation.emit(nextPageAnnotations[0]); + this.shouldDeselectAnnotationsOnPageChangeChange.emit(false); + this.selectAnnotations.emit([nextPageAnnotations[0]]); } } else { if (idx !== 0) { // If not first item in page - this.selectAnnotation.emit(annotationsOnPage[idx - 1]); + this.selectAnnotations.emit([annotationsOnPage[idx - 1]]); } else if (pageIdx) { // If not first page const prevPageAnnotations = this.displayedAnnotations[this.displayedPages[pageIdx - 1]].annotations; this.shouldDeselectAnnotationsOnPageChange = false; - this.selectAnnotation.emit(prevPageAnnotations[prevPageAnnotations.length - 1]); + this.shouldDeselectAnnotationsOnPageChangeChange.emit(false); + this.selectAnnotations.emit([prevPageAnnotations[prevPageAnnotations.length - 1]]); } } } diff --git a/apps/red-ui/src/app/modules/projects/components/page-indicator/page-indicator.component.html b/apps/red-ui/src/app/modules/projects/components/page-indicator/page-indicator.component.html index d0955c95d..d912f51bd 100644 --- a/apps/red-ui/src/app/modules/projects/components/page-indicator/page-indicator.component.html +++ b/apps/red-ui/src/app/modules/projects/components/page-indicator/page-indicator.component.html @@ -10,4 +10,5 @@
{{ number }}
+
diff --git a/apps/red-ui/src/app/modules/projects/components/page-indicator/page-indicator.component.scss b/apps/red-ui/src/app/modules/projects/components/page-indicator/page-indicator.component.scss index 3bbdd9b5f..dd386e81d 100644 --- a/apps/red-ui/src/app/modules/projects/components/page-indicator/page-indicator.component.scss +++ b/apps/red-ui/src/app/modules/projects/components/page-indicator/page-indicator.component.scss @@ -41,4 +41,14 @@ color: $grey-1; } } + + .dot { + background: $primary; + height: 8px; + width: 8px; + border-radius: 50%; + position: absolute; + top: 9px; + right: 10px; + } } diff --git a/apps/red-ui/src/app/modules/projects/components/page-indicator/page-indicator.component.ts b/apps/red-ui/src/app/modules/projects/components/page-indicator/page-indicator.component.ts index c2f0ce4a0..5e7bb8cf0 100644 --- a/apps/red-ui/src/app/modules/projects/components/page-indicator/page-indicator.component.ts +++ b/apps/red-ui/src/app/modules/projects/components/page-indicator/page-indicator.component.ts @@ -14,6 +14,7 @@ export class PageIndicatorComponent implements OnChanges, OnInit, OnDestroy { @Input() active: boolean; @Input() number: number; @Input() viewedPages: ViewedPages; + @Input() activeSelection = false; @Output() pageSelected = new EventEmitter(); diff --git a/apps/red-ui/src/app/modules/projects/components/pdf-viewer/pdf-viewer.component.ts b/apps/red-ui/src/app/modules/projects/components/pdf-viewer/pdf-viewer.component.ts index 701917417..f576348bf 100644 --- a/apps/red-ui/src/app/modules/projects/components/pdf-viewer/pdf-viewer.component.ts +++ b/apps/red-ui/src/app/modules/projects/components/pdf-viewer/pdf-viewer.component.ts @@ -28,6 +28,7 @@ export class PdfViewerComponent implements OnInit, AfterViewInit, OnChanges { @Input() canPerformActions = false; @Input() annotations: AnnotationWrapper[]; @Input() shouldDeselectAnnotationsOnPageChange = true; + @Input() multiSelectActive: boolean; @Output() fileReady = new EventEmitter(); @Output() annotationSelected = new EventEmitter(); @@ -83,13 +84,13 @@ export class PdfViewerComponent implements OnInit, AfterViewInit, OnChanges { this._configureTextPopup(); instance.annotManager.on('annotationSelected', (annotations, action) => { + this.annotationSelected.emit(instance.annotManager.getSelectedAnnotations().map((ann) => ann.Id)); if (action === 'deselected') { - this.annotationSelected.emit([]); this._toggleRectangleAnnotationAction(true); } else { this._configureAnnotationSpecificActions(annotations); this._toggleRectangleAnnotationAction(annotations.length === 1 && annotations[0].ReadOnly); - this.annotationSelected.emit(annotations.map((a) => a.Id)); + // this.annotationSelected.emit(annotations.map((a) => a.Id)); } }); @@ -103,7 +104,7 @@ export class PdfViewerComponent implements OnInit, AfterViewInit, OnChanges { instance.docViewer.on('pageNumberUpdated', (pageNumber) => { if (this.shouldDeselectAnnotationsOnPageChange) { - this.instance.annotManager.deselectAllAnnotations(); + this.deselectAllAnnotations(); } this._ngZone.run(() => this.pageChanged.emit(pageNumber)); }); @@ -253,7 +254,7 @@ export class PdfViewerComponent implements OnInit, AfterViewInit, OnChanges { quadsObject[activePage] = [quad]; const mre = this._getManualRedactionEntry(quadsObject, 'Rectangle'); // cleanup selection and button state - this.instance.annotManager.deselectAllAnnotations(); + this.deselectAllAnnotations(); this.instance.disableElements(['shapeToolGroupButton']); this.instance.enableElements(['shapeToolGroupButton']); // dispatch event @@ -381,12 +382,33 @@ export class PdfViewerComponent implements OnInit, AfterViewInit, OnChanges { }; } - public selectAnnotation(annotation: AnnotationWrapper) { + public deselectAllAnnotations() { this.instance.annotManager.deselectAllAnnotations(); - const annotationFromViewer = this.instance.annotManager.getAnnotationById(annotation.id); - this.instance.annotManager.selectAnnotation(annotationFromViewer); - this.navigateToPage(annotation.pageNumber); - this.instance.annotManager.jumpToAnnotation(annotationFromViewer); + } + + public selectAnnotations($event: AnnotationWrapper[] | { annotations: AnnotationWrapper[]; multiSelect: boolean }) { + let annotations: AnnotationWrapper[]; + let multiSelect: boolean; + if ($event instanceof Array) { + annotations = $event; + multiSelect = false; + } else { + annotations = $event.annotations; + multiSelect = $event.multiSelect; + } + + if (!this.multiSelectActive && !multiSelect) { + this.deselectAllAnnotations(); + } + + const annotationsFromViewer = annotations.map((ann) => this.instance.annotManager.getAnnotationById(ann.id)); + this.instance.annotManager.selectAnnotations(annotationsFromViewer); + this.navigateToPage(annotations[0].pageNumber); + this.instance.annotManager.jumpToAnnotation(annotationsFromViewer[0]); + } + + public deselectAnnotations(annotations: AnnotationWrapper[]) { + this.instance.annotManager.deselectAnnotations(annotations.map((ann) => this.instance.annotManager.getAnnotationById(ann.id))); } public navigateToPage(pageNumber: number) { diff --git a/apps/red-ui/src/app/modules/projects/projects.module.ts b/apps/red-ui/src/app/modules/projects/projects.module.ts index 46f6b10d9..81ebdb3a4 100644 --- a/apps/red-ui/src/app/modules/projects/projects.module.ts +++ b/apps/red-ui/src/app/modules/projects/projects.module.ts @@ -34,6 +34,7 @@ import { PdfViewerDataService } from './services/pdf-viewer-data.service'; import { ManualAnnotationService } from './services/manual-annotation.service'; import { AnnotationDrawService } from './services/annotation-draw.service'; import { AnnotationProcessingService } from './services/annotation-processing.service'; +import { AnnotationRemoveActionsComponent } from './components/annotation-remove-actions/annotation-remove-actions.component'; const screens = [ProjectListingScreenComponent, ProjectOverviewScreenComponent, FilePreviewScreenComponent]; @@ -62,6 +63,7 @@ const components = [ ProjectListingActionsComponent, DocumentInfoComponent, FileWorkloadComponent, + AnnotationRemoveActionsComponent, ...screens, ...dialogs diff --git a/apps/red-ui/src/app/modules/projects/screens/file-preview-screen/file-preview-screen.component.html b/apps/red-ui/src/app/modules/projects/screens/file-preview-screen/file-preview-screen.component.html index 48d5737a4..2c957ea18 100644 --- a/apps/red-ui/src/app/modules/projects/screens/file-preview-screen/file-preview-screen.component.html +++ b/apps/red-ui/src/app/modules/projects/screens/file-preview-screen/file-preview-screen.component.html @@ -205,6 +205,7 @@ [fileStatus]="appStateService.activeFile" [shouldDeselectAnnotationsOnPageChange]="shouldDeselectAnnotationsOnPageChange" [annotations]="annotations" + [multiSelectActive]="!!fileWorkloadComponent?.multiSelectActive" >
@@ -229,15 +230,17 @@ [annotations]="annotations" [selectedAnnotations]="selectedAnnotations" [activeViewerPage]="activeViewerPage" - [shouldDeselectAnnotationsOnPageChange]="shouldDeselectAnnotationsOnPageChange" + [(shouldDeselectAnnotationsOnPageChange)]="shouldDeselectAnnotationsOnPageChange" [dialogRef]="dialogRef" [annotationFilters]="annotationFilters" [fileData]="fileData" [hideSkipped]="hideSkipped" [annotationActionsTemplate]="annotationActionsTemplate" - (selectAnnotation)="selectAnnotation($event)" + (selectAnnotations)="selectAnnotations($event)" + (deselectAnnotations)="deselectAnnotations($event)" (selectPage)="selectPage($event)" (toggleSkipped)="toggleSkipped($event)" + (annotationsChanged)="annotationsChangedByReviewAction($event)" >
diff --git a/apps/red-ui/src/app/modules/projects/screens/file-preview-screen/file-preview-screen.component.scss b/apps/red-ui/src/app/modules/projects/screens/file-preview-screen/file-preview-screen.component.scss index cf0fe3019..99f4f8411 100644 --- a/apps/red-ui/src/app/modules/projects/screens/file-preview-screen/file-preview-screen.component.scss +++ b/apps/red-ui/src/app/modules/projects/screens/file-preview-screen/file-preview-screen.component.scss @@ -38,6 +38,15 @@ align-items: center; justify-content: space-between; padding: 0 24px; + + > div { + display: flex; + align-items: center; + + > *:not(:last-child) { + margin-right: 16px; + } + } } ::ng-deep .right-content { @@ -45,139 +54,8 @@ box-sizing: border-box; display: flex; } - // - // .quick-navigation, - // .annotations { - // overflow-y: scroll; - // outline: none; - // - // &.active-panel { - // background-color: #fafafa; - // } - // } - // - // .quick-navigation { - // border-right: 1px solid $separator; - // min-width: 61px; - // overflow: hidden; - // display: flex; - // flex-direction: column; - // - // .jump { - // min-height: 32px; - // display: flex; - // justify-content: center; - // align-items: center; - // cursor: pointer; - // transition: background-color 0.25s; - // - // &:not(.disabled):hover { - // background-color: $grey-6; - // } - // - // mat-icon { - // width: 16px; - // height: 16px; - // } - // - // &.disabled { - // cursor: default; - // - // mat-icon { - // opacity: 0.3; - // } - // } - // } - // - // .pages { - // @include no-scroll-bar(); - // overflow: auto; - // flex: 1; - // } - // } - // - // .page-separator { - // border-bottom: 1px solid $separator; - // height: 32px; - // box-sizing: border-box; - // padding: 0 10px; - // display: flex; - // align-items: center; - // background-color: $grey-6; - // } - // - // .annotations { - // overflow: hidden; - // width: 100%; - // height: calc(100% - 32px); - // - // .annotation-wrapper { - // display: flex; - // border-bottom: 1px solid $separator; - // - // .active-marker { - // min-width: 4px; - // min-height: 100%; - // } - // - // &.active { - // .active-marker { - // background-color: $primary; - // } - // } - // - // .annotation { - // padding: 10px 21px 10px 6px; - // font-size: 11px; - // line-height: 14px; - // cursor: pointer; - // display: flex; - // flex-direction: column; - // - // &.removed { - // text-decoration: line-through; - // color: $grey-7; - // } - // - // .details { - // display: flex; - // position: relative; - // } - // - // redaction-type-annotation-icon { - // margin-top: 6px; - // margin-right: 10px; - // } - // } - // - // &:hover { - // background-color: #f9fafb; - // - // ::ng-deep .annotation-actions { - // display: flex; - // } - // } - // } - // - // &:hover { - // overflow-y: auto; - // @include scroll-bar; - // } - // - // &.has-scrollbar:hover { - // .annotation { - // padding-right: 10px; - // } - // } - // } - //} } -//.no-annotations { -// padding: 24px; -// text-align: center; -//} - .assign-actions-wrapper { display: flex; margin-left: 8px; diff --git a/apps/red-ui/src/app/modules/projects/screens/file-preview-screen/file-preview-screen.component.ts b/apps/red-ui/src/app/modules/projects/screens/file-preview-screen/file-preview-screen.component.ts index c2ed3bdb7..ada33a26a 100644 --- a/apps/red-ui/src/app/modules/projects/screens/file-preview-screen/file-preview-screen.component.ts +++ b/apps/red-ui/src/app/modules/projects/screens/file-preview-screen/file-preview-screen.component.ts @@ -65,6 +65,7 @@ export class FilePreviewScreenComponent implements OnInit, OnDestroy { @ViewChild('fileWorkloadComponent') private _workloadComponent: FileWorkloadComponent; @ViewChild(PdfViewerComponent) private _viewerComponent: PdfViewerComponent; + @ViewChild(FileWorkloadComponent) public fileWorkloadComponent: FileWorkloadComponent; constructor( public readonly appStateService: AppStateService, @@ -111,11 +112,11 @@ export class FilePreviewScreenComponent implements OnInit, OnDestroy { } get canSwitchToRedactedView() { - return !this.permissionsService.fileRequiresReanalysis() && !this.fileData.fileStatus.isExcluded; + return this.fileData && !this.permissionsService.fileRequiresReanalysis() && !this.fileData.fileStatus.isExcluded; } get canSwitchToDeltaView() { - return this.fileData?.redactionChangeLog?.redactionLogEntry?.length > 0 && !this.fileData.fileStatus.isExcluded; + return this.fileData && this.fileData.redactionChangeLog?.redactionLogEntry?.length > 0 && !this.fileData.fileStatus.isExcluded; } get displayData() { @@ -233,13 +234,26 @@ export class FilePreviewScreenComponent implements OnInit, OnDestroy { } handleAnnotationSelected(annotationIds: string[]) { - this.selectedAnnotations = annotationIds.map((annotationId) => this.annotations.find((annotationWrapper) => annotationWrapper.id === annotationId)); + this.selectedAnnotations = annotationIds + .map((annotationId) => this.annotations.find((annotationWrapper) => annotationWrapper.id === annotationId)) + .filter((ann) => ann !== undefined); + if (this.selectedAnnotations.length > 1) { + this._workloadComponent.multiSelectActive = true; + } this._workloadComponent.scrollToSelectedAnnotation(); this._changeDetectorRef.detectChanges(); } - selectAnnotation(annotation: AnnotationWrapper) { - this._viewerComponent.selectAnnotation(annotation); + selectAnnotations(annotations?: AnnotationWrapper[] | { annotations: AnnotationWrapper[]; multiSelect: boolean }) { + if (!!annotations) { + this._viewerComponent.selectAnnotations(annotations); + } else { + this._viewerComponent.deselectAllAnnotations(); + } + } + + deselectAnnotations(annotations: AnnotationWrapper[]) { + this._viewerComponent.deselectAnnotations(annotations); } selectPage(pageNumber: number) { @@ -298,7 +312,9 @@ export class FilePreviewScreenComponent implements OnInit, OnDestroy { viewerPageChanged($event: any) { if (typeof $event === 'number') { this._scrollViews(); - this.shouldDeselectAnnotationsOnPageChange = true; + if (!this.fileWorkloadComponent.multiSelectActive) { + this.shouldDeselectAnnotationsOnPageChange = true; + } // Add current page in URL query params this._router.navigate([], { @@ -321,7 +337,7 @@ export class FilePreviewScreenComponent implements OnInit, OnDestroy { setTimeout(() => { this.selectPage(parseInt(pageNumber, 10)); this._scrollViews(); - }, 500); + }, 600); } } @@ -381,7 +397,7 @@ export class FilePreviewScreenComponent implements OnInit, OnDestroy { /* Close fullscreen */ closeFullScreen() { - if (document.exitFullscreen) { + if (!!document.fullscreenElement && document.exitFullscreen) { document.exitFullscreen(); } } diff --git a/apps/red-ui/src/app/modules/projects/services/annotation-actions.service.ts b/apps/red-ui/src/app/modules/projects/services/annotation-actions.service.ts index 58d95d3ec..954108a56 100644 --- a/apps/red-ui/src/app/modules/projects/services/annotation-actions.service.ts +++ b/apps/red-ui/src/app/modules/projects/services/annotation-actions.service.ts @@ -65,7 +65,10 @@ export class AnnotationActionsService { public markAsFalsePositive($event: MouseEvent, annotations: AnnotationWrapper[], annotationsChanged: EventEmitter) { annotations.forEach((annotation) => { - this._markAsFalsePositive($event, annotation, this._getFalsePositiveText(annotation), annotationsChanged); + const permissions = AnnotationPermissions.forUser(this._permissionsService.isManagerAndOwner(), this._permissionsService.currentUser, annotation); + const value = permissions.canMarkTextOnlyAsFalsePositive ? annotation.value : this._getFalsePositiveText(annotation); + + this._markAsFalsePositive($event, annotation, value, annotationsChanged); }); } @@ -85,12 +88,6 @@ export class AnnotationActionsService { }); } - public markTextOnlyAsFalsePositive($event: MouseEvent, annotations: AnnotationWrapper[], annotationsChanged: EventEmitter) { - annotations.forEach((annotation) => { - this._markAsFalsePositive($event, annotation, annotation.value, annotationsChanged); - }); - } - private _processObsAndEmit(obs: Observable, annotation: AnnotationWrapper, annotationsChanged: EventEmitter) { obs.subscribe( () => { @@ -216,21 +213,10 @@ export class AnnotationActionsService { }); } - const canMarkTextOnlyAsFalsePositive = annotationPermissions.reduce((acc, next) => acc && next.permissions.canMarkTextOnlyAsFalsePositive, true); - if (canMarkTextOnlyAsFalsePositive) { - availableActions.push({ - type: 'actionButton', - img: '/assets/icons/general/thumb-down.svg', - title: this._translateService.instant('annotation-actions.remove-annotation.false-positive'), - onClick: () => { - this._ngZone.run(() => { - this.markTextOnlyAsFalsePositive(null, annotations, annotationsChanged); - }); - } - }); - } - - const canMarkAsFalsePositive = annotationPermissions.reduce((acc, next) => acc && next.permissions.canMarkAsFalsePositive, true); + const canMarkAsFalsePositive = annotationPermissions.reduce( + (acc, next) => acc && (next.permissions.canMarkAsFalsePositive || next.permissions.canMarkTextOnlyAsFalsePositive), + true + ); if (canMarkAsFalsePositive) { availableActions.push({ type: 'actionButton', diff --git a/apps/red-ui/src/assets/i18n/en.json b/apps/red-ui/src/assets/i18n/en.json index fb7999a45..608cecfcd 100644 --- a/apps/red-ui/src/assets/i18n/en.json +++ b/apps/red-ui/src/assets/i18n/en.json @@ -422,6 +422,7 @@ "label": "Accept Recommendation" }, "suggest-remove-annotation": "Remove or Suggest to remove this entry", + "suggest-remove-annotations": "Remove or Suggest to remove selected entries", "reject-suggestion": "Reject Suggestion", "remove-annotation": { "suggest-remove-from-dict": "Suggest to remove from dictionary", diff --git a/apps/red-ui/src/assets/styles/red-button.scss b/apps/red-ui/src/assets/styles/red-button.scss index d63f68b46..bae06f57f 100644 --- a/apps/red-ui/src/assets/styles/red-button.scss +++ b/apps/red-ui/src/assets/styles/red-button.scss @@ -102,6 +102,10 @@ redaction-icon-button { &[aria-expanded='true'] { button { background: rgba($primary, 0.1); + + &.primary { + background: $red-2; + } } } } diff --git a/apps/red-ui/src/assets/styles/red-components.scss b/apps/red-ui/src/assets/styles/red-components.scss index dc71cde87..6e9a3f051 100644 --- a/apps/red-ui/src/assets/styles/red-components.scss +++ b/apps/red-ui/src/assets/styles/red-components.scss @@ -194,6 +194,10 @@ &.always-visible { opacity: 1; } + + &.primary-bg { + background-color: transparent; + } } .selection-icon {