Pull request #13: Ui updates
Merge in RED/ui from ui-updates to master * commit '43510a448f08cf48a6e2c8a235069ca415b2e626': post rebase code fixes Expandable filters Subfilters Cleanup some styles File preview right panel & annotation icons Project overview chart
This commit is contained in:
commit
d18ab65a66
@ -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,64 @@
|
||||
@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;
|
||||
}
|
||||
|
||||
.hint_only {
|
||||
background-color: $orange-1;
|
||||
}
|
||||
|
||||
.vertebrate {
|
||||
background-color: $green-1;
|
||||
}
|
||||
|
||||
.names {
|
||||
background-color: $yellow-2;
|
||||
}
|
||||
@ -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 {
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
<div class="container">
|
||||
<div [class]="'container flex ' + direction">
|
||||
<svg attr.height="{{size}}" attr.width="{{size}}" attr.viewBox="0 0 {{size}} {{size}}" class="donut-chart">
|
||||
<g *ngFor="let value of parsedConfig; let i = index">
|
||||
<circle attr.cx="{{cx}}"
|
||||
@ -13,16 +13,16 @@
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
<div class="text-container" [style]="'height: ' + size + 'px; width: ' + size + 'px;'">
|
||||
<div class="text-container" [style]="'height: ' + size + 'px; width: ' + size + 'px; padding: ' + strokeWidth + 'px;'">
|
||||
<div class="heading-xl">{{ dataTotal }}</div>
|
||||
<div class="projects-text mt-5">{{ subtitle }}</div>
|
||||
<div class="projects-text mt-5">{{ subtitle | translate }}</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-20 breakdown-container">
|
||||
<div class="breakdown-container">
|
||||
<div>
|
||||
<div *ngFor="let val of parsedConfig">
|
||||
<redaction-status-bar [small]="true"
|
||||
[config]="[{ length: val.value, color: val.color, label: val.value + ' ' + val.label}]">
|
||||
[config]="[{ length: val.value, color: val.color, label: val.value + ' ' + (val.label | translate | lowercase) }]">
|
||||
</redaction-status-bar>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -3,8 +3,12 @@
|
||||
.container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
|
||||
&.column {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.text-container {
|
||||
@ -14,6 +18,7 @@
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.breakdown-container {
|
||||
|
||||
@ -19,6 +19,7 @@ export class SimpleDoughnutChartComponent implements OnInit {
|
||||
@Input() angleOffset = -90;
|
||||
@Input() radius = 85;
|
||||
@Input() strokeWidth = 20;
|
||||
@Input() direction: 'row' | 'column' = 'column';
|
||||
|
||||
public chartData: any[] = [];
|
||||
public perimeter: number;
|
||||
|
||||
@ -15,7 +15,7 @@ export class IconsModule {
|
||||
) {
|
||||
const icons = [
|
||||
'add', 'analyse', 'arrow-down', 'arrow-up', 'assign', 'calendar', 'check',
|
||||
'close', 'document', 'double-chevron-right', 'download', 'drop-down-arrow',
|
||||
'close', 'document', 'double-chevron-right', 'download',
|
||||
'edit', 'error', 'folder', 'info', 'lightning', 'logout', 'menu', 'pages',
|
||||
'plus', 'preview', 'refresh', 'report', 'secret', 'sort-asc', 'sort-desc',
|
||||
'status', 'trash', 'user'
|
||||
|
||||
@ -40,11 +40,8 @@
|
||||
</div>
|
||||
<div class="menu right">
|
||||
<button [matMenuTriggerFor]="menu" mat-button>
|
||||
<div class="account-button-wrapper">
|
||||
<redaction-initials-avatar color="red-white" size="small" [username]="user?.name"></redaction-initials-avatar>
|
||||
<span>{{user?.name}}</span>
|
||||
<mat-icon svgIcon="red:drop-down-arrow"></mat-icon>
|
||||
</div>
|
||||
<redaction-initials-avatar color="red-white" size="small" [username]="user?.name" [withName]="true"></redaction-initials-avatar>
|
||||
<mat-icon>arrow_drop_down</mat-icon>
|
||||
</button>
|
||||
<mat-menu #menu="matMenu">
|
||||
|
||||
|
||||
@ -1,17 +0,0 @@
|
||||
@import "../../../assets/styles/red-variables";
|
||||
|
||||
|
||||
.account-button-wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
redaction-initials-avatar{
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
mat-icon {
|
||||
width: 10px;
|
||||
margin-left: 6px;
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
<section class="" [class.hidden]="!viewReady">
|
||||
<section [class.hidden]="!viewReady">
|
||||
<div class="page-header">
|
||||
<mat-slide-toggle color="primary"
|
||||
[(ngModel)]="redactedView"
|
||||
@ -7,7 +7,7 @@
|
||||
<button color="primary" mat-flat-button
|
||||
[matMenuTriggerFor]="downloadMenu">
|
||||
<span translate="file-preview.download.label"></span>
|
||||
<mat-icon class="dropdown-icon">arrow_drop_down</mat-icon>
|
||||
<mat-icon>arrow_drop_down</mat-icon>
|
||||
</button>
|
||||
<mat-menu #downloadMenu="matMenu" xPosition="before">
|
||||
<div mat-menu-item
|
||||
@ -42,134 +42,98 @@
|
||||
</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>arrow_drop_down</mat-icon>
|
||||
</button>
|
||||
<div class="dot" *ngIf="hasActiveFilters"></div>
|
||||
<mat-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(filters, true); applyFilters(); $event.stopPropagation();"></div>
|
||||
<div class="all-caps-label primary pointer" translate="file-preview.filter-menu.none.label"
|
||||
(click)="setAllFilters(filters, false); applyFilters(); $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 }}
|
||||
</div>
|
||||
<div *ngFor="let key of filterKeys()">
|
||||
<div class="mat-menu-item flex" (click)="$event.stopPropagation()">
|
||||
<div class="arrow-wrapper" *ngIf="hasSubsections(filters[key])">
|
||||
<mat-icon *ngIf="expandedFilters[key]" (click)="setExpanded(key, false, $event)">arrow_drop_down</mat-icon>
|
||||
<mat-icon *ngIf="!expandedFilters[key]" (click)="setExpanded(key, true, $event)">arrow_right</mat-icon>
|
||||
</div>
|
||||
<mat-checkbox [checked]="isChecked(key)"
|
||||
[indeterminate]="isIndeterminate(key)"
|
||||
(change)="setAllFilters(filters[key], $event.checked, hasSubsections(filters[key]) ? null : key)"
|
||||
color="primary">
|
||||
<redaction-annotation-icon [type]="key"></redaction-annotation-icon>
|
||||
{{"file-preview.filter-menu." + 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 *ngIf="hasSubsections(filters[key]) && expandedFilters[key]">
|
||||
<div *ngFor="let subkey of filterKeys(key)"
|
||||
class="padding-left mat-menu-item"
|
||||
(click)="$event.stopPropagation()"
|
||||
>
|
||||
<mat-checkbox [(ngModel)]="filters[key][subkey]" (change)="applyFilters()" color="primary">
|
||||
<redaction-annotation-icon [type]="key + ' ' + subkey"></redaction-annotation-icon>
|
||||
{{"file-preview.filter-menu." + key + "." + subkey + ".label" | translate }}
|
||||
</mat-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</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) + ' ' + getDictionary(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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -156,8 +143,8 @@ redaction-pdf-viewer {
|
||||
.filter-menu-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 250px;
|
||||
padding: 7px 15px 15px;
|
||||
width: 350px;
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
|
||||
@ -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,32 +36,20 @@ 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);
|
||||
}
|
||||
|
||||
private _manualRedactionEntry: AddRedactionRequest;
|
||||
|
||||
activeViewerPage: number;
|
||||
public expandedFilters: AnnotationFilters = { hint: false };
|
||||
public activeViewerPage: number;
|
||||
|
||||
constructor(
|
||||
public readonly appStateService: AppStateService,
|
||||
@ -96,10 +75,18 @@ export class FilePreviewScreenComponent implements OnInit {
|
||||
this.filters = _filtersService.filters;
|
||||
}
|
||||
|
||||
get user() {
|
||||
public get user() {
|
||||
return this._userService.user;
|
||||
}
|
||||
|
||||
public filterKeys(key?: string) {
|
||||
if (key) {
|
||||
return Object.keys(this.filters[key]);
|
||||
}
|
||||
|
||||
return Object.keys(this.filters);
|
||||
}
|
||||
|
||||
public get redactedView() {
|
||||
return this._activeViewer === 'REDACTED';
|
||||
}
|
||||
@ -141,56 +128,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 +157,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 +182,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 +192,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 +205,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 +220,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 +270,44 @@ 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;
|
||||
});
|
||||
public setAllFilters(filter: AnnotationFilters, value: boolean, rootKey?: string) {
|
||||
if (rootKey) {
|
||||
this.filters[rootKey] = value;
|
||||
} else {
|
||||
|
||||
for (const key of Object.keys(filter)) {
|
||||
if (AnnotationUtils.hasSubsections(filter[key])) {
|
||||
this.setAllFilters(filter[key], value);
|
||||
} else {
|
||||
filter[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.applyFilters();
|
||||
}
|
||||
|
||||
public isChecked(key: string): boolean {
|
||||
return AnnotationUtils.isChecked(this.filters[key]);
|
||||
}
|
||||
|
||||
public isIndeterminate(key: string): boolean {
|
||||
return AnnotationUtils.isIndeterminate(this.filters[key]);
|
||||
}
|
||||
|
||||
public get hasActiveFilters(): boolean {
|
||||
let activeFilters = false;
|
||||
Object.keys(this.filters).map((key) => {
|
||||
if (this.filters[key].value) {
|
||||
activeFilters = true;
|
||||
}
|
||||
});
|
||||
return activeFilters;
|
||||
return AnnotationUtils.hasActiveFilters(this.filters);
|
||||
}
|
||||
|
||||
public showAnnotations(item: QuickNavigationItem, type: string): boolean {
|
||||
return item[type] && (!this.hasActiveFilters || (this.hasActiveFilters && this.filters[type]?.value));
|
||||
public hasSubsections(filter: AnnotationFilters | boolean) {
|
||||
return AnnotationUtils.hasSubsections(filter);
|
||||
}
|
||||
|
||||
public setExpanded(key: string, value: boolean, $event: MouseEvent) {
|
||||
$event.stopPropagation();
|
||||
this.expandedFilters[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,11 +9,15 @@ 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: {
|
||||
hint_only: false,
|
||||
vertebrate: false,
|
||||
names: false,
|
||||
},
|
||||
redaction: false,
|
||||
comment: false,
|
||||
suggestion: false,
|
||||
ignore: false,
|
||||
}
|
||||
|
||||
public get filters(): AnnotationFilters {
|
||||
|
||||
@ -1,24 +1,24 @@
|
||||
<div class="page-header">
|
||||
<div class="filters flex-row">
|
||||
<span translate="filters.filter-by.label"></span>
|
||||
<div translate="filters.status.label" class="icon-button">
|
||||
<div translate="filters.filter-by.label"></div>
|
||||
<button mat-button translate="filters.status.label">
|
||||
<mat-icon svgIcon="red:status"></mat-icon>
|
||||
</div>
|
||||
<div translate="filters.people.label" class="icon-button">
|
||||
</button>
|
||||
<button mat-button translate="filters.people.label">
|
||||
<mat-icon svgIcon="red:user"></mat-icon>
|
||||
</div>
|
||||
<div translate="filters.due-date.label" class="icon-button">
|
||||
</button>
|
||||
<button mat-button translate="filters.due-date.label">
|
||||
<mat-icon svgIcon="red:lightning"></mat-icon>
|
||||
</div>
|
||||
<div translate="filters.created-on.label" class="icon-button">
|
||||
</button>
|
||||
<button mat-button translate="filters.created-on.label">
|
||||
<mat-icon svgIcon="red:calendar"></mat-icon>
|
||||
</div>
|
||||
<div translate="filters.project.label" class="icon-button">
|
||||
</button>
|
||||
<button mat-button translate="filters.project.label">
|
||||
<mat-icon svgIcon="red:folder"></mat-icon>
|
||||
</div>
|
||||
<div translate="filters.document.label" class="icon-button">
|
||||
</button>
|
||||
<button mat-button translate="filters.document.label">
|
||||
<mat-icon svgIcon="red:document"></mat-icon>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<button (click)="openAddProjectDialog()" color="primary" mat-flat-button class="add-project-btn">
|
||||
<mat-icon svgIcon="red:plus">
|
||||
@ -105,7 +105,7 @@
|
||||
<div>
|
||||
<redaction-simple-doughnut-chart [config]="projectsChartData"
|
||||
[strokeWidth]="15"
|
||||
[subtitle]="'project-listing.stats.charts.projects.label' | translate"
|
||||
[subtitle]="'project-listing.stats.charts.projects.label'"
|
||||
></redaction-simple-doughnut-chart>
|
||||
|
||||
<div class="project-stats-container">
|
||||
@ -129,7 +129,7 @@
|
||||
<div>
|
||||
<redaction-simple-doughnut-chart [config]="documentsChartData"
|
||||
[strokeWidth]="15"
|
||||
[subtitle]="'project-listing.stats.charts.total-documents.label' | translate"
|
||||
[subtitle]="'project-listing.stats.charts.total-documents.label'"
|
||||
></redaction-simple-doughnut-chart>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -41,8 +41,8 @@ export class ProjectListingScreenComponent implements OnInit {
|
||||
ngOnInit(): void {
|
||||
this.appStateService.reset();
|
||||
this.projectsChartData = [
|
||||
{ value: this.activeProjects, color: 'active', label: 'active-projects' },
|
||||
{ value: this.inactiveProjects, color: 'archived', label: 'Archived' }
|
||||
{ value: this.activeProjects, color: 'active', label: 'active' },
|
||||
{ value: this.inactiveProjects, color: 'archived', label: 'archived' }
|
||||
];
|
||||
this.documentsChartData = [
|
||||
{ value: this.appStateService.totalDocuments, color: 'unassigned', label: 'unassigned' },
|
||||
|
||||
@ -4,25 +4,25 @@
|
||||
|
||||
<div *ngIf="appStateService.activeProject" class="page-header">
|
||||
<div class="filters flex-row">
|
||||
<span translate="filters.filter-by.label"></span>
|
||||
<div translate="filters.status.label" class="icon-button">
|
||||
<div translate="filters.filter-by.label"></div>
|
||||
<button mat-button translate="filters.status.label">
|
||||
<mat-icon svgIcon="red:status"></mat-icon>
|
||||
</div>
|
||||
<div translate="filters.people.label" class="icon-button">
|
||||
</button>
|
||||
<button mat-button translate="filters.people.label">
|
||||
<mat-icon svgIcon="red:user"></mat-icon>
|
||||
</div>
|
||||
<div translate="filters.due-date.label" class="icon-button">
|
||||
</button>
|
||||
<button mat-button translate="filters.due-date.label">
|
||||
<mat-icon svgIcon="red:lightning"></mat-icon>
|
||||
</div>
|
||||
<div translate="filters.created-on.label" class="icon-button">
|
||||
</button>
|
||||
<button mat-button translate="filters.created-on.label">
|
||||
<mat-icon svgIcon="red:calendar"></mat-icon>
|
||||
</div>
|
||||
<div translate="filters.project.label" class="icon-button">
|
||||
</button>
|
||||
<button mat-button translate="filters.project.label">
|
||||
<mat-icon svgIcon="red:folder"></mat-icon>
|
||||
</div>
|
||||
<div translate="filters.document.label" class="icon-button">
|
||||
</button>
|
||||
<button mat-button translate="filters.document.label">
|
||||
<mat-icon svgIcon="red:document"></mat-icon>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<button (click)="fileInput.click()" color="primary" mat-flat-button
|
||||
translate="project-overview.upload-document.label"></button>
|
||||
@ -53,7 +53,8 @@
|
||||
<div class="flex-4 small-label min-width" translate="project-overview.table-col-names.added-on.label"></div>
|
||||
<div class="flex-2 small-label min-width" translate="project-overview.table-col-names.needs-work.label"></div>
|
||||
<div class="flex-2 small-label min-width" translate="project-overview.table-col-names.assigned-to.label"></div>
|
||||
<div class="flex-1 small-label status-container min-width" translate="project-overview.table-col-names.status.label"></div>
|
||||
<div class="flex-1 small-label status-container min-width"
|
||||
translate="project-overview.table-col-names.status.label"></div>
|
||||
</div>
|
||||
|
||||
<div class="table-item"
|
||||
@ -69,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">
|
||||
@ -112,13 +113,13 @@
|
||||
|
||||
<div class="project-details-container right-fixed-container">
|
||||
<div class="actions-row">
|
||||
<button mat-icon-button class="icon-button" (click)="deleteProject($event)">
|
||||
<button mat-icon-button (click)="deleteProject($event)">
|
||||
<mat-icon svgIcon="red:trash"></mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button class="icon-button" (click)="editProject($event)">
|
||||
<button mat-icon-button (click)="editProject($event)">
|
||||
<mat-icon svgIcon="red:edit"></mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button class="icon-button" (click)="showDetailsDialog($event)">
|
||||
<button mat-icon-button (click)="showDetailsDialog($event)">
|
||||
<mat-icon svgIcon="red:report"></mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
@ -168,5 +169,14 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-20">
|
||||
<redaction-simple-doughnut-chart [config]="documentsChartData"
|
||||
[strokeWidth]="15"
|
||||
[radius]="70"
|
||||
[subtitle]="'project-overview.project-details.charts.total-documents.label'"
|
||||
direction="row"
|
||||
></redaction-simple-doughnut-chart>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import {ChangeDetectorRef, Component, OnDestroy, OnInit, ViewChild} from '@angular/core';
|
||||
import {ActivatedRoute, Router} from '@angular/router';
|
||||
import { ChangeDetectorRef, Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import {
|
||||
FileStatus,
|
||||
FileUploadControllerService,
|
||||
@ -7,19 +7,20 @@ import {
|
||||
ReanalysisControllerService,
|
||||
StatusControllerService
|
||||
} from '@redaction/red-ui-http';
|
||||
import {NotificationService, NotificationType} from '../../notification/notification.service';
|
||||
import {TranslateService} from '@ngx-translate/core';
|
||||
import {ConfirmationDialogComponent} from '../../common/confirmation-dialog/confirmation-dialog.component';
|
||||
import {MatDialog} from '@angular/material/dialog';
|
||||
import {AppStateService} from '../../state/app-state.service';
|
||||
import {ProjectDetailsDialogComponent} from './project-details-dialog/project-details-dialog.component';
|
||||
import {FileDropOverlayService} from '../../upload/file-drop/service/file-drop-overlay.service';
|
||||
import {FileUploadModel} from "../../upload/model/file-upload.model";
|
||||
import {FileUploadService} from "../../upload/file-upload.service";
|
||||
import {UploadStatusOverlayService} from "../../upload/upload-status-dialog/service/upload-status-overlay.service";
|
||||
import {AddEditProjectDialogComponent} from "../project-listing-screen/add-edit-project-dialog/add-edit-project-dialog.component";
|
||||
import {UserService} from "../../user/user.service";
|
||||
import { NotificationService, NotificationType } from '../../notification/notification.service';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { ConfirmationDialogComponent } from '../../common/confirmation-dialog/confirmation-dialog.component';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { AppStateService } from '../../state/app-state.service';
|
||||
import { ProjectDetailsDialogComponent } from './project-details-dialog/project-details-dialog.component';
|
||||
import { FileDropOverlayService } from '../../upload/file-drop/service/file-drop-overlay.service';
|
||||
import { FileUploadModel } from '../../upload/model/file-upload.model';
|
||||
import { FileUploadService } from '../../upload/file-upload.service';
|
||||
import { UploadStatusOverlayService } from '../../upload/upload-status-dialog/service/upload-status-overlay.service';
|
||||
import { AddEditProjectDialogComponent } from '../project-listing-screen/add-edit-project-dialog/add-edit-project-dialog.component';
|
||||
import { UserService } from '../../user/user.service';
|
||||
import { SortingOption } from '../../utils/types';
|
||||
import { DoughnutChartConfig } from '../../components/simple-doughnut-chart/simple-doughnut-chart.component';
|
||||
|
||||
|
||||
@Component({
|
||||
@ -28,7 +29,7 @@ import { SortingOption } from '../../utils/types';
|
||||
styleUrls: ['./project-overview-screen.component.scss']
|
||||
})
|
||||
export class ProjectOverviewScreenComponent implements OnInit, OnDestroy {
|
||||
@ViewChild('dropzoneComponent', {static: true})
|
||||
@ViewChild('dropzoneComponent', { static: true })
|
||||
dropZoneComponent;
|
||||
|
||||
dragActive = false;
|
||||
@ -37,10 +38,12 @@ export class ProjectOverviewScreenComponent implements OnInit, OnDestroy {
|
||||
{ label: 'project-overview.sorting.recent.label', order: 'desc', column: 'lastUpdated' },
|
||||
{ label: 'project-overview.sorting.alphabetically.label', order: 'asc', column: 'filename' },
|
||||
{ label: 'project-overview.sorting.number-of-pages.label', order: 'asc', column: 'numberOfPages' },
|
||||
{ label: 'project-overview.sorting.number-of-analyses.label', order: 'desc', column: 'numberOfAnalyses' },
|
||||
{ label: 'project-overview.sorting.number-of-analyses.label', order: 'desc', column: 'numberOfAnalyses' }
|
||||
];
|
||||
public sortingOption: SortingOption = this.sortingOptions[0];
|
||||
|
||||
public documentsChartData: DoughnutChartConfig[] = [];
|
||||
|
||||
projectId: string;
|
||||
private _fileStatusInterval;
|
||||
|
||||
@ -71,6 +74,7 @@ export class ProjectOverviewScreenComponent implements OnInit, OnDestroy {
|
||||
|
||||
ngOnInit(): void {
|
||||
this._fileDropOverlayService.initFileDropHandling();
|
||||
this._calculateChartConfig();
|
||||
this._fileStatusInterval = setInterval(() => {
|
||||
this._getFileStatus();
|
||||
}, 5000);
|
||||
@ -120,7 +124,22 @@ export class ProjectOverviewScreenComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
private _getFileStatus() {
|
||||
this.appStateService.reloadActiveProjectFiles();
|
||||
this.appStateService.reloadActiveProjectFiles().then(() => {
|
||||
this._calculateChartConfig();
|
||||
});
|
||||
}
|
||||
|
||||
private _calculateChartConfig() {
|
||||
const obj = this.appStateService.activeProject.files.reduce((acc, file) => {
|
||||
acc[file.status === 'PROCESSED' ? 'finished' : file.status === 'ERROR' ? 'under-approval' : 'under-review']++;
|
||||
return acc;
|
||||
}, { 'finished': 0, 'under-approval': 0, 'under-review': 0 });
|
||||
|
||||
this.documentsChartData = Object.keys(obj).map((key) => ({
|
||||
value: obj[key],
|
||||
color: key,
|
||||
label: key
|
||||
})) as DoughnutChartConfig[];
|
||||
}
|
||||
|
||||
fileId(index, item) {
|
||||
@ -137,7 +156,7 @@ export class ProjectOverviewScreenComponent implements OnInit, OnDestroy {
|
||||
progress: 0,
|
||||
completed: false,
|
||||
error: null
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
this._fileUploadService.uploadFiles(uploadFiles);
|
||||
@ -157,7 +176,7 @@ export class ProjectOverviewScreenComponent implements OnInit, OnDestroy {
|
||||
$event.stopPropagation();
|
||||
const dialogRef = this._dialog.open(ConfirmationDialogComponent, {
|
||||
width: '400px',
|
||||
maxWidth: '90vw',
|
||||
maxWidth: '90vw'
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe(result => {
|
||||
|
||||
@ -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,89 @@ export class AnnotationUtils {
|
||||
return ann1.getPageNumber() < ann2.getPageNumber() ? -1 : 1;
|
||||
});
|
||||
}
|
||||
|
||||
public static hasSubsections(filter: AnnotationFilters | boolean) {
|
||||
return filter instanceof Object;
|
||||
}
|
||||
|
||||
public static checkedSubkeys(filter: AnnotationFilters | boolean) {
|
||||
return Object.keys(filter).filter(subkey => this.isChecked(filter[subkey])).length;
|
||||
}
|
||||
|
||||
// Only some of the sub-items are selected
|
||||
public static isIndeterminate(filter: AnnotationFilters | boolean): boolean {
|
||||
return this.hasSubsections(filter) ? AnnotationUtils.checkedSubkeys(filter) > 0 && !this.isChecked(filter) : false;
|
||||
}
|
||||
|
||||
// All sub-items are selected
|
||||
public static isChecked(filter: AnnotationFilters | boolean): boolean {
|
||||
return this.hasSubsections(filter) ?
|
||||
AnnotationUtils.checkedSubkeys(filter) === Object.keys(filter).length :
|
||||
filter as boolean;
|
||||
}
|
||||
|
||||
public static hasActiveFilters(filter: AnnotationFilters): boolean {
|
||||
const activeFilters = Object.keys(filter).filter(key => {
|
||||
return this.isChecked(filter[key]) || this.isIndeterminate(filter[key]);
|
||||
});
|
||||
return activeFilters.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);
|
||||
const dictionary = this.getDictionary(ann);
|
||||
|
||||
if (this.hasActiveFilters(filters)) {
|
||||
if (!this.hasSubsections(filters[type]) && !filters[type]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (this.hasSubsections(filters[type]) && !filters[type][dictionary]) {
|
||||
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
|
||||
}
|
||||
|
||||
@ -329,6 +329,11 @@
|
||||
"project-details": {
|
||||
"project-team": {
|
||||
"label": "Project team"
|
||||
},
|
||||
"charts": {
|
||||
"total-documents": {
|
||||
"label": "Total Documents"
|
||||
}
|
||||
}
|
||||
},
|
||||
"header": {
|
||||
@ -357,18 +362,27 @@
|
||||
"label": "Filter types"
|
||||
},
|
||||
"hint": {
|
||||
"label": "Hint annotation"
|
||||
"label": "Hint annotation",
|
||||
"hint_only": {
|
||||
"label": "Hint only"
|
||||
},
|
||||
"vertebrate": {
|
||||
"label": "Vertebrate"
|
||||
},
|
||||
"names": {
|
||||
"label": "Names"
|
||||
}
|
||||
},
|
||||
"redaction": {
|
||||
"label": "Redaction"
|
||||
},
|
||||
"comment": {
|
||||
"label": "Comment"
|
||||
"label": "Comment annotation"
|
||||
},
|
||||
"suggestion": {
|
||||
"label": "Suggested redaction"
|
||||
},
|
||||
"ignored": {
|
||||
"ignore": {
|
||||
"label": "Ignored redaction"
|
||||
}
|
||||
},
|
||||
@ -414,5 +428,22 @@
|
||||
"unassigned": {
|
||||
"label": "Unassigned"
|
||||
}
|
||||
}
|
||||
},
|
||||
"unassigned": "Unassigned",
|
||||
"under-review": "Under review",
|
||||
"under-approval": "Under approval",
|
||||
"efsa": "EFSA approval",
|
||||
"finished": "Finished",
|
||||
"approved": "Approved",
|
||||
"submitted": "Submitted",
|
||||
"active": "Active",
|
||||
"archived": "Archived",
|
||||
"hint": "Hint",
|
||||
"ignore": "Ignore",
|
||||
"redaction": "Redaction",
|
||||
"comment": "Comment",
|
||||
"suggestion": "Suggestion for redaction",
|
||||
"dictionary": "Dictionary",
|
||||
"content": "Content",
|
||||
"page": "Page"
|
||||
}
|
||||
|
||||
@ -1,9 +1,7 @@
|
||||
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px"
|
||||
width="255px" height="255px" viewBox="0 0 255 255" style="enable-background:new 0 0 255 255;" xml:space="preserve">
|
||||
<g>
|
||||
<g id="arrow-drop-down">
|
||||
<polygon points="0,63.75 127.5,191.25 255,63.75 "/>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
</svg>
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="14px" height="14px" viewBox="0 0 14 14" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>down</title>
|
||||
<g id="down" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<polygon id="Fill-1" fill="#283241" points="7 9 10 5 4 5"></polygon>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 308 B After Width: | Height: | Size: 381 B |
@ -20,25 +20,16 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.dropdown-icon {
|
||||
width: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.mat-button[aria-expanded="true"], .mat-button.overlay {
|
||||
background: rgba($primary, 0.1);
|
||||
}
|
||||
|
||||
.icon-button {
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
.mat-button, .mat-flat-button, .mat-icon-button {
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
|
||||
|
||||
mat-icon {
|
||||
width: 14px;
|
||||
color: $accent;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -3,14 +3,27 @@
|
||||
.mat-menu-panel {
|
||||
border-radius: 8px !important;
|
||||
box-shadow: 0 2px 6px 0 rgba(40, 50, 65, 0.3);
|
||||
max-width: none !important;
|
||||
|
||||
.mat-menu-item {
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-size: 13px;
|
||||
color: $accent;
|
||||
|
||||
.arrow-wrapper {
|
||||
width: 24px;
|
||||
|
||||
mat-icon {
|
||||
width: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
&.padding-left {
|
||||
padding-left: 64px;
|
||||
}
|
||||
|
||||
.mat-checkbox-layout {
|
||||
width: 100%;
|
||||
margin-left: 4px;
|
||||
|
||||
.mat-checkbox-inner-container {
|
||||
margin-left: 0;
|
||||
|
||||
@ -58,8 +58,12 @@ html, body {
|
||||
}
|
||||
|
||||
.filters {
|
||||
> div {
|
||||
padding: 10px 14px;
|
||||
div {
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
button {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
}
|
||||
|
||||
@ -69,7 +73,7 @@ html, body {
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
display: flex !important;
|
||||
}
|
||||
|
||||
.flex-1 {
|
||||
|
||||
@ -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;
|
||||
@ -18,8 +14,15 @@ $blue-3: #5B97DB;
|
||||
$blue-4: #374C81;
|
||||
$red-1: #DD4D50;
|
||||
$yellow-1: #FFB83B;
|
||||
$green-1: #46CE7D;
|
||||
$yellow-2: #FFFF02;
|
||||
$green-1: #00FF00;
|
||||
$green-2: #5CE594;
|
||||
$orange-1: #FF801A;
|
||||
|
||||
$primary: $red-1;
|
||||
$accent: $grey-1;
|
||||
$light: $white;
|
||||
$dark: $black;
|
||||
|
||||
$separator: rgba(226,228,233,0.9);
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user