Added Manual Redaction Dialog

This commit is contained in:
Timo Bejan 2020-10-13 16:21:30 +03:00
parent 3e5388903d
commit c11f84ab72
16 changed files with 387 additions and 121 deletions

View File

@ -34,5 +34,17 @@
"secure": false,
"changeOrigin": true,
"logLevel": "debug"
},
"/dictionary": {
"target": "https://timo-redaction-dev.iqser.cloud/",
"secure": false,
"changeOrigin": true,
"logLevel": "debug"
},
"/manualRedaction": {
"target": "https://timo-redaction-dev.iqser.cloud/",
"secure": false,
"changeOrigin": true,
"logLevel": "debug"
}
}

View File

@ -51,6 +51,8 @@ import {CompositeRouteGuard} from "./utils/composite-route.guard";
import {AppStateGuard} from "./state/app-state.guard";
import {ChartsModule} from "ng2-charts";
import { SimpleDoughnutChartComponent } from './simple-doughnut-chart/simple-doughnut-chart.component';
import { ManualRedactionDialogComponent } from './screens/file/manual-redaction-dialog/manual-redaction-dialog.component';
import {MatCheckboxModule} from "@angular/material/checkbox";
export function HttpLoaderFactory(httpClient: HttpClient) {
return new TranslateHttpLoader(httpClient, '/assets/i18n/', '.json');
@ -72,80 +74,82 @@ export function HttpLoaderFactory(httpClient: HttpClient) {
InitialsAvatarComponent,
StatusBarComponent,
LogoComponent,
SimpleDoughnutChartComponent
SimpleDoughnutChartComponent,
ManualRedactionDialogComponent
],
imports: [
BrowserModule,
BrowserAnimationsModule,
ChartsModule,
ReactiveFormsModule,
HttpClientModule,
AuthModule,
IconsModule,
ApiModule,
MatDialogModule,
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useFactory: HttpLoaderFactory,
deps: [HttpClient]
}
}),
RouterModule.forRoot([
{
path: '',
redirectTo: 'ui/projects',
pathMatch: 'full'
},
{
path: 'ui',
component: BaseScreenComponent,
children: [
{
path: 'projects',
component: ProjectListingScreenComponent,
canActivate: [CompositeRouteGuard],
data: {
routeGuards: [AuthGuard, AppStateGuard],
imports: [
BrowserModule,
BrowserAnimationsModule,
ChartsModule,
ReactiveFormsModule,
HttpClientModule,
AuthModule,
IconsModule,
ApiModule,
MatDialogModule,
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useFactory: HttpLoaderFactory,
deps: [HttpClient]
}
},
{
path: 'projects/:projectId',
component: ProjectOverviewScreenComponent,
canActivate: [CompositeRouteGuard],
data: {
routeGuards: [AuthGuard, AppStateGuard],
}),
RouterModule.forRoot([
{
path: '',
redirectTo: 'ui/projects',
pathMatch: 'full'
},
{
path: 'ui',
component: BaseScreenComponent,
children: [
{
path: 'projects',
component: ProjectListingScreenComponent,
canActivate: [CompositeRouteGuard],
data: {
routeGuards: [AuthGuard, AppStateGuard],
}
},
{
path: 'projects/:projectId',
component: ProjectOverviewScreenComponent,
canActivate: [CompositeRouteGuard],
data: {
routeGuards: [AuthGuard, AppStateGuard],
}
},
{
path: 'projects/:projectId/file/:fileId',
component: FilePreviewScreenComponent,
canActivate: [CompositeRouteGuard],
data: {
routeGuards: [AuthGuard, AppStateGuard],
}
}
]
}
},
{
path: 'projects/:projectId/file/:fileId',
component: FilePreviewScreenComponent,
canActivate: [CompositeRouteGuard],
data: {
routeGuards: [AuthGuard, AppStateGuard],
}
}
]
}
]),
NgpSortModule,
MatToolbarModule,
MatButtonModule,
MatMenuModule,
MatIconModule,
MatTooltipModule,
MatSnackBarModule,
MatTabsModule,
MatButtonToggleModule,
MatFormFieldModule,
ToastrModule.forRoot(),
MatSelectModule,
MatSidenavModule,
FileUploadModule,
ServiceWorkerModule.register('ngsw-worker.js', {enabled: environment.production}),
MatProgressSpinnerModule
],
]),
NgpSortModule,
MatToolbarModule,
MatButtonModule,
MatMenuModule,
MatIconModule,
MatTooltipModule,
MatSnackBarModule,
MatTabsModule,
MatButtonToggleModule,
MatFormFieldModule,
ToastrModule.forRoot(),
MatSelectModule,
MatSidenavModule,
FileUploadModule,
ServiceWorkerModule.register('ngsw-worker.js', {enabled: environment.production}),
MatProgressSpinnerModule,
MatCheckboxModule
],
providers: [{
provide: HTTP_INTERCEPTORS,
multi: true,

View File

@ -12,7 +12,6 @@
</button>
</div>
</div>
<button color="warn" mat-flat-button
translate="file-preview.download-redacted.label"></button>
</div>
@ -22,9 +21,11 @@
<redaction-pdf-viewer [class.visible]="activeViewer === 'ANNOTATED'" [fileId]="fileId" fileType="ANNOTATED"
[fileStatus]="appStateService.activeFile"
(fileReady)="fileReady('ANNOTATED')"
(manualAnnotationRequested)="handleManualAnnotationRequest($event)"
(annotationSelected)="handleAnnotationSelected($event)"
(annotationsAdded)="handleAnnotationsAdded($event)"></redaction-pdf-viewer>
<redaction-pdf-viewer [class.visible]="activeViewer === 'REDACTED'" [fileId]="fileId" fileType="REDACTED"
(manualAnnotationRequested)="handleManualAnnotationRequest($event)"
(fileReady)="fileReady('REDACTED')"></redaction-pdf-viewer>
</div>
@ -126,4 +127,6 @@
</button>
</section>
<button class="hidden" (click)="openManualRedactionDialog()" id="open-manual-redaction-dialog-btn"></button>
<redaction-full-page-loading-indicator [displayed]="!viewReady"></redaction-full-page-loading-indicator>

View File

@ -1,16 +1,21 @@
import { ChangeDetectorRef, Component, ElementRef, OnInit, ViewChild } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { FileUploadControllerService, ProjectControllerService, StatusControllerService } from '@redaction/red-ui-http';
import { TranslateService } from '@ngx-translate/core';
import { NotificationService } 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 {ChangeDetectorRef, Component, ElementRef, OnInit, ViewChild} from '@angular/core';
import {ActivatedRoute, Router} from '@angular/router';
import {
FileUploadControllerService,
ManualRedactionEntry,
ProjectControllerService,
StatusControllerService
} from '@redaction/red-ui-http';
import {TranslateService} from '@ngx-translate/core';
import {NotificationService} 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";
@Component({
@ -36,6 +41,8 @@ export class FilePreviewScreenComponent implements OnInit {
public selectedPageNumber: number;
public quickNavigation: { pageNumber: number, redactions: number, hints: number }[] = [];
private _manualRedactionEntry: ManualRedactionEntry;
constructor(
public readonly appStateService: AppStateService,
private readonly _changeDetectorRef: ChangeDetectorRef,
@ -97,11 +104,15 @@ export class FilePreviewScreenComponent implements OnInit {
const pageNumber = annotation.getPageNumber();
let el = this.quickNavigation.find((page) => page.pageNumber === pageNumber);
if (!el) {
el = { pageNumber, redactions: 0, hints: 0 }
el = {pageNumber, redactions: 0, hints: 0}
this.quickNavigation.push(el);
}
if (annotation.Id.startsWith('hint:')) { el.hints++; }
if (annotation.Id.startsWith('redaction:')) { el.redactions++; }
if (annotation.Id.startsWith('hint:')) {
el.hints++;
}
if (annotation.Id.startsWith('redaction:')) {
el.redactions++;
}
}
}
this.annotations = AnnotationUtils.sortAnnotations(this.annotations);
@ -131,12 +142,12 @@ export class FilePreviewScreenComponent implements OnInit {
return;
}
const { top, height } = el.getBoundingClientRect();
const {top, height} = el.getBoundingClientRect();
const headerHeight = window.innerHeight - this._annotationsContainer.nativeElement.getBoundingClientRect().height;
if (top < headerHeight || top > window.innerHeight - height - 30) {
const scrollTop = this._annotationsContainer.nativeElement.scrollTop - 30;
this._annotationsContainer.nativeElement.scroll({ top: scrollTop + top - headerHeight, behavior: 'smooth' });
this._annotationsContainer.nativeElement.scroll({top: scrollTop + top - headerHeight, behavior: 'smooth'});
}
}
@ -156,4 +167,21 @@ export class FilePreviewScreenComponent implements OnInit {
this.selectedPageNumber = pageNumber;
this._viewerComponent.navigateToPage(pageNumber);
}
handleManualAnnotationRequest($event: ManualRedactionEntry) {
this._manualRedactionEntry = $event;
document.getElementById('open-manual-redaction-dialog-btn').click();
}
openManualRedactionDialog() {
const ref = this._dialog.open(ManualRedactionDialogComponent, {
width: '600px',
maxWidth: '90vw',
data: this._manualRedactionEntry
});
ref.afterClosed().subscribe(() => {
this._manualRedactionEntry = null;
})
}
}

View File

@ -0,0 +1,45 @@
<section class="dialog">
<form (submit)="saveManualRedaction()" [formGroup]="redactionForm">
<div
class="dialog-header heading-l" translate="manual-redaction.dialog.header.label">
</div>
<div class="dialog-content">
<div class="file-details">
<div class="detail-row"
[innerHTML]="'manual-redaction.dialog.content.text.label' | translate:manualRedactionInput">
</div>
</div>
<div class="red-input-group">
<label translate="manual-redaction.dialog.content.reason.label"></label>
<textarea formControlName="reason" name="reason" type="text" rows="3"></textarea>
</div>
<div class="red-input-group">
<mat-checkbox
formControlName="addToDictionary">{{'manual-redaction.dialog.content.dictionary.add.label' | translate}}</mat-checkbox>
</div>
<mat-form-field [class.hidden]="showDictionary">
<mat-label>{{'manual-redaction.dialog.content.dictionary.label' | translate}}</mat-label>
<mat-select formControlName="dictionary">
<mat-option *ngFor="let dictionary of dictionaries | async" [value]="dictionary.type">
{{dictionary.type}}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="dialog-actions">
<button color="primary" mat-flat-button [disabled]="!redactionForm.valid"
translate="manual-redaction.dialog.actions.save.label" type="submit"></button>
</div>
</form>
<button (click)="dialogRef.close()" class="dialog-close" mat-icon-button>
<mat-icon svgIcon="red:close"></mat-icon>
</button>
</section>

View File

@ -0,0 +1,70 @@
import {Component, Inject, OnInit} from '@angular/core';
import {FormBuilder, FormGroup, Validators} from "@angular/forms";
import {AppStateService} from "../../../state/app-state.service";
import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog";
import {
DictionaryControllerService,
ManualRedactionControllerService,
ManualRedactionEntry,
TypeValue
} from "@redaction/red-ui-http";
import {NotificationService, NotificationType} from "../../../notification/notification.service";
import {TranslateService} from "@ngx-translate/core";
import {map} from "rxjs/operators";
import {Observable} from "rxjs";
@Component({
selector: 'redaction-manual-redaction-dialog',
templateUrl: './manual-redaction-dialog.component.html',
styleUrls: ['./manual-redaction-dialog.component.scss']
})
export class ManualRedactionDialogComponent implements OnInit {
redactionForm: FormGroup;
dictionaries: Observable<Array<TypeValue>>;
constructor(
private readonly _appStateService: AppStateService,
private readonly _formBuilder: FormBuilder,
private readonly _notificationService: NotificationService,
private readonly _translateService: TranslateService,
private readonly _manualRedactionControllerService: ManualRedactionControllerService,
private readonly _dictionaryControllerService: DictionaryControllerService,
public dialogRef: MatDialogRef<ManualRedactionDialogComponent>,
@Inject(MAT_DIALOG_DATA) public manualRedactionInput: ManualRedactionEntry) {
}
async ngOnInit() {
this.redactionForm = this._formBuilder.group({
addToDictionary: [false],
reason: [null, Validators.required],
dictionary: null,
});
this.dictionaries = this._dictionaryControllerService.getAllTypes().pipe(map(r => r.types));
}
saveManualRedaction() {
const mre = Object.assign({}, this.manualRedactionInput);
this._enhanceManualRedaction(mre);
this._manualRedactionControllerService.updateManualRedaction({entriesToAdd: [mre]}, this._appStateService.activeProject.project.projectId, this._appStateService.activeFile.fileId).subscribe(ok=>{
this._notificationService.showToastNotification(this._translateService.instant('manual-redaction.dialog.add-redaction.success.label'), null, NotificationType.SUCCESS);
this.dialogRef.close();
},(err)=>{
this._notificationService.showToastNotification(this._translateService.instant('manual-redaction.dialog.add-redaction.failed.label',err), null, NotificationType.SUCCESS);
});
}
private _enhanceManualRedaction(mre: ManualRedactionEntry) {
mre.type = this.redactionForm.get('dictionary').value;
mre.addToDictionary = this.redactionForm.get('addToDictionary').value;
mre.reason = this.redactionForm.get('reason').value;
}
get showDictionary(){
return !this.redactionForm.get('addToDictionary').value
}
}

View File

@ -9,13 +9,15 @@ import {
Output,
ViewChild
} from '@angular/core';
import { AppConfigKey, AppConfigService } from '../../../app-config/app-config.service';
import { FileStatus, FileUploadControllerService } from '@redaction/red-ui-http';
import { Observable, of } from 'rxjs';
import { tap } from 'rxjs/operators';
import WebViewer, { Annotations, WebViewerInstance } from '@pdftron/webviewer';
import { TranslateService } from '@ngx-translate/core';
import { ViewerSyncService } from '../service/viewer-sync.service';
import {AppConfigKey, AppConfigService} from '../../../app-config/app-config.service';
import {FileStatus, FileUploadControllerService, ManualRedactionEntry, Rectangle} from '@redaction/red-ui-http';
import {Observable, of} from 'rxjs';
import {tap} from 'rxjs/operators';
import WebViewer, {Annotations, WebViewerInstance} from '@pdftron/webviewer';
import {TranslateService} from '@ngx-translate/core';
import {ViewerSyncService} from '../service/viewer-sync.service';
import {MatDialog} from "@angular/material/dialog";
import {ManualRedactionDialogComponent} from "../manual-redaction-dialog/manual-redaction-dialog.component";
export enum FileType {
ORIGINAL = 'ORIGINAL',
@ -36,8 +38,9 @@ export class PdfViewerComponent implements OnInit, AfterViewInit, OnDestroy {
@Output() fileReady = new EventEmitter();
@Output() annotationsAdded = new EventEmitter<Annotations.Annotation[]>();
@Output() annotationSelected = new EventEmitter<Annotations.Annotation>();
@Output() manualAnnotationRequested = new EventEmitter<ManualRedactionEntry>();
@ViewChild('viewer', { static: true }) viewer: ElementRef;
@ViewChild('viewer', {static: true}) viewer: ElementRef;
wvInstance: WebViewerInstance;
_fileData: Blob;
@ -46,6 +49,7 @@ export class PdfViewerComponent implements OnInit, AfterViewInit, OnDestroy {
private readonly _viewerSyncService: ViewerSyncService,
private readonly _translateService: TranslateService,
private readonly _fileUploadControllerService: FileUploadControllerService,
private readonly _dialog: MatDialog,
private readonly _appConfigService: AppConfigService) {
}
@ -95,7 +99,7 @@ export class PdfViewerComponent implements OnInit, AfterViewInit, OnDestroy {
}));
instance.docViewer.on('documentLoaded', this.wvDocumentLoadedHandler);
instance.loadDocument(pdfBlob, { filename: this.fileStatus ? this.fileStatus.filename : 'file.pdf' });
instance.loadDocument(pdfBlob, {filename: this.fileStatus ? this.fileStatus.filename : 'file.pdf'});
});
}
@ -145,11 +149,31 @@ export class PdfViewerComponent implements OnInit, AfterViewInit, OnDestroy {
title: this._translateService.instant('pdf-viewer.text-popup.actions.suggestion-redaction.label'),
onClick: () => {
const selectedQuads = this.wvInstance.docViewer.getSelectedTextQuads();
console.log(selectedQuads);
const text = this.wvInstance.docViewer.getSelectedText();
const entry: ManualRedactionEntry = {positions: []};
for (let key of Object.keys(selectedQuads)) {
for (let quad of selectedQuads[key]) {
entry.positions.push(this.toPosition(parseInt(key), quad));
}
}
entry.value = text;
this.manualAnnotationRequested.emit(entry);
}
});
}
private toPosition(page: number, selectedQuad: any): Rectangle {
return {
page: page,
topLeft: {
x: selectedQuad.x4,
y: selectedQuad.y4
},
height: selectedQuad.y2 - selectedQuad.y4,
width: selectedQuad.x3 - selectedQuad.x4
}
}
private _configureHeader() {
this.wvInstance.setToolbarGroup('toolbarGroup-View');
}

View File

@ -1,4 +1,38 @@
{
"manual-redaction": {
"dialog": {
"header": {
"label": "Add Manual Redaction"
},
"add-redaction": {
"success": {
"label": "Redaction suggestion added!"
},
"failed": {
"label": "Failed to add manual redaction: {{message}}"
}
},
"actions": {
"save": {
"label": "Save Manual Redaction"
}
},
"content": {
"text": {
"label": "<strong>Selected Text: </strong> {{value}}"
},
"dictionary": {
"add": {
"label": "Add to dictionary"
},
"label": "Dictionary"
},
"reason": {
"label": "Reason"
}
}
}
},
"app-name": {
"label": "Redacto"
},
@ -195,15 +229,12 @@
"name": {
"label": "Name"
},
"added-on": {
"label": "Added on"
},
"added-by": {
"label": "Added by"
},
"assigned-to": {
"label": "Assigned to"
},

View File

@ -185,5 +185,5 @@ html, body {
}
.hidden {
display: none;
display: none !important;
}

View File

@ -17,6 +17,12 @@ server {
location /project {
proxy_pass $API_URL;
}
location /dictionary {
proxy_pass $API_URL;
}
location /manualRedaction {
proxy_pass $API_URL;
}
location /reanalyse {
proxy_pass $API_URL;
}

View File

@ -0,0 +1,17 @@
/**
* API Documentation for Redaction Gateway
* Description for redaction
*
* OpenAPI spec version: 1.0
*
*
* NOTE: This class is auto generated by the swagger code generator program.
* https://github.com/swagger-api/swagger-codegen.git
* Do not edit the class manually.
*/
export interface Comment {
date?: string;
text?: string;
user?: string;
}

View File

@ -0,0 +1,19 @@
/**
* API Documentation for Redaction Gateway
* Description for redaction
*
* OpenAPI spec version: 1.0
*
*
* NOTE: This class is auto generated by the swagger code generator program.
* https://github.com/swagger-api/swagger-codegen.git
* Do not edit the class manually.
*/
import { Comment } from './comment';
export interface IdRemoval {
approved?: boolean;
comments?: Array<Comment>;
id?: string;
removeFromDictionary?: boolean;
}

View File

@ -1,19 +1,23 @@
/**
* Api Documentation
* Api Documentation
* API Documentation for Redaction Gateway
* Description for redaction
*
* OpenAPI spec version: 1.0
*
*
*
* NOTE: This class is auto generated by the swagger code generator program.
* https://github.com/swagger-api/swagger-codegen.git
* Do not edit the class manually.
*/
import {Rectangle} from './rectangle';
import { Comment } from './comment';
import { Rectangle } from './rectangle';
export interface ManualRedactionEntry {
positions?: Array<Rectangle>;
reason?: string;
type?: string;
value?: string;
}
export interface ManualRedactionEntry {
addToDictionary?: boolean;
approved?: boolean;
comments?: Array<Comment>;
positions?: Array<Rectangle>;
reason?: string;
type?: string;
value?: string;
}

View File

@ -1,17 +1,18 @@
/**
* Api Documentation
* Api Documentation
* API Documentation for Redaction Gateway
* Description for redaction
*
* OpenAPI spec version: 1.0
*
*
*
* NOTE: This class is auto generated by the swagger code generator program.
* https://github.com/swagger-api/swagger-codegen.git
* Do not edit the class manually.
*/
import {ManualRedactionEntry} from './manualRedactionEntry';
import { IdRemoval } from './idRemoval';
import { ManualRedactionEntry } from './manualRedactionEntry';
export interface ManualRedactions {
entriesToAdd?: Array<ManualRedactionEntry>;
idsToRemove?: Array<string>;
}
export interface ManualRedactions {
entriesToAdd?: Array<ManualRedactionEntry>;
idsToRemove?: Array<IdRemoval>;
}

View File

@ -20,3 +20,5 @@ export * from './typeResponse';
export * from './typeValue';
export * from './fileUploadResult';
export * from './authInfo';
export * from './comment';
export * from './idRemoval';