File preview right panel & annotation icons
This commit is contained in:
parent
ba58977844
commit
e9c60015ae
@ -53,6 +53,7 @@ import { SimpleDoughnutChartComponent } from './components/simple-doughnut-chart
|
||||
import { ManualRedactionDialogComponent } from './screens/file/manual-redaction-dialog/manual-redaction-dialog.component';
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
|
||||
import { AnnotationIconComponent } from './components/annotation-icon/annotation-icon.component';
|
||||
|
||||
export function HttpLoaderFactory(httpClient: HttpClient) {
|
||||
return new TranslateHttpLoader(httpClient, '/assets/i18n/', '.json');
|
||||
@ -76,6 +77,7 @@ export function HttpLoaderFactory(httpClient: HttpClient) {
|
||||
LogoComponent,
|
||||
SimpleDoughnutChartComponent,
|
||||
ManualRedactionDialogComponent,
|
||||
AnnotationIconComponent,
|
||||
],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
|
||||
@ -0,0 +1,3 @@
|
||||
<div [ngClass]="type" class="icon">
|
||||
<span>{{ type[0] }}</span>
|
||||
</div>
|
||||
@ -0,0 +1,52 @@
|
||||
@import "../../../assets/styles/red-variables";
|
||||
|
||||
.icon {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
font-size: 11px;
|
||||
line-height: 14px;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
text-transform: uppercase;
|
||||
color: $white;
|
||||
}
|
||||
|
||||
.suggestion {
|
||||
width: 0;
|
||||
height: 0;
|
||||
border: 9px solid transparent;
|
||||
border-bottom-color: $grey-1;
|
||||
position: relative;
|
||||
top: -9px;
|
||||
|
||||
&:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: -9px;
|
||||
top: 9px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border: 9px solid transparent;
|
||||
border-top-color: $grey-1;
|
||||
}
|
||||
|
||||
span {
|
||||
transform: translateY(9px);
|
||||
z-index: 2;
|
||||
}
|
||||
}
|
||||
|
||||
.hint, .comment, .ignore {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.hint, .redaction, .comment {
|
||||
background-color: $grey-1;
|
||||
}
|
||||
|
||||
.ignore {
|
||||
background-color: $grey-5;
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
import { Component, Input, OnInit } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'redaction-annotation-icon',
|
||||
templateUrl: './annotation-icon.component.html',
|
||||
styleUrls: ['./annotation-icon.component.scss']
|
||||
})
|
||||
export class AnnotationIconComponent implements OnInit {
|
||||
@Input() public type: 'hint' | 'redaction' | 'suggestion' | 'ignore' | 'comment';
|
||||
|
||||
constructor() {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
}
|
||||
}
|
||||
@ -42,134 +42,76 @@
|
||||
</div>
|
||||
|
||||
<div class="right-fixed-container">
|
||||
|
||||
<!-- Quick navigation tab-->
|
||||
<div class="vertical" (click)="selectTab('NAVIGATION')" [ngClass]="{ active: navigationTab}"
|
||||
#navigationTabElement>
|
||||
<div class="tab-title" [ngClass]="navigationTab ? 'heading' : 'all-caps-label'"
|
||||
translate="file-preview.tabs.quick-navigation.label">
|
||||
<div>
|
||||
<button color="accent" mat-button
|
||||
[matMenuTriggerFor]="filterMenu" [ngClass]="{ overlay: hasActiveFilters }">
|
||||
<span translate="file-preview.filter-menu.label"></span>
|
||||
<mat-icon class="dropdown-icon">arrow_drop_down</mat-icon>
|
||||
</button>
|
||||
<div class="dot" *ngIf="hasActiveFilters"></div>
|
||||
<mat-menu class="filter-menu" #filterMenu="matMenu" xPosition="before">
|
||||
<div class="filter-menu-header">
|
||||
<div class="all-caps-label" translate="file-preview.filter-menu.filter-types.label"></div>
|
||||
<div class="actions">
|
||||
<div class="all-caps-label primary pointer" translate="file-preview.filter-menu.all.label"
|
||||
(click)="setAllFilters(true); $event.stopPropagation();"></div>
|
||||
<div class="all-caps-label primary pointer" translate="file-preview.filter-menu.none.label"
|
||||
(click)="setAllFilters(false); $event.stopPropagation();"></div>
|
||||
</div>
|
||||
<div class="right-title heading"
|
||||
translate="file-preview.tabs.annotations.label">
|
||||
<div>
|
||||
<button color="accent" mat-button
|
||||
[matMenuTriggerFor]="filterMenu" [ngClass]="{ overlay: hasActiveFilters }">
|
||||
<span translate="file-preview.filter-menu.label"></span>
|
||||
<mat-icon class="dropdown-icon">arrow_drop_down</mat-icon>
|
||||
</button>
|
||||
<div class="dot" *ngIf="hasActiveFilters"></div>
|
||||
<mat-menu class="filter-menu" #filterMenu="matMenu" xPosition="before">
|
||||
<div class="filter-menu-header">
|
||||
<div class="all-caps-label" translate="file-preview.filter-menu.filter-types.label"></div>
|
||||
<div class="actions">
|
||||
<div class="all-caps-label primary pointer" translate="file-preview.filter-menu.all.label"
|
||||
(click)="setAllFilters(true); $event.stopPropagation();"></div>
|
||||
<div class="all-caps-label primary pointer" translate="file-preview.filter-menu.none.label"
|
||||
(click)="setAllFilters(false); $event.stopPropagation();"></div>
|
||||
</div>
|
||||
<div class="mat-menu-item" *ngFor="let key of filterKeys" (click)="$event.stopPropagation()">
|
||||
<mat-checkbox [(ngModel)]="filters[key].value" color="primary">
|
||||
<div [class]="filters[key].class + ' x-small'">
|
||||
{{ filters[key].symbol }}
|
||||
</div>
|
||||
{{filters[key].label | translate }}
|
||||
</mat-checkbox>
|
||||
</div>
|
||||
</mat-menu>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-content" [class.not-visible]="!navigationTab">
|
||||
<div *ngFor="let item of quickNavigation | sortBy:'asc':'number'"
|
||||
class="page-navigation"
|
||||
[id]="'quick-nav-page-'+item.pageNumber"
|
||||
[ngClass]="{ active: item.pageNumber === activeViewerPage, hidden: !showQuickNavigationItem(item) }"
|
||||
(click)="selectPage(item.pageNumber)"
|
||||
>
|
||||
<div class="page-number small-label">{{ item.pageNumber }}</div>
|
||||
<div *ngFor="let key of filterKeys"
|
||||
[ngClass]="{ hidden: !showAnnotations(item, key)}" class="page-stats small-label">
|
||||
<div [class]="filters[key].class + ' x-small'">{{ filters[key].symbol }}</div>
|
||||
{{item[key]}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="mat-menu-item" *ngFor="let key of filterKeys" (click)="$event.stopPropagation()">
|
||||
<mat-checkbox [(ngModel)]="filters[key]" (change)="applyFilters()" color="primary">
|
||||
<redaction-annotation-icon [type]="key"></redaction-annotation-icon>
|
||||
{{"file-preview.filter-menu."+ key + ".label" | translate }}
|
||||
</mat-checkbox>
|
||||
</div>
|
||||
</mat-menu>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Annotations tab-->
|
||||
<div (click)="selectTab('ANNOTATIONS')" class="vertical" [ngClass]="{ active: annotationsTab }"
|
||||
#annotationsContainer>
|
||||
<div class="tab-title" [ngClass]="annotationsTab ? 'heading' : 'all-caps-label'"
|
||||
translate="file-preview.tabs.annotations.label">
|
||||
<mat-icon class="close-icon"
|
||||
*ngIf="annotationsTab"
|
||||
(click)="selectTab('NAVIGATION', $event)"
|
||||
svgIcon="red:close"></mat-icon>
|
||||
<div class="right-content">
|
||||
<div class="pages" #quickNavigation>
|
||||
<div class="page-number pointer"
|
||||
[ngClass]="{ active: pageNumber === activeViewerPage }"
|
||||
*ngFor="let pageNumber of displayedPages"
|
||||
[id]="'quick-nav-page-'+pageNumber" (click)="selectPage(pageNumber)">
|
||||
{{pageNumber}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-content" [class.not-visible]="!annotationsTab">
|
||||
<div *ngFor="let annotation of annotations"
|
||||
class="annotation" attr.annotation-id="{{annotation.Id}}"
|
||||
attr.annotation-page="{{annotation.getPageNumber()}}"
|
||||
(click)="selectAnnotation(annotation)"
|
||||
[ngClass]="{ active: selectedAnnotation === annotation }">
|
||||
<div><strong>Type: </strong>{{getType(annotation.Id)}}</div>
|
||||
<div><strong>Dictionary: </strong>{{getDictionary(annotation.Id)}}</div>
|
||||
<div><strong>Page: </strong> {{annotation.getPageNumber()}}</div>
|
||||
<div *ngIf="annotation.getContents()"><strong>Content: </strong>{{annotation.getContents()}}</div>
|
||||
|
||||
<div class="annotation-actions">
|
||||
<button mat-icon-button (click)="suggestRemoveAnnotation($event, annotation)">
|
||||
<mat-icon svgIcon="red:trash"></mat-icon>
|
||||
</button>
|
||||
<div class="annotations" #annotations>
|
||||
<div *ngFor="let page of displayedPages">
|
||||
<div class="page-separator">
|
||||
<span class="all-caps-label"><span translate="page"></span> {{page}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Info tab-->
|
||||
<div class="vertical" (click)="selectTab('INFO')" [ngClass]="{ active: infoTab}">
|
||||
<div class="tab-title" [ngClass]="infoTab ? 'heading' : 'all-caps-label'"
|
||||
translate="file-preview.tabs.info.label">
|
||||
<mat-icon class="close-icon"
|
||||
*ngIf="infoTab"
|
||||
(click)="selectTab('NAVIGATION', $event)"
|
||||
svgIcon="red:close"></mat-icon>
|
||||
</div>
|
||||
<div *ngFor="let annotation of displayedAnnotations[page].annotations"
|
||||
class="annotation" attr.annotation-id="{{annotation.Id}}"
|
||||
attr.annotation-page="{{page}}"
|
||||
[ngClass]="{ active: selectedAnnotation === annotation }"
|
||||
(click)="selectAnnotation(annotation)"
|
||||
>
|
||||
|
||||
<div *ngIf="infoTab" class="tab-content info-container">
|
||||
<redaction-status-bar [small]="true"
|
||||
labelClass="small-label"
|
||||
[config]="[{ length: 1, label: 'Unassigned', color: 'unassigned'}]"></redaction-status-bar>
|
||||
<redaction-annotation-icon [type]="getType(annotation)"></redaction-annotation-icon>
|
||||
<div class="flex-1">
|
||||
<div><strong>{{getType(annotation) | translate}}</strong></div>
|
||||
<div><strong><span translate="dictionary"></span>: </strong>{{getDictionary(annotation)}}</div>
|
||||
<div *ngIf="annotation.getContents()"><strong><span translate="content"></span>:
|
||||
</strong>{{annotation.getContents()}}</div>
|
||||
</div>
|
||||
|
||||
<div class="small-label stats-subtitle mt-5">
|
||||
<div>
|
||||
<mat-icon svgIcon="red:pages"></mat-icon>
|
||||
{{appStateService.activeFile.numberOfPages}}</div>
|
||||
<div>
|
||||
<mat-icon svgIcon="red:analyse"></mat-icon>
|
||||
{{annotations.length}}</div>
|
||||
</div>
|
||||
<div class="page-number">
|
||||
{{annotation.getPageNumber()}}
|
||||
</div>
|
||||
|
||||
<div class="flex-row mt-20">
|
||||
<redaction-initials-avatar size="large" color="red-white"></redaction-initials-avatar>
|
||||
<a class="assign-reviewer" translate="file-preview.tabs.info.assign-reviewer.label"></a>
|
||||
</div>
|
||||
|
||||
<div class="all-caps-label mt-20" translate="file-preview.tabs.info.added-on.label">
|
||||
</div>
|
||||
<div class="small-label mt-5">
|
||||
{{appStateService.activeFile.added | date:'medium'}}
|
||||
</div>
|
||||
|
||||
<div class="all-caps-label mt-20" translate="file-preview.tabs.info.added-by.label">
|
||||
</div>
|
||||
<div class="small-label mt-5">
|
||||
{{user.name}}
|
||||
</div>
|
||||
|
||||
<div class="all-caps-label mt-20" translate="file-preview.tabs.info.last-modified-on.label">
|
||||
</div>
|
||||
<div class="small-label mt-5">
|
||||
{{appStateService.activeFile.lastUpdated | date:'medium'}}
|
||||
<div class="annotation-actions">
|
||||
<button mat-icon-button>
|
||||
<mat-icon svgIcon="red:trash"></mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -13,142 +13,129 @@ redaction-pdf-viewer {
|
||||
|
||||
.right-fixed-container {
|
||||
padding: 0;
|
||||
width: calc(#{$right-container-width} - 1px);
|
||||
display: flex;
|
||||
width: $right-container-width;
|
||||
box-sizing: border-box;
|
||||
|
||||
.vertical {
|
||||
height: 100%;
|
||||
border-right: 1px solid $separator;
|
||||
.right-title {
|
||||
height: 70px;
|
||||
display: flex;
|
||||
border-bottom: 1px solid $separator;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 24px;
|
||||
|
||||
&.active {
|
||||
width: calc(#{$right-container-width} - 80px);
|
||||
padding-top: 0;
|
||||
> div {
|
||||
position: relative;
|
||||
|
||||
.tab-title {
|
||||
height: 70px;
|
||||
display: flex;
|
||||
.dot {
|
||||
background: $primary;
|
||||
height: 10px;
|
||||
width: 10px;
|
||||
border-radius: 50%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.close-icon {
|
||||
height: 14px;
|
||||
width: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.right-content {
|
||||
height: calc(100vh - 110px - 72px);
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
|
||||
.pages, .annotations {
|
||||
overflow-y: scroll;
|
||||
|
||||
scrollbar-width: none; /* Firefox */
|
||||
-ms-overflow-style: none; /* IE 10+ */
|
||||
&::-webkit-scrollbar {
|
||||
width: 0;
|
||||
background: transparent; /* Chrome/Safari/Webkit */
|
||||
}
|
||||
}
|
||||
|
||||
.pages {
|
||||
border-right: 1px solid $separator;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 16px;
|
||||
min-width: 28px;
|
||||
}
|
||||
|
||||
.annotations {
|
||||
width: 100%;
|
||||
|
||||
.page-separator {
|
||||
border-bottom: 1px solid $separator;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 25px;
|
||||
height: 40px;
|
||||
box-sizing: border-box;
|
||||
padding: 8px 10px;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
background-color: $grey-6;
|
||||
}
|
||||
|
||||
> div {
|
||||
position: relative;
|
||||
.annotation {
|
||||
border-bottom: 1px solid $separator;
|
||||
padding: 10px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
|
||||
.dot {
|
||||
background: $primary;
|
||||
height: 10px;
|
||||
width: 10px;
|
||||
border-radius: 50%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
redaction-annotation-icon {
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: #F9FAFB;
|
||||
|
||||
.annotation-actions {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.close-icon {
|
||||
height: 14px;
|
||||
width: 14px;
|
||||
cursor: pointer;
|
||||
&.active {
|
||||
border-left: 2px solid $primary;
|
||||
}
|
||||
|
||||
.annotation-actions {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
display: none;
|
||||
width: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
height: calc(100vh - 110px - 73px);
|
||||
box-sizing: border-box;
|
||||
|
||||
scrollbar-width: none; /* Firefox */
|
||||
-ms-overflow-style: none; /* IE 10+ */
|
||||
&::-webkit-scrollbar {
|
||||
width: 0;
|
||||
background: transparent; /* Chrome/Safari/Webkit */
|
||||
}
|
||||
}
|
||||
|
||||
.info-container {
|
||||
padding: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.active) {
|
||||
width: 40px;
|
||||
padding-top: 15px;
|
||||
cursor: pointer;
|
||||
|
||||
.tab-title {
|
||||
transform: translateX(27px) rotate(90deg);
|
||||
transform-origin: 0 0;
|
||||
position: absolute;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.assign-reviewer {
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
.annotation {
|
||||
border-bottom: 1px solid $separator;
|
||||
padding: 14px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
background-color: #F9FAFB;
|
||||
|
||||
.annotation-actions {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
border-left: 2px solid $primary;
|
||||
}
|
||||
|
||||
.annotation-actions {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
display: none;
|
||||
width: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
.page-navigation {
|
||||
.page-number {
|
||||
border: 1px solid $separator;
|
||||
padding: 5px 10px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid $separator;
|
||||
padding: 14px;
|
||||
border-left: 4px solid transparent;
|
||||
|
||||
&:hover {
|
||||
background-color: $grey-2;
|
||||
}
|
||||
|
||||
.page-number {
|
||||
border: 1px solid $separator;
|
||||
padding: 7px;
|
||||
margin-right: 3px;
|
||||
width: 14px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.page-stats {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
opacity: 1;
|
||||
}
|
||||
height: 28px;
|
||||
min-height: 28px;
|
||||
min-width: 14px;
|
||||
opacity: 0.7;
|
||||
font-size: 11px;
|
||||
line-height: 14px;
|
||||
|
||||
&.active {
|
||||
border-left: 4px solid $primary;
|
||||
border: 1px solid $primary;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import {ChangeDetectorRef, Component, ElementRef, OnInit, ViewChild} from '@angular/core';
|
||||
import {ActivatedRoute, Router} from '@angular/router';
|
||||
import { ChangeDetectorRef, Component, ElementRef, OnInit, ViewChild } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import {
|
||||
AddRedactionRequest,
|
||||
FileUploadControllerService,
|
||||
@ -8,34 +8,25 @@ import {
|
||||
ProjectControllerService,
|
||||
StatusControllerService
|
||||
} from '@redaction/red-ui-http';
|
||||
import {TranslateService} from '@ngx-translate/core';
|
||||
import {NotificationService, NotificationType} from '../../../notification/notification.service';
|
||||
import {MatDialog} from '@angular/material/dialog';
|
||||
import {AppStateService} from '../../../state/app-state.service';
|
||||
import {FileDetailsDialogComponent} from './file-details-dialog/file-details-dialog.component';
|
||||
import {ViewerSyncService} from '../service/viewer-sync.service';
|
||||
import {Annotations} from '@pdftron/webviewer';
|
||||
import {PdfViewerComponent} from '../pdf-viewer/pdf-viewer.component';
|
||||
import {AnnotationUtils} from '../../../utils/annotation-utils';
|
||||
import {ManualRedactionDialogComponent} from '../manual-redaction-dialog/manual-redaction-dialog.component';
|
||||
import {UserService} from '../../../user/user.service';
|
||||
import {debounce} from '../../../utils/debounce';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { NotificationService, NotificationType } from '../../../notification/notification.service';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { AppStateService } from '../../../state/app-state.service';
|
||||
import { FileDetailsDialogComponent } from './file-details-dialog/file-details-dialog.component';
|
||||
import { ViewerSyncService } from '../service/viewer-sync.service';
|
||||
import { Annotations } from '@pdftron/webviewer';
|
||||
import { PdfViewerComponent } from '../pdf-viewer/pdf-viewer.component';
|
||||
import { AnnotationUtils } from '../../../utils/annotation-utils';
|
||||
import { ManualRedactionDialogComponent } from '../manual-redaction-dialog/manual-redaction-dialog.component';
|
||||
import { UserService } from '../../../user/user.service';
|
||||
import { debounce } from '../../../utils/debounce';
|
||||
import scrollIntoView from 'scroll-into-view-if-needed';
|
||||
import {ConfirmationDialogComponent} from '../../../common/confirmation-dialog/confirmation-dialog.component';
|
||||
import {AnnotationFilters} from '../../../utils/types';
|
||||
import {FiltersService} from '../service/filters.service';
|
||||
import {FileDownloadService} from "../service/file-download.service";
|
||||
import {saveAs} from 'file-saver';
|
||||
import {FileType} from "../model/file-type";
|
||||
|
||||
class QuickNavigationItem {
|
||||
pageNumber: number;
|
||||
hints: number;
|
||||
redactions: number;
|
||||
comments: number;
|
||||
suggestions: number;
|
||||
ignored: number;
|
||||
}
|
||||
import { AnnotationFilters } from '../../../utils/types';
|
||||
import { FiltersService } from '../service/filters.service';
|
||||
import { FileDownloadService } from '../service/file-download.service';
|
||||
import { saveAs } from 'file-saver';
|
||||
import { FileType } from '../model/file-type';
|
||||
import { ConfirmationDialogComponent } from '../../../common/confirmation-dialog/confirmation-dialog.component';
|
||||
|
||||
@Component({
|
||||
selector: 'redaction-file-preview-screen',
|
||||
@ -45,28 +36,19 @@ class QuickNavigationItem {
|
||||
export class FilePreviewScreenComponent implements OnInit {
|
||||
private _readyViewers: string[] = [];
|
||||
private projectId: string;
|
||||
private _selectedTab: 'NAVIGATION' | 'ANNOTATIONS' | 'INFO' = 'NAVIGATION';
|
||||
private _activeViewer: 'ANNOTATED' | 'REDACTED' = 'ANNOTATED';
|
||||
private _manualRedactionEntry: ManualRedactionEntry;
|
||||
|
||||
@ViewChild(PdfViewerComponent)
|
||||
private _viewerComponent: PdfViewerComponent;
|
||||
|
||||
@ViewChild('annotationsContainer')
|
||||
private _annotationsContainer: ElementRef;
|
||||
|
||||
@ViewChild('navigationTabElement')
|
||||
private _navigationTabElement: ElementRef;
|
||||
@ViewChild(PdfViewerComponent) private _viewerComponent: PdfViewerComponent;
|
||||
@ViewChild('annotations') private _annotationsElement: ElementRef;
|
||||
@ViewChild('quickNavigation') private _quickNavigationElement: ElementRef;
|
||||
|
||||
public fileId: string;
|
||||
public annotations: Annotations.Annotation[] = [];
|
||||
public displayedAnnotations: { [key: number]: { annotations: Annotations.Annotation[] } } = {};
|
||||
public selectedAnnotation: Annotations.Annotation;
|
||||
public quickNavigation: QuickNavigationItem[] = [];
|
||||
|
||||
public filters: AnnotationFilters;
|
||||
|
||||
public get filterKeys() {
|
||||
return Object.keys(this.filters);
|
||||
}
|
||||
public activeViewerPage: number;
|
||||
|
||||
private _manualRedactionEntry: AddRedactionRequest;
|
||||
|
||||
@ -96,10 +78,14 @@ export class FilePreviewScreenComponent implements OnInit {
|
||||
this.filters = _filtersService.filters;
|
||||
}
|
||||
|
||||
get user() {
|
||||
public get user() {
|
||||
return this._userService.user;
|
||||
}
|
||||
|
||||
public get filterKeys() {
|
||||
return Object.keys(this.filters);
|
||||
}
|
||||
|
||||
public get redactedView() {
|
||||
return this._activeViewer === 'REDACTED';
|
||||
}
|
||||
@ -141,56 +127,22 @@ export class FilePreviewScreenComponent implements OnInit {
|
||||
this._viewerSyncService.activateViewer(value);
|
||||
}
|
||||
|
||||
public selectTab(value: 'ANNOTATIONS' | 'INFO' | 'NAVIGATION', $event?: MouseEvent) {
|
||||
if ($event) {
|
||||
$event.stopPropagation();
|
||||
}
|
||||
if (value !== this._selectedTab) {
|
||||
this._selectedTab = value;
|
||||
setTimeout(() => {
|
||||
this._scrollViews();
|
||||
}, 50);
|
||||
}
|
||||
public applyFilters() {
|
||||
this.displayedAnnotations = AnnotationUtils.parseAnnotations(this.annotations, this.filters);
|
||||
}
|
||||
|
||||
public handleAnnotationsAdded(annotations: Annotations.Annotation[]) {
|
||||
this._changeDetectorRef.detectChanges();
|
||||
for (const annotation of annotations) {
|
||||
if (annotation.Id.indexOf(':') > 0) {
|
||||
this.annotations.push(annotation);
|
||||
const pageNumber = annotation.getPageNumber();
|
||||
let el = this.quickNavigation.find((page) => page.pageNumber === pageNumber);
|
||||
if (!el) {
|
||||
el = {pageNumber, redactions: 0, hints: 0, ignored: 0, comments: 0, suggestions: 0};
|
||||
this.quickNavigation.push(el);
|
||||
}
|
||||
if (annotation.Id.startsWith('hint:')) {
|
||||
el.hints++;
|
||||
}
|
||||
if (annotation.Id.startsWith('ignore:')) {
|
||||
el.ignored++;
|
||||
}
|
||||
if (annotation.Id.startsWith('redaction:')) {
|
||||
el.redactions++;
|
||||
}
|
||||
}
|
||||
}
|
||||
this.annotations = AnnotationUtils.sortAnnotations(this.annotations);
|
||||
AnnotationUtils.addAnnotations(this.annotations, annotations);
|
||||
this.applyFilters();
|
||||
}
|
||||
|
||||
public showQuickNavigationItem(item: QuickNavigationItem): boolean {
|
||||
let showItem = false;
|
||||
Object.keys(this.filters).map((key) => {
|
||||
if (this.showAnnotations(item, key)) {
|
||||
showItem = true;
|
||||
}
|
||||
})
|
||||
return showItem;
|
||||
public get displayedPages(): number[] {
|
||||
return Object.keys(this.displayedAnnotations).map(key => Number(key));
|
||||
}
|
||||
|
||||
public handleAnnotationSelected(annotation: Annotations.Annotation) {
|
||||
this.selectedAnnotation = annotation;
|
||||
this.selectTab('ANNOTATIONS');
|
||||
this.scrollToSelectedAnnotation();
|
||||
this._changeDetectorRef.detectChanges();
|
||||
}
|
||||
@ -204,22 +156,10 @@ export class FilePreviewScreenComponent implements OnInit {
|
||||
if (!this.selectedAnnotation) {
|
||||
return;
|
||||
}
|
||||
const elements: any[] = this._annotationsContainer.nativeElement.querySelectorAll(`div[annotation-id="${this.selectedAnnotation.Id}"].active`);
|
||||
const elements: any[] = this._annotationsElement.nativeElement.querySelectorAll(`div[annotation-id="${this.selectedAnnotation.Id}"].active`);
|
||||
this._scrollToFirstElement(elements);
|
||||
}
|
||||
|
||||
public get navigationTab() {
|
||||
return this._selectedTab === 'NAVIGATION';
|
||||
}
|
||||
|
||||
public get annotationsTab() {
|
||||
return this._selectedTab === 'ANNOTATIONS';
|
||||
}
|
||||
|
||||
public get infoTab() {
|
||||
return this._selectedTab === 'INFO';
|
||||
}
|
||||
|
||||
public selectPage(pageNumber: number) {
|
||||
this._viewerComponent.navigateToPage(pageNumber);
|
||||
}
|
||||
@ -241,7 +181,7 @@ export class FilePreviewScreenComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
viewerPageChanged(pageNumber: number) {
|
||||
public viewerPageChanged(pageNumber: number) {
|
||||
if (Number.isInteger(pageNumber)) {
|
||||
this.activeViewerPage = this._viewerSyncService.activeViewerPage;
|
||||
this._scrollViews();
|
||||
@ -251,13 +191,12 @@ export class FilePreviewScreenComponent implements OnInit {
|
||||
|
||||
@debounce()
|
||||
private _scrollViews() {
|
||||
console.log('scroll views');
|
||||
this._scrollQuickNavigation();
|
||||
this._scrollAnnotations();
|
||||
}
|
||||
|
||||
private _scrollQuickNavigation() {
|
||||
const elements: any[] = this._navigationTabElement.nativeElement.querySelectorAll(`#quick-nav-page-${this.activeViewerPage}`);
|
||||
const elements: any[] = this._quickNavigationElement.nativeElement.querySelectorAll(`#quick-nav-page-${this.activeViewerPage}`);
|
||||
this._scrollToFirstElement(elements);
|
||||
}
|
||||
|
||||
@ -265,7 +204,7 @@ export class FilePreviewScreenComponent implements OnInit {
|
||||
if (this.selectedAnnotation?.getPageNumber() === this.activeViewerPage) {
|
||||
return;
|
||||
}
|
||||
const elements: any[] = this._annotationsContainer.nativeElement.querySelectorAll(`div[annotation-page="${this.activeViewerPage}"]`);
|
||||
const elements: any[] = this._annotationsElement.nativeElement.querySelectorAll(`div[annotation-page="${this.activeViewerPage}"]`);
|
||||
this._scrollToFirstElement(elements);
|
||||
}
|
||||
|
||||
@ -280,14 +219,12 @@ export class FilePreviewScreenComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
getType(id: string) {
|
||||
const parts = id.split(':');
|
||||
return parts.length >= 1 ? parts[0] : 'n/a';
|
||||
getType(annotation: Annotations.Annotation): string {
|
||||
return AnnotationUtils.getType(annotation);
|
||||
}
|
||||
|
||||
getDictionary(id: string) {
|
||||
const parts = id.split(':');
|
||||
return parts.length >= 2 ? parts[1] : 'n/a';
|
||||
getDictionary(annotation: Annotations.Annotation): string {
|
||||
return AnnotationUtils.getDictionary(annotation);
|
||||
}
|
||||
|
||||
// async getText(pageNumber: number, rect) {
|
||||
@ -332,26 +269,16 @@ export class FilePreviewScreenComponent implements OnInit {
|
||||
public downloadFile(type: FileType | string) {
|
||||
this._fileDownloadService.loadFile(type, this.fileId).subscribe(data => {
|
||||
saveAs(data, this.appStateService.activeFile.filename);
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
public setAllFilters(value: boolean) {
|
||||
Object.keys(this.filters).map((key) => {
|
||||
this.filters[key].value = value;
|
||||
this.filters[key] = value;
|
||||
});
|
||||
}
|
||||
|
||||
public get hasActiveFilters(): boolean {
|
||||
let activeFilters = false;
|
||||
Object.keys(this.filters).map((key) => {
|
||||
if (this.filters[key].value) {
|
||||
activeFilters = true;
|
||||
}
|
||||
});
|
||||
return activeFilters;
|
||||
}
|
||||
|
||||
public showAnnotations(item: QuickNavigationItem, type: string): boolean {
|
||||
return item[type] && (!this.hasActiveFilters || (this.hasActiveFilters && this.filters[type]?.value));
|
||||
return AnnotationUtils.hasActiveFilters(this.filters);
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,11 +9,11 @@ export class FiltersService {
|
||||
}
|
||||
|
||||
private _filters: AnnotationFilters = {
|
||||
hints: { label: 'file-preview.filter-menu.hint.label', value: false, class: 'oval darkgray-white', symbol: 'H' },
|
||||
redactions: { label: 'file-preview.filter-menu.redaction.label', value: false, class: 'square darkgray-white', symbol: 'R' },
|
||||
comments: { label: 'file-preview.filter-menu.comment.label', value: false, class: 'oval darkgray-white', symbol: 'C' },
|
||||
suggestions: { label: 'file-preview.filter-menu.suggestion.label', value: false, class: 'oval red-white', symbol: 'S' },
|
||||
ignored: { label: 'file-preview.filter-menu.ignored.label', value: false, class: 'oval lightgray-white', symbol: 'I' },
|
||||
hint: false,
|
||||
redaction: false,
|
||||
comment: false,
|
||||
suggestion: false,
|
||||
ignore: false,
|
||||
}
|
||||
|
||||
public get filters(): AnnotationFilters {
|
||||
|
||||
@ -70,8 +70,8 @@
|
||||
</div>
|
||||
|
||||
<div class="flex-2 min-width needs-work">
|
||||
<div class="oval darkgray-white x-small">R</div>
|
||||
<div class="oval red-white x-small">S</div>
|
||||
<redaction-annotation-icon type="redaction"></redaction-annotation-icon>
|
||||
<redaction-annotation-icon type="hint"></redaction-annotation-icon>
|
||||
</div>
|
||||
|
||||
<div class="small-label flex-2 assigned-to min-width">
|
||||
|
||||
@ -131,7 +131,6 @@ export class ProjectOverviewScreenComponent implements OnInit, OnDestroy {
|
||||
|
||||
private _calculateChartConfig() {
|
||||
const obj = this.appStateService.activeProject.files.reduce((acc, file) => {
|
||||
console.log(file.status);
|
||||
acc[file.status === 'PROCESSED' ? 'finished' : file.status === 'ERROR' ? 'under-approval' : 'under-review']++;
|
||||
return acc;
|
||||
}, { 'finished': 0, 'under-approval': 0, 'under-review': 0 });
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { Annotations } from '@pdftron/webviewer';
|
||||
import { AnnotationFilters } from './types';
|
||||
|
||||
export class AnnotationUtils {
|
||||
public static sortAnnotations(annotations: Annotations.Annotation[]): Annotations.Annotation[] {
|
||||
@ -15,4 +16,60 @@ export class AnnotationUtils {
|
||||
return ann1.getPageNumber() < ann2.getPageNumber() ? -1 : 1;
|
||||
});
|
||||
}
|
||||
|
||||
public static hasActiveFilters(filters: AnnotationFilters): boolean {
|
||||
return Object.keys(filters).filter(type => filters[type]).length > 0;
|
||||
}
|
||||
|
||||
public static parseAnnotations(annotations: Annotations.Annotation[], filters: AnnotationFilters):
|
||||
{ [key: number]: { annotations: Annotations.Annotation[] } } {
|
||||
const obj = {};
|
||||
|
||||
for (const ann of annotations) {
|
||||
const pageNumber = ann.getPageNumber();
|
||||
const type = this.getType(ann);
|
||||
|
||||
if (this.hasActiveFilters(filters) && !filters[type]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!obj[pageNumber]) {
|
||||
obj[pageNumber] = {
|
||||
annotations: [],
|
||||
hint: 0,
|
||||
redaction: 0,
|
||||
comment: 0,
|
||||
suggestion: 0,
|
||||
ignore: 0
|
||||
};
|
||||
}
|
||||
obj[pageNumber].annotations.push(ann);
|
||||
obj[pageNumber][type]++;
|
||||
}
|
||||
|
||||
Object.keys(obj).map(page => {
|
||||
obj[page].annotations = this.sortAnnotations(obj[page].annotations);
|
||||
});
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
|
||||
public static addAnnotations(initialAnnotations: Annotations.Annotation[], addedAnnotations: Annotations.Annotation[]) {
|
||||
for (const annotation of addedAnnotations) {
|
||||
if (annotation.Id.indexOf(':') > 0) {
|
||||
initialAnnotations.push(annotation);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static getType(annotation: Annotations.Annotation): string {
|
||||
const parts = annotation.Id.split(':');
|
||||
return parts.length >= 1 ? parts[0] : 'n/a';
|
||||
}
|
||||
|
||||
public static getDictionary(annotation: Annotations.Annotation): string {
|
||||
const parts = annotation.Id.split(':');
|
||||
return parts.length >= 2 ? parts[1] : 'n/a';
|
||||
}
|
||||
}
|
||||
|
||||
14
apps/red-ui/src/app/utils/types.d.ts
vendored
14
apps/red-ui/src/app/utils/types.d.ts
vendored
@ -9,6 +9,13 @@ export type Color =
|
||||
'active' |
|
||||
'archived';
|
||||
|
||||
export type AnnotationType =
|
||||
'hint' |
|
||||
'redaction' |
|
||||
'suggestion' |
|
||||
'comment' |
|
||||
'ignore'
|
||||
|
||||
export class SortingOption {
|
||||
label: string;
|
||||
order: string;
|
||||
@ -16,10 +23,5 @@ export class SortingOption {
|
||||
}
|
||||
|
||||
export class AnnotationFilters {
|
||||
[key: string]: {
|
||||
label: string,
|
||||
value: boolean,
|
||||
class: string,
|
||||
symbol: string
|
||||
}
|
||||
[key: AnnotationType]: boolean
|
||||
}
|
||||
|
||||
@ -368,12 +368,12 @@
|
||||
"label": "Redaction"
|
||||
},
|
||||
"comment": {
|
||||
"label": "Comment"
|
||||
"label": "Comment annotation"
|
||||
},
|
||||
"suggestion": {
|
||||
"label": "Suggested redaction"
|
||||
},
|
||||
"ignored": {
|
||||
"ignore": {
|
||||
"label": "Ignored redaction"
|
||||
}
|
||||
},
|
||||
@ -428,5 +428,13 @@
|
||||
"approved": "Approved",
|
||||
"submitted": "Submitted",
|
||||
"active": "Active",
|
||||
"archived": "Archived"
|
||||
"archived": "Archived",
|
||||
"hint": "Hint",
|
||||
"ignore": "Ignore",
|
||||
"redaction": "Redaction",
|
||||
"comment": "Comment",
|
||||
"suggestion": "Suggestion for redaction",
|
||||
"dictionary": "Dictionary",
|
||||
"content": "Content",
|
||||
"page": "Page"
|
||||
}
|
||||
|
||||
@ -19,13 +19,6 @@
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
&.x-small {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
font-size: 11px;
|
||||
line-height: 14px;
|
||||
}
|
||||
|
||||
&.lightgray-dark {
|
||||
background-color: $grey-4;
|
||||
}
|
||||
|
||||
@ -1,16 +1,12 @@
|
||||
$white: #FFF;
|
||||
$black: #000;
|
||||
|
||||
$primary: #DD4D50;
|
||||
$accent: #283241;
|
||||
$light: #FFF;
|
||||
$dark: #000;
|
||||
|
||||
$grey-1: #283241;
|
||||
$grey-2: #F4F5F7;
|
||||
$grey-3: #AAACB3;
|
||||
$grey-4: #E2E4E9;
|
||||
$grey-5: #D3D5DA;
|
||||
$grey-6: #F0F1F4;
|
||||
|
||||
$blue-1: #4875F7;
|
||||
$blue-2: #48C9F7;
|
||||
@ -21,6 +17,11 @@ $yellow-1: #FFB83B;
|
||||
$green-1: #46CE7D;
|
||||
$green-2: #5CE594;
|
||||
|
||||
$primary: $red-1;
|
||||
$accent: $grey-1;
|
||||
$light: $white;
|
||||
$dark: $black;
|
||||
|
||||
$separator: rgba(226,228,233,0.9);
|
||||
|
||||
$right-container-inside-width: 340px;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user