Multi annotation select

This commit is contained in:
Adina Țeudan 2021-04-15 19:27:35 +03:00
parent e27578ebe6
commit c9016f0ccc
20 changed files with 490 additions and 349 deletions

View File

@ -20,7 +20,7 @@
</redaction-circle-button>
<redaction-circle-button
(action)="annotationActionsService.markTextOnlyAsFalsePositive($event, [annotation], annotationsChanged)"
(action)="annotationActionsService.markAsFalsePositive($event, [annotation], annotationsChanged)"
type="dark-bg"
*ngIf="annotationPermissions.canMarkTextOnlyAsFalsePositive && !annotationPermissions.canPerformMultipleRemoveActions"
tooltipPosition="before"
@ -79,61 +79,9 @@
>
</redaction-circle-button>
<redaction-circle-button
(action)="annotationActionsService.suggestRemoveAnnotation($event, [annotation], false, annotationsChanged)"
type="dark-bg"
icon="red:trash"
*ngIf="annotationPermissions.canRemoveOrSuggestToRemoveOnlyHere && !annotationPermissions.canPerformMultipleRemoveActions"
tooltipPosition="before"
tooltip="annotation-actions.suggest-remove-annotation"
>
</redaction-circle-button>
<redaction-circle-button
*ngIf="annotationPermissions.canPerformMultipleRemoveActions"
(action)="openMenu($event)"
[class.active]="menuOpen"
[matMenuTriggerFor]="menu"
tooltipPosition="before"
tooltip="annotation-actions.suggest-remove-annotation"
type="dark-bg"
icon="red:trash"
>
</redaction-circle-button>
<mat-menu #menu="matMenu" (closed)="onMenuClosed()" xPosition="before">
<div
(click)="annotationActionsService.suggestRemoveAnnotation($event, [annotation], true, annotationsChanged)"
mat-menu-item
*ngIf="annotationPermissions.canRemoveOrSuggestToRemoveFromDictionary"
>
<redaction-annotation-icon [type]="'rhombus'" [label]="'S'" [color]="dictionaryColor"></redaction-annotation-icon>
<div [translate]="'annotation-actions.remove-annotation.remove-from-dict'"></div>
</div>
<div
(click)="annotationActionsService.suggestRemoveAnnotation($event, [annotation], false, annotationsChanged)"
mat-menu-item
*ngIf="annotationPermissions.canRemoveOrSuggestToRemoveOnlyHere"
>
<redaction-annotation-icon [type]="'rhombus'" [label]="'S'" [color]="suggestionColor"></redaction-annotation-icon>
<div translate="annotation-actions.remove-annotation.only-here"></div>
</div>
<div
(click)="annotationActionsService.markAsFalsePositive($event, [annotation], annotationsChanged)"
mat-menu-item
*ngIf="annotationPermissions.canMarkAsFalsePositive"
>
<mat-icon svgIcon="red:thumb-down" class="false-positive-icon"></mat-icon>
<div translate="annotation-actions.remove-annotation.false-positive"></div>
</div>
<div
(click)="annotationActionsService.markTextOnlyAsFalsePositive($event, [annotation], annotationsChanged)"
mat-menu-item
*ngIf="annotationPermissions.canMarkTextOnlyAsFalsePositive"
>
<mat-icon svgIcon="red:thumb-down" class="false-positive-icon"></mat-icon>
<div translate="annotation-actions.remove-annotation.false-positive"></div>
</div>
</mat-menu>
<redaction-annotation-remove-actions
[annotations]="[annotation]"
[(menuOpen)]="menuOpen"
[annotationsChanged]="annotationsChanged"
></redaction-annotation-remove-actions>
</div>

View File

@ -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');
}
}

View File

@ -0,0 +1,37 @@
<redaction-circle-button
(action)="suggestRemoveAnnotations($event, false)"
[type]="btnType"
icon="red:trash"
*ngIf="permissions.canRemoveOrSuggestToRemoveOnlyHere && permissions.canNotPerformMultipleRemoveActions"
[tooltipPosition]="tooltipPosition"
tooltip="annotation-actions.suggest-remove-annotation"
>
</redaction-circle-button>
<redaction-circle-button
*ngIf="permissions.canPerformMultipleRemoveActions"
(action)="openMenu($event)"
[class.active]="menuOpen"
[matMenuTriggerFor]="menu"
[tooltipPosition]="tooltipPosition"
tooltip="annotation-actions.suggest-remove-annotation"
[type]="btnType"
icon="red:trash"
>
</redaction-circle-button>
<mat-menu #menu="matMenu" (closed)="onMenuClosed()" xPosition="before">
<div (click)="suggestRemoveAnnotations($event, true)" mat-menu-item *ngIf="permissions.canRemoveOrSuggestToRemoveFromDictionary">
<redaction-annotation-icon [type]="'rhombus'" [label]="'S'" [color]="dictionaryColor"></redaction-annotation-icon>
<div [translate]="'annotation-actions.remove-annotation.remove-from-dict'"></div>
</div>
<div (click)="suggestRemoveAnnotations($event, false)" mat-menu-item *ngIf="permissions.canRemoveOrSuggestToRemoveOnlyHere">
<redaction-annotation-icon [type]="'rhombus'" [label]="'S'" [color]="suggestionColor"></redaction-annotation-icon>
<div translate="annotation-actions.remove-annotation.only-here"></div>
</div>
<div (click)="markAsFalsePositive($event)" mat-menu-item *ngIf="permissions.canMarkAsFalsePositive">
<mat-icon svgIcon="red:thumb-down" class="false-positive-icon"></mat-icon>
<div translate="annotation-actions.remove-annotation.false-positive"></div>
</div>
</mat-menu>

View File

@ -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<boolean>();
@Input() annotationsChanged: EventEmitter<AnnotationWrapper>;
@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);
}
}

View File

@ -1,5 +1,6 @@
<div class="right-title heading" translate="file-preview.tabs.annotations.label">
<div>
<div *ngIf="!multiSelectActive" class="all-caps-label primary pointer" (click)="multiSelectActive = true">SELECT</div>
<redaction-filter
(filtersChanged)="filtersChanged($event)"
[chevron]="true"
@ -10,103 +11,138 @@
</div>
</div>
<div class="right-content">
<div
#quickNavigation
(keydown)="preventKeyDefault($event)"
(keyup)="preventKeyDefault($event)"
[class.active-panel]="pagesPanelActive"
class="quick-navigation"
tabindex="0"
>
<div
class="jump"
[class.disabled]="!quickScrollFirstEnabled"
[matTooltip]="'file-preview.quick-nav.jump-first' | translate"
matTooltipPosition="above"
(click)="quickScrollFirstEnabled && scrollQuickNavFirst()"
>
<mat-icon svgIcon="red:nav-first"></mat-icon>
</div>
<div class="pages" (scroll)="computeQuickNavButtonsState()" id="pages">
<redaction-page-indicator
(pageSelected)="pageSelectedByClick($event)"
*ngFor="let pageNumber of displayedPages"
[active]="pageNumber === activeViewerPage"
[number]="pageNumber"
[viewedPages]="fileData.viewedPages"
>
</redaction-page-indicator>
</div>
<div
class="jump"
[class.disabled]="!quickScrollLastEnabled"
[matTooltip]="'file-preview.quick-nav.jump-last' | translate"
matTooltipPosition="above"
(click)="scrollQuickNavLast()"
>
<mat-icon svgIcon="red:nav-last"></mat-icon>
<div *ngIf="multiSelectActive" class="multi-select">
<div class="selected-wrapper">
<div *ngIf="!selectedAnnotations?.length" class="select-oval always-visible primary-bg"></div>
<mat-icon
*ngIf="selectedAnnotations?.length"
(click)="selectAnnotations.emit()"
class="selection-icon"
svgIcon="red:radio-indeterminate"
></mat-icon>
<span class="all-caps-label">{{ selectedAnnotations?.length || 0 }} selected </span>
<redaction-annotation-remove-actions
*ngIf="selectedAnnotations?.length > 0"
[annotations]="selectedAnnotations"
[annotationsChanged]="annotationsChanged"
btnType="primary"
tooltipPosition="above"
></redaction-annotation-remove-actions>
</div>
<redaction-circle-button (action)="multiSelectActive = false" icon="red:close" type="primary"></redaction-circle-button>
</div>
<div style="overflow: hidden; width: 100%;">
<div attr.anotation-page-header="{{ activeViewerPage }}" class="page-separator">
<span *ngIf="!!activeViewerPage" class="all-caps-label"
><span translate="page"></span> {{ activeViewerPage }} - {{ displayedAnnotations[activeViewerPage]?.annotations?.length || 0 }}
<span [translate]="displayedAnnotations[activeViewerPage]?.annotations?.length === 1 ? 'annotation' : 'annotations'"></span
></span>
</div>
<div class="annotations-wrapper" [class.multi-select-active]="multiSelectActive">
<div
#annotationsElement
#quickNavigation
(keydown)="preventKeyDefault($event)"
(keyup)="preventKeyDefault($event)"
[class.active-panel]="!pagesPanelActive"
redactionHasScrollbar
class="annotations"
tabindex="1"
[class.active-panel]="pagesPanelActive"
class="quick-navigation"
tabindex="0"
>
<div *ngIf="!displayedAnnotations[activeViewerPage]" class="heading-l no-annotations">
{{ 'file-preview.no-annotations-for-page' | translate }}
<div
class="jump"
[class.disabled]="!quickScrollFirstEnabled"
[matTooltip]="'file-preview.quick-nav.jump-first' | translate"
matTooltipPosition="above"
(click)="quickScrollFirstEnabled && scrollQuickNavFirst()"
>
<mat-icon svgIcon="red:nav-first"></mat-icon>
</div>
<div class="pages" (scroll)="computeQuickNavButtonsState()" id="pages">
<redaction-page-indicator
(pageSelected)="pageSelectedByClick($event)"
*ngFor="let pageNumber of displayedPages"
[active]="pageNumber === activeViewerPage"
[number]="pageNumber"
[viewedPages]="fileData.viewedPages"
[activeSelection]="pageHasSelection(pageNumber)"
>
</redaction-page-indicator>
</div>
<div
class="jump"
[class.disabled]="!quickScrollLastEnabled"
[matTooltip]="'file-preview.quick-nav.jump-last' | translate"
matTooltipPosition="above"
(click)="scrollQuickNavLast()"
>
<mat-icon svgIcon="red:nav-last"></mat-icon>
</div>
</div>
<div style="overflow: hidden; width: 100%;">
<div attr.anotation-page-header="{{ activeViewerPage }}" class="page-separator">
<span *ngIf="!!activeViewerPage" class="all-caps-label"
><span translate="page"></span> {{ activeViewerPage }} - {{ displayedAnnotations[activeViewerPage]?.annotations?.length || 0 }}
<span [translate]="displayedAnnotations[activeViewerPage]?.annotations?.length === 1 ? 'annotation' : 'annotations'"></span
></span>
<div *ngIf="multiSelectActive">
<div class="all-caps-label primary pointer" (click)="selectAllOnActivePage()">All</div>
<div class="all-caps-label primary pointer" (click)="deselectAllOnActivePage()">None</div>
</div>
</div>
<div
(click)="annotationClicked(annotation)"
class="annotation-wrapper"
*ngFor="let annotation of displayedAnnotations[activeViewerPage]?.annotations"
attr.annotation-id="{{ annotation.id }}"
attr.annotation-page="{{ activeViewerPage }}"
[class.active]="annotationIsSelected(annotation)"
#annotationsElement
(keydown)="preventKeyDefault($event)"
(keyup)="preventKeyDefault($event)"
[class.active-panel]="!pagesPanelActive"
redactionHasScrollbar
class="annotations"
tabindex="1"
>
<div class="active-marker"></div>
<div class="annotation" [class.removed]="annotation.isChangeLogRemoved">
<redaction-hidden-action (action)="logAnnotation(annotation)" [requiredClicks]="2">
<div class="details">
<redaction-type-annotation-icon [annotation]="annotation"></redaction-type-annotation-icon>
<div class="flex-1">
<div>
<strong>{{ annotation.typeLabel | translate }}</strong>
<div *ngIf="!displayedAnnotations[activeViewerPage]" class="heading-l no-annotations">
{{ 'file-preview.no-annotations-for-page' | translate }}
</div>
<div
(click)="annotationClicked(annotation, $event)"
class="annotation-wrapper"
*ngFor="let annotation of displayedAnnotations[activeViewerPage]?.annotations"
attr.annotation-id="{{ annotation.id }}"
attr.annotation-page="{{ activeViewerPage }}"
[class.active]="annotationIsSelected(annotation)"
[class.multi-select-active]="multiSelectActive"
>
<div class="active-bar-marker"></div>
<div class="annotation" [class.removed]="annotation.isChangeLogRemoved">
<redaction-hidden-action (action)="logAnnotation(annotation)" [requiredClicks]="2">
<div class="details">
<redaction-type-annotation-icon [annotation]="annotation"></redaction-type-annotation-icon>
<div class="flex-1">
<div>
<strong>{{ annotation.typeLabel | translate }}</strong>
</div>
<div *ngIf="annotation.dictionary && annotation.dictionary !== 'manual'">
<strong
><span>{{ annotation.descriptor | translate }}</span
>: </strong
>{{ annotation.dictionary | humanize: false }}
</div>
<div *ngIf="annotation.content && !annotation.isHint">
<strong><span translate="content"></span>: </strong>{{ annotation.content }}
</div>
{{ annotation.id }}
</div>
<div *ngIf="annotation.dictionary && annotation.dictionary !== 'manual'">
<strong
><span>{{ annotation.descriptor | translate }}</span
>: </strong
>{{ annotation.dictionary | humanize: false }}
</div>
<div *ngIf="annotation.content && !annotation.isHint">
<strong><span translate="content"></span>: </strong>{{ annotation.content }}
<ng-container
*ngIf="!multiSelectActive || !annotationIsSelected(annotation)"
[ngTemplateOutlet]="annotationActionsTemplate"
[ngTemplateOutletContext]="{ annotation: annotation }"
>
</ng-container>
<div class="active-icon-marker-container">
<mat-icon
class="active-icon-marker"
*ngIf="multiSelectActive && annotationIsSelected(annotation)"
svgIcon="red:check"
></mat-icon>
</div>
</div>
<ng-container [ngTemplateOutlet]="annotationActionsTemplate" [ngTemplateOutletContext]="{ annotation: annotation }"> </ng-container>
<!-- <redaction-annotation-actions-->
<!-- (annotationsChanged)="annotationsChangedByReviewAction($event)"-->
<!-- [annotation]="annotation"-->
<!-- [canPerformAnnotationActions]="canPerformAnnotationActions"-->
<!-- [viewer]="activeViewer"-->
<!-- ></redaction-annotation-actions>-->
</div>
</redaction-hidden-action>
<redaction-comments [annotation]="annotation"></redaction-comments>
</redaction-hidden-action>
<redaction-comments [annotation]="annotation"></redaction-comments>
</div>
</div>
</div>
</div>

View File

@ -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;
}
}

View File

@ -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<boolean>();
@Input() dialogRef: MatDialogRef<any>;
@Input() annotationFilters: FilterModel[];
@Input() fileData: FileDataModel;
@Input() hideSkipped: boolean;
@Input() annotationActionsTemplate: TemplateRef<any>;
@Output() selectAnnotation = new EventEmitter<AnnotationWrapper>();
@Output() selectAnnotations = new EventEmitter<AnnotationWrapper[] | { annotations: AnnotationWrapper[]; multiSelect: boolean }>();
@Output() deselectAnnotations = new EventEmitter<AnnotationWrapper[]>();
@Output() selectPage = new EventEmitter<number>();
@Output() toggleSkipped = new EventEmitter<any>();
@Output() annotationsChanged = new EventEmitter<AnnotationWrapper>();
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]]);
}
}
}

View File

@ -10,4 +10,5 @@
<div class="text">
{{ number }}
</div>
<div class="dot" *ngIf="activeSelection"></div>
</div>

View File

@ -41,4 +41,14 @@
color: $grey-1;
}
}
.dot {
background: $primary;
height: 8px;
width: 8px;
border-radius: 50%;
position: absolute;
top: 9px;
right: 10px;
}
}

View File

@ -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<number>();

View File

@ -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<string[]>();
@ -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) {

View File

@ -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

View File

@ -205,6 +205,7 @@
[fileStatus]="appStateService.activeFile"
[shouldDeselectAnnotationsOnPageChange]="shouldDeselectAnnotationsOnPageChange"
[annotations]="annotations"
[multiSelectActive]="!!fileWorkloadComponent?.multiSelectActive"
></redaction-pdf-viewer>
</div>
@ -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)"
></redaction-file-workload>
</div>
</div>

View File

@ -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;

View File

@ -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();
}
}

View File

@ -65,7 +65,10 @@ export class AnnotationActionsService {
public markAsFalsePositive($event: MouseEvent, annotations: AnnotationWrapper[], annotationsChanged: EventEmitter<AnnotationWrapper>) {
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<AnnotationWrapper>) {
annotations.forEach((annotation) => {
this._markAsFalsePositive($event, annotation, annotation.value, annotationsChanged);
});
}
private _processObsAndEmit(obs: Observable<any>, annotation: AnnotationWrapper, annotationsChanged: EventEmitter<AnnotationWrapper>) {
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',

View File

@ -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",

View File

@ -102,6 +102,10 @@ redaction-icon-button {
&[aria-expanded='true'] {
button {
background: rgba($primary, 0.1);
&.primary {
background: $red-2;
}
}
}
}

View File

@ -194,6 +194,10 @@
&.always-visible {
opacity: 1;
}
&.primary-bg {
background-color: transparent;
}
}
.selection-icon {