1 2 3 4 1 2 3 4 1 2 3 4 1 2 3 4 1 2 3 4 1 2 3 4 1 2 3 4 1 2 3 4
diff --git a/apps/red-ui/src/app/components/document-info/document-info.component.scss b/apps/red-ui/src/app/components/document-info/document-info.component.scss
index 5f677f05a..ee9825e39 100644
--- a/apps/red-ui/src/app/components/document-info/document-info.component.scss
+++ b/apps/red-ui/src/app/components/document-info/document-info.component.scss
@@ -11,8 +11,9 @@
@include inset-shadow;
}
-.content {
- max-height: calc(100% - 71px);
+.right-content {
+ flex-direction: column;
+
@include scroll-bar;
overflow: hidden;
diff --git a/apps/red-ui/src/app/components/file-workload/file-workload.component.html b/apps/red-ui/src/app/components/file-workload/file-workload.component.html
new file mode 100644
index 000000000..54f1c8512
--- /dev/null
+++ b/apps/red-ui/src/app/components/file-workload/file-workload.component.html
@@ -0,0 +1,133 @@
+
+
+
+
+
+
+ {{ activeViewerPage }} - {{ displayedAnnotations[activeViewerPage]?.annotations?.length || 0 }}
+
+
+
+
+
+ {{ 'file-preview.no-annotations-for-page' | translate }}
+
+
+
+
+
+
+
+
+
+
+ {{ annotation.typeLabel | translate }}
+
+
+ {{ annotation.descriptor | translate }}: {{ annotation.dictionary | humanize: false }}
+
+
+ : {{ annotation.content }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ filter.key | humanize: false }}
+
+
+
+
+
+
+
+
+
diff --git a/apps/red-ui/src/app/components/file-workload/file-workload.component.scss b/apps/red-ui/src/app/components/file-workload/file-workload.component.scss
new file mode 100644
index 000000000..149a4fb93
--- /dev/null
+++ b/apps/red-ui/src/app/components/file-workload/file-workload.component.scss
@@ -0,0 +1,134 @@
+@import '../../../assets/styles/red-variables';
+@import '../../../assets/styles/red-mixins';
+
+.right-content {
+ .no-annotations {
+ padding: 24px;
+ text-align: center;
+ }
+
+ .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;
+ }
+ }
+ }
+}
diff --git a/apps/red-ui/src/app/components/file-workload/file-workload.component.ts b/apps/red-ui/src/app/components/file-workload/file-workload.component.ts
new file mode 100644
index 000000000..6d261bdfa
--- /dev/null
+++ b/apps/red-ui/src/app/components/file-workload/file-workload.component.ts
@@ -0,0 +1,296 @@
+import { ChangeDetectorRef, Component, ElementRef, EventEmitter, HostListener, Input, Output, TemplateRef, ViewChild } from '@angular/core';
+import { FilterModel } from '../filter/model/filter.model';
+import { AnnotationWrapper } from '../../screens/file/model/annotation.wrapper';
+import { AnnotationProcessingService } from '../../screens/file/service/annotation-processing.service';
+import { MatDialogRef, MatDialogState } from '@angular/material/dialog';
+import scrollIntoView from 'scroll-into-view-if-needed';
+import { debounce } from '../../utils/debounce';
+import { FileDataModel } from '../../screens/file/model/file-data.model';
+
+const COMMAND_KEY_ARRAY = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Escape'];
+const ALL_HOTKEY_ARRAY = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'];
+
+@Component({
+ selector: 'redaction-file-workload',
+ templateUrl: './file-workload.component.html',
+ styleUrls: ['./file-workload.component.scss']
+})
+export class FileWorkloadComponent {
+ public displayedAnnotations: { [key: number]: { annotations: AnnotationWrapper[] } } = {};
+ private _annotations: AnnotationWrapper[];
+
+ @Input()
+ set annotations(value: AnnotationWrapper[]) {
+ this._annotations = value;
+ // this.computeQuickNavButtonsState();
+ }
+
+ @Input() selectedAnnotations: AnnotationWrapper[];
+ @Input() activeViewerPage: number;
+ @Input() shouldDeselectAnnotationsOnPageChange: boolean;
+ @Input() dialogRef: MatDialogRef
;
+ @Input() annotationFilters: FilterModel[];
+ @Input() fileData: FileDataModel;
+ @Input() hideSkipped: boolean;
+ @Input() annotationActionsTemplate: TemplateRef;
+
+ @Output() selectAnnotation = new EventEmitter();
+ @Output() selectPage = new EventEmitter();
+ @Output() toggleSkipped = new EventEmitter();
+
+ public quickScrollFirstEnabled = false;
+ public quickScrollLastEnabled = false;
+ public displayedPages: number[] = [];
+ public pagesPanelActive = true;
+
+ @ViewChild('annotationsElement') private _annotationsElement: ElementRef;
+ @ViewChild('quickNavigation') private _quickNavigationElement: ElementRef;
+
+ constructor(private _changeDetectorRef: ChangeDetectorRef, private _annotationProcessingService: AnnotationProcessingService) {}
+
+ private get firstSelectedAnnotation() {
+ return this.selectedAnnotations?.length ? this.selectedAnnotations[0] : null;
+ }
+
+ private static _scrollToFirstElement(elements: HTMLElement[], mode: 'always' | 'if-needed' = 'if-needed') {
+ if (elements.length > 0) {
+ scrollIntoView(elements[0], {
+ behavior: 'smooth',
+ scrollMode: mode,
+ block: 'start',
+ inline: 'start'
+ });
+ }
+ }
+
+ public annotationIsSelected(annotation: AnnotationWrapper) {
+ return this.selectedAnnotations?.find((a) => a.id === annotation.id);
+ }
+
+ public logAnnotation(annotation: AnnotationWrapper) {
+ console.log(annotation);
+ }
+
+ @debounce(0)
+ public filtersChanged(filters: FilterModel[]) {
+ this.displayedAnnotations = this._annotationProcessingService.filterAndGroupAnnotations(this._annotations, filters);
+ this.displayedPages = Object.keys(this.displayedAnnotations).map((key) => Number(key));
+ this.computeQuickNavButtonsState();
+ this._changeDetectorRef.markForCheck();
+ }
+
+ public computeQuickNavButtonsState() {
+ setTimeout(() => {
+ const element: HTMLElement = this._quickNavigationElement.nativeElement.querySelector(`#pages`);
+ const { scrollTop, scrollHeight, clientHeight } = element;
+ this.quickScrollFirstEnabled = scrollTop !== 0;
+ this.quickScrollLastEnabled = scrollHeight !== scrollTop + clientHeight;
+ }, 0);
+ }
+
+ public annotationClicked(annotation: AnnotationWrapper) {
+ this.pagesPanelActive = false;
+ this.selectAnnotation.emit(annotation);
+ }
+
+ @HostListener('window:keyup', ['$event'])
+ handleKeyEvent($event: KeyboardEvent) {
+ if (!ALL_HOTKEY_ARRAY.includes($event.key) || this.dialogRef?.getState() === MatDialogState.OPEN) {
+ return;
+ }
+
+ if ($event.key === 'ArrowLeft') {
+ this.pagesPanelActive = true;
+ return;
+ }
+
+ 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) {
+ this._selectFirstAnnotationOnCurrentPageIfNecessary();
+ }
+ return;
+ }
+
+ if (!this.pagesPanelActive) {
+ this._navigateAnnotations($event);
+ } else {
+ this._navigatePages($event);
+ }
+
+ this._changeDetectorRef.detectChanges();
+ }
+
+ public scrollAnnotations() {
+ if (this.firstSelectedAnnotation?.pageNumber === this.activeViewerPage) {
+ return;
+ }
+ this.scrollAnnotationsToPage(this.activeViewerPage, 'always');
+ }
+
+ public scrollAnnotationsToPage(page: number, mode: 'always' | 'if-needed' = 'if-needed') {
+ const elements: any[] = this._annotationsElement.nativeElement.querySelectorAll(`div[anotation-page-header="${page}"]`);
+ FileWorkloadComponent._scrollToFirstElement(elements, mode);
+ }
+
+ @debounce()
+ public scrollToSelectedAnnotation() {
+ if (!this.selectedAnnotations || this.selectedAnnotations.length === 0) {
+ return;
+ }
+ const elements: any[] = this._annotationsElement.nativeElement.querySelectorAll(`div[annotation-id="${this.firstSelectedAnnotation.id}"].active`);
+ FileWorkloadComponent._scrollToFirstElement(elements);
+ }
+
+ public scrollQuickNavigation() {
+ let quickNavPageIndex = this.displayedPages.findIndex((p) => p >= this.activeViewerPage);
+ if (quickNavPageIndex === -1 || this.displayedPages[quickNavPageIndex] !== this.activeViewerPage) {
+ quickNavPageIndex = Math.max(0, quickNavPageIndex - 1);
+ }
+ this._scrollQuickNavigationToPage(this.displayedPages[quickNavPageIndex]);
+ }
+
+ public scrollQuickNavFirst() {
+ if (this.displayedPages.length > 0) {
+ this._scrollQuickNavigationToPage(this.displayedPages[0]);
+ }
+ }
+
+ public scrollQuickNavLast() {
+ if (this.displayedPages.length > 0) {
+ this._scrollQuickNavigationToPage(this.displayedPages[this.displayedPages.length - 1]);
+ }
+ }
+
+ public pageSelectedByClick($event: number) {
+ this.pagesPanelActive = true;
+ this.selectPage.emit($event);
+ }
+
+ public preventKeyDefault($event: KeyboardEvent) {
+ if (COMMAND_KEY_ARRAY.includes($event.key)) {
+ $event.preventDefault();
+ }
+ }
+
+ private _selectFirstAnnotationOnCurrentPageIfNecessary() {
+ if (
+ (!this.firstSelectedAnnotation || this.activeViewerPage !== this.firstSelectedAnnotation.pageNumber) &&
+ this.displayedPages.indexOf(this.activeViewerPage) >= 0
+ ) {
+ this.selectAnnotation.emit(this.displayedAnnotations[this.activeViewerPage].annotations[0]);
+ }
+ }
+
+ private _navigateAnnotations($event: KeyboardEvent) {
+ if (!this.firstSelectedAnnotation || this.activeViewerPage !== this.firstSelectedAnnotation.pageNumber) {
+ const pageIdx = this.displayedPages.indexOf(this.activeViewerPage);
+ if (pageIdx !== -1) {
+ // Displayed page has annotations
+ this.selectAnnotation.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]);
+ } else {
+ const prevPage = this._prevPageWithAnnotations();
+ this.shouldDeselectAnnotationsOnPageChange = false;
+ const prevPageAnnotations = this.displayedAnnotations[prevPage].annotations;
+ this.selectAnnotation.emit(prevPageAnnotations[prevPageAnnotations.length - 1]);
+ }
+ }
+ } else {
+ const page = this.firstSelectedAnnotation.pageNumber;
+ const pageIdx = this.displayedPages.indexOf(page);
+ const annotationsOnPage = this.displayedAnnotations[page].annotations;
+ const idx = annotationsOnPage.findIndex((a) => a.id === this.firstSelectedAnnotation.id);
+
+ if ($event.key === 'ArrowDown') {
+ if (idx + 1 !== annotationsOnPage.length) {
+ // If not last item in page
+ this.selectAnnotation.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]);
+ }
+ } else {
+ if (idx !== 0) {
+ // If not first item in page
+ this.selectAnnotation.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]);
+ }
+ }
+ }
+ }
+
+ private _navigatePages($event: KeyboardEvent) {
+ const pageIdx = this.displayedPages.indexOf(this.activeViewerPage);
+
+ if ($event.key === 'ArrowDown') {
+ if (pageIdx !== -1) {
+ // If active page has annotations
+ if (pageIdx !== this.displayedPages.length - 1) {
+ this.selectPage.emit(this.displayedPages[pageIdx + 1]);
+ }
+ } else {
+ // If active page doesn't have annotations
+ const nextPage = this._nextPageWithAnnotations();
+ if (nextPage) {
+ this.selectPage.emit(nextPage);
+ }
+ }
+ } else {
+ if (pageIdx !== -1) {
+ // If active page has annotations
+ if (pageIdx !== 0) {
+ this.selectPage.emit(this.displayedPages[pageIdx - 1]);
+ }
+ } else {
+ // If active page doesn't have annotations
+ const prevPage = this._prevPageWithAnnotations();
+ if (prevPage) {
+ this.selectPage.emit(prevPage);
+ }
+ }
+ }
+ }
+
+ private _nextPageWithAnnotations() {
+ let idx = 0;
+ for (const page of this.displayedPages) {
+ if (page > this.activeViewerPage) {
+ break;
+ }
+ ++idx;
+ }
+ return idx < this.displayedPages.length ? this.displayedPages[idx] : null;
+ }
+
+ private _prevPageWithAnnotations() {
+ let idx = this.displayedPages.length - 1;
+ for (const page of this.displayedPages.reverse()) {
+ if (page < this.activeViewerPage) {
+ this.selectPage.emit(this.displayedPages[idx]);
+ this.scrollAnnotations();
+ break;
+ }
+ --idx;
+ }
+ return idx >= 0 ? this.displayedPages[idx] : null;
+ }
+
+ private _scrollQuickNavigationToPage(page: number) {
+ const elements: any[] = this._quickNavigationElement.nativeElement.querySelectorAll(`#quick-nav-page-${page}`);
+ FileWorkloadComponent._scrollToFirstElement(elements);
+ }
+}
diff --git a/apps/red-ui/src/app/screens/file/file-preview-screen/file-preview-screen.component.html b/apps/red-ui/src/app/screens/file/file-preview-screen/file-preview-screen.component.html
index 243552329..2599fb9dd 100644
--- a/apps/red-ui/src/app/screens/file/file-preview-screen/file-preview-screen.component.html
+++ b/apps/red-ui/src/app/screens/file/file-preview-screen/file-preview-screen.component.html
@@ -207,118 +207,21 @@
-
-
-
-
-
-
- {{ activeViewerPage }} - {{ displayedAnnotations[activeViewerPage]?.annotations?.length || 0 }}
-
-
-
-
-
- {{ 'file-preview.no-annotations-for-page' | translate }}
-
-
-
-
-
-
-
-
-
-
- {{ annotation.typeLabel | translate }}
-
-
- {{ annotation.descriptor | translate }}: {{ annotation.dictionary | humanize: false }}
-
-
- : {{ annotation.content }}
-
-
-
-
-
-
-
-
-
-
-
+