diff --git a/apps/red-ui/src/app/app.module.ts b/apps/red-ui/src/app/app.module.ts index 8ce6ccdba..dbe3ba001 100644 --- a/apps/red-ui/src/app/app.module.ts +++ b/apps/red-ui/src/app/app.module.ts @@ -60,6 +60,7 @@ import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatNativeDateModule } from '@angular/material/core'; import { MatInputModule } from '@angular/material/input'; import { ProjectMemberGuard } from './auth/project-member-guard.service'; +import { HumanizePipe } from './utils/humanize.pipe'; export function HttpLoaderFactory(httpClient: HttpClient) { return new TranslateHttpLoader(httpClient, '/assets/i18n/', '.json'); @@ -84,7 +85,8 @@ export function HttpLoaderFactory(httpClient: HttpClient) { SimpleDoughnutChartComponent, ManualRedactionDialogComponent, AnnotationIconComponent, - AuthErrorComponent + AuthErrorComponent, + HumanizePipe ], imports: [ BrowserModule, diff --git a/apps/red-ui/src/app/dialogs/dialog.service.ts b/apps/red-ui/src/app/dialogs/dialog.service.ts index a8dd76ed4..9f1573358 100644 --- a/apps/red-ui/src/app/dialogs/dialog.service.ts +++ b/apps/red-ui/src/app/dialogs/dialog.service.ts @@ -16,6 +16,7 @@ import { AddEditProjectDialogComponent } from './add-edit-project-dialog/add-edi import { AssignOwnerDialogComponent } from './assign-owner-dialog/assign-owner-dialog.component'; import { ManualRedactionDialogComponent } from './manual-redaction-dialog/manual-redaction-dialog.component'; import { Annotations } from '@pdftron/webviewer'; +import { ManualRedactionEntryWrapper } from '../screens/file/model/manual-redaction-entry.wrapper'; const dialogConfig = { width: '600px', @@ -79,7 +80,7 @@ export class DialogService { } public openManualRedactionDialog( - $event: ManualRedactionEntry, + $event: ManualRedactionEntryWrapper, cb?: Function ): MatDialogRef { const ref = this._dialog.open(ManualRedactionDialogComponent, { @@ -89,6 +90,7 @@ export class DialogService { }); ref.afterClosed().subscribe((result) => { + console.log(result); if (cb) { cb(result); } diff --git a/apps/red-ui/src/app/dialogs/manual-redaction-dialog/manual-redaction-dialog.component.html b/apps/red-ui/src/app/dialogs/manual-redaction-dialog/manual-redaction-dialog.component.html index a30ad3022..e0553bffd 100644 --- a/apps/red-ui/src/app/dialogs/manual-redaction-dialog/manual-redaction-dialog.component.html +++ b/apps/red-ui/src/app/dialogs/manual-redaction-dialog/manual-redaction-dialog.component.html @@ -1,36 +1,39 @@
-
+
- {{ addRedactionRequest.value }} + {{ manualRedactionEntryWrapper.manualRedactionEntry.value }}
- +
- +
- {{ dictionary.type }} + {{ dictionary.label }}
-
+
{{ 'manual-redaction.dialog.content.dictionary.add.label' | translate }} diff --git a/apps/red-ui/src/app/dialogs/manual-redaction-dialog/manual-redaction-dialog.component.ts b/apps/red-ui/src/app/dialogs/manual-redaction-dialog/manual-redaction-dialog.component.ts index 57274bfca..fb8803313 100644 --- a/apps/red-ui/src/app/dialogs/manual-redaction-dialog/manual-redaction-dialog.component.ts +++ b/apps/red-ui/src/app/dialogs/manual-redaction-dialog/manual-redaction-dialog.component.ts @@ -10,9 +10,8 @@ import { } 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'; import { UserService } from '../../user/user.service'; +import { ManualRedactionEntryWrapper } from '../../screens/file/model/manual-redaction-entry.wrapper'; @Component({ selector: 'redaction-manual-redaction-dialog', @@ -20,8 +19,8 @@ import { UserService } from '../../user/user.service'; styleUrls: ['./manual-redaction-dialog.component.scss'] }) export class ManualRedactionDialogComponent implements OnInit { + dictionaryOptions: TypeValue[] = []; redactionForm: FormGroup; - dictionaries: Observable>; isDocumentAdmin: boolean; constructor( @@ -33,7 +32,7 @@ export class ManualRedactionDialogComponent implements OnInit { private readonly _manualRedactionControllerService: ManualRedactionControllerService, private readonly _dictionaryControllerService: DictionaryControllerService, public dialogRef: MatDialogRef, - @Inject(MAT_DIALOG_DATA) public addRedactionRequest: AddRedactionRequest + @Inject(MAT_DIALOG_DATA) public manualRedactionEntryWrapper: ManualRedactionEntryWrapper ) {} async ngOnInit() { @@ -47,9 +46,18 @@ export class ManualRedactionDialogComponent implements OnInit { dictionary: [null, Validators.required], comment: commentField }); - this.dictionaries = this._dictionaryControllerService - .getAllTypes() - .pipe(map((r) => r.types)); + + for (let key of Object.keys(this._appStateService.dictionaryData)) { + const dictionaryData = this._appStateService.dictionaryData[key]; + if (!dictionaryData.virtual) { + if (dictionaryData.hint && this.manualRedactionEntryWrapper.type === 'HINT') { + this.dictionaryOptions.push(dictionaryData); + } + if (!dictionaryData.hint && this.manualRedactionEntryWrapper.type === 'REDACTION') { + this.dictionaryOptions.push(dictionaryData); + } + } + } } handleAddRedaction() { @@ -61,7 +69,7 @@ export class ManualRedactionDialogComponent implements OnInit { } suggestManualRedaction() { - const mre = Object.assign({}, this.addRedactionRequest); + const mre = Object.assign({}, this.manualRedactionEntryWrapper.manualRedactionEntry); this._enhanceManualRedaction(mre); this._manualRedactionControllerService .requestAddRedaction( @@ -71,7 +79,6 @@ export class ManualRedactionDialogComponent implements OnInit { ) .subscribe( (ok) => { - this._appStateService.reanalyseActiveFile(); this._notificationService.showToastNotification( this._translateService.instant( 'manual-redaction.dialog.add-redaction.success.label' @@ -79,7 +86,7 @@ export class ManualRedactionDialogComponent implements OnInit { null, NotificationType.SUCCESS ); - this.dialogRef.close(); + this.dialogRef.close({ mode: 'suggestion', request: mre }); }, (err) => { this._notificationService.showToastNotification( @@ -95,7 +102,7 @@ export class ManualRedactionDialogComponent implements OnInit { } addManualRedaction() { - const mre = Object.assign({}, this.addRedactionRequest); + const mre = Object.assign({}, this.manualRedactionEntryWrapper.manualRedactionEntry); this._enhanceManualRedaction(mre); this._manualRedactionControllerService .addRedaction( @@ -105,7 +112,6 @@ export class ManualRedactionDialogComponent implements OnInit { ) .subscribe( (ok) => { - this._appStateService.reanalyseActiveFile(); this._notificationService.showToastNotification( this._translateService.instant( 'manual-redaction.dialog.add-redaction.success.label' @@ -113,7 +119,7 @@ export class ManualRedactionDialogComponent implements OnInit { null, NotificationType.SUCCESS ); - this.dialogRef.close(); + this.dialogRef.close({ mode: 'redaction', request: mre }); }, (err) => { this._notificationService.showToastNotification( @@ -128,9 +134,28 @@ export class ManualRedactionDialogComponent implements OnInit { ); } + get title() { + if (this.isDocumentAdmin) { + if (this.manualRedactionEntryWrapper.type === 'HINT') { + return 'manual-redaction.dialog.header.hint.label'; + } else { + return 'manual-redaction.dialog.header.redaction.label'; + } + } else { + if (this.manualRedactionEntryWrapper.type === 'HINT') { + return 'manual-redaction.dialog.header.request-hint.label'; + } else { + return 'manual-redaction.dialog.header.request-redaction.label'; + } + } + } + private _enhanceManualRedaction(addRedactionRequest: AddRedactionRequest) { addRedactionRequest.type = this.redactionForm.get('dictionary').value; - addRedactionRequest.addToDictionary = this.redactionForm.get('addToDictionary').value; + addRedactionRequest.addToDictionary = + this.manualRedactionEntryWrapper.type === 'HINT' + ? true + : this.redactionForm.get('addToDictionary').value; addRedactionRequest.reason = this.redactionForm.get('reason').value; const commentValue = this.redactionForm.get('comment').value; addRedactionRequest.comment = commentValue ? { text: commentValue } : null; diff --git a/apps/red-ui/src/app/screens/file/file-preview-screen/file-preview-screen.component.ts b/apps/red-ui/src/app/screens/file/file-preview-screen/file-preview-screen.component.ts index d986c9a05..31615f339 100644 --- a/apps/red-ui/src/app/screens/file/file-preview-screen/file-preview-screen.component.ts +++ b/apps/red-ui/src/app/screens/file/file-preview-screen/file-preview-screen.component.ts @@ -23,6 +23,7 @@ import { saveAs } from 'file-saver'; import { FileType } from '../model/file-type'; import { DialogService } from '../../../dialogs/dialog.service'; import { MatDialogRef, MatDialogState } from '@angular/material/dialog'; +import { ManualRedactionEntryWrapper } from '../model/manual-redaction-entry.wrapper'; @Component({ selector: 'redaction-file-preview-screen', @@ -94,7 +95,8 @@ export class FilePreviewScreenComponent implements OnInit { this._reloadFiles(); this.appStateService.fileStatusChanged.subscribe((fileStatus) => { if (fileStatus.fileId === this.fileId) { - this._reloadFiles(); + // no more automatic reloads + // this._reloadFiles(); } }); } @@ -185,23 +187,29 @@ export class FilePreviewScreenComponent implements OnInit { this._scrollAnnotationsToPage(pageNumber, 'always'); } - public openManualRedactionDialog($event: ManualRedactionEntry) { + public openManualRedactionDialog($event: ManualRedactionEntryWrapper) { this.ngZone.run(() => { this._dialogRef = this._dialogService.openManualRedactionDialog( $event, - (annotation) => { - // const annotManager = this.activeViewer.annotManager; - // const rectangleAnnot = new this.activeViewer.Annotations.RectangleAnnotation(); - // rectangleAnnot.PageNumber = 1; - // // values are in page coordinates with (0, 0) in the top left - // rectangleAnnot.X = 100; - // rectangleAnnot.Y = 150; - // rectangleAnnot.Width = 200; - // rectangleAnnot.Height = 50; - // rectangleAnnot.Author = annotManager.getCurrentUser(); - // - // annotManager.addAnnotation(rectangleAnnot,true); - // annotManager.redrawAnnotation(rectangleAnnot); + (response: any) => { + const request: ManualRedactionEntry = response.request; + + const annotManager = this.activeViewer.annotManager; + const originalQuads = request.quads; + for (const key of Object.keys(originalQuads)) { + const pageNumber = parseInt(key, 10); + const highlight = new this.activeViewer.Annotations.TextHighlightAnnotation(); + highlight.PageNumber = pageNumber; + highlight.StrokeColor = new this.activeViewer.Annotations.Color( + 255, + 255, + 0 + ); + highlight.Quads = originalQuads[key]; + highlight.Id = this._computeId($event.type, request); + annotManager.addAnnotation(highlight, true); + annotManager.redrawAnnotation(highlight); + } } ); }); @@ -487,4 +495,8 @@ export class FilePreviewScreenComponent implements OnInit { this.applyFilters(); this._changeDetectorRef.detectChanges(); } + + private _computeId(type: 'HINT' | 'REDACTION', request: ManualRedactionEntry) { + return 'request:' + type.toLowerCase() + ':' + new Date().getTime(); + } } diff --git a/apps/red-ui/src/app/screens/file/model/manual-redaction-entry.wrapper.ts b/apps/red-ui/src/app/screens/file/model/manual-redaction-entry.wrapper.ts new file mode 100644 index 000000000..5444940b0 --- /dev/null +++ b/apps/red-ui/src/app/screens/file/model/manual-redaction-entry.wrapper.ts @@ -0,0 +1,10 @@ +import { ManualRedactionEntry } from '@redaction/red-ui-http'; + +export class ManualRedactionEntryWrapper { + public mode: 'REQUEST' | 'ACTUAL'; + + constructor( + public readonly manualRedactionEntry: ManualRedactionEntry, + public readonly type: 'HINT' | 'REDACTION' + ) {} +} diff --git a/apps/red-ui/src/app/screens/file/pdf-viewer/pdf-viewer.component.ts b/apps/red-ui/src/app/screens/file/pdf-viewer/pdf-viewer.component.ts index 76fef7b8b..280ce6496 100644 --- a/apps/red-ui/src/app/screens/file/pdf-viewer/pdf-viewer.component.ts +++ b/apps/red-ui/src/app/screens/file/pdf-viewer/pdf-viewer.component.ts @@ -4,6 +4,7 @@ import { ElementRef, EventEmitter, Input, + NgZone, OnChanges, OnInit, Output, @@ -17,6 +18,7 @@ import { TranslateService } from '@ngx-translate/core'; import { FileDownloadService } from '../service/file-download.service'; import { Subject } from 'rxjs'; import { throttleTime } from 'rxjs/operators'; +import { ManualRedactionEntryWrapper } from '../model/manual-redaction-entry.wrapper'; export interface ViewerState { displayMode?: any; @@ -43,7 +45,7 @@ export class PdfViewerComponent implements OnInit, AfterViewInit, OnChanges { @Output() fileReady = new EventEmitter(); @Output() annotationsAdded = new EventEmitter(); @Output() annotationSelected = new EventEmitter(); - @Output() manualAnnotationRequested = new EventEmitter(); + @Output() manualAnnotationRequested = new EventEmitter(); @Output() pageChanged = new EventEmitter(); @Output() keyUp = new EventEmitter(); @@ -57,17 +59,21 @@ export class PdfViewerComponent implements OnInit, AfterViewInit, OnChanges { constructor( private readonly _translateService: TranslateService, private readonly _fileDownloadService: FileDownloadService, - private readonly _appConfigService: AppConfigService + private readonly _appConfigService: AppConfigService, + private readonly _ngZone: NgZone ) {} ngOnInit() { this._restoreViewerState = this._restoreViewerState.bind(this); // always publish all existing annotations this way everything gets drawn always - this._annotationEventDebouncer - .pipe(throttleTime(300)) - .subscribe((value) => - this.annotationsAdded.emit(this.instance.annotManager.getAnnotationsList()) + this._annotationEventDebouncer.pipe(throttleTime(300)).subscribe((value) => { + this.annotationsAdded.emit(this.instance.annotManager.getAnnotationsList()); + // nasty double-emit fix, the annotationList is not updated when the event is fired + setTimeout( + () => this.annotationsAdded.emit(this.instance.annotManager.getAnnotationsList()), + 200 ); + }); } ngOnChanges(changes: SimpleChanges): void { @@ -95,6 +101,7 @@ export class PdfViewerComponent implements OnInit, AfterViewInit, OnChanges { this._configureTextPopup(); this._configureHeader(); instance.annotManager.on('annotationChanged', (annotations, action) => { + console.log(action, annotations); if (action === 'add') { this._annotationEventDebouncer.next(annotations); } @@ -109,7 +116,7 @@ export class PdfViewerComponent implements OnInit, AfterViewInit, OnChanges { }); instance.docViewer.on('pageComplete', (p) => { - this.pageChanged.emit(p); + this._ngZone.run(() => this.pageChanged.emit(p)); }); instance.docViewer.on('documentLoaded', this._restoreViewerState); @@ -153,6 +160,37 @@ export class PdfViewerComponent implements OnInit, AfterViewInit, OnChanges { } private _configureTextPopup() { + this.instance.textPopup.add({ + type: 'actionButton', + img: '/assets/icons/general/search-viewer.svg', + title: this._translateService.instant('pdf-viewer.text-popup.actions.search.label'), + onClick: () => { + const text = this.instance.docViewer.getSelectedText(); + const searchOptions = { + caseSensitive: true, // match case + wholeWord: true, // match whole words only + wildcard: false, // allow using '*' as a wildcard value + regex: false, // string is treated as a regular expression + searchUp: false, // search from the end of the document upwards + ambientString: true // return ambient string as part of the result + }; + this.instance.openElements(['searchPanel']); + setTimeout(() => { + this.instance.searchTextFull(text, searchOptions); + }, 250); + } + }); + this.instance.textPopup.add({ + type: 'actionButton', + img: '/assets/icons/general/add-hint.svg', + title: this._translateService.instant( + 'pdf-viewer.text-popup.actions.suggestion-hint.label' + ), + onClick: () => { + const mre = this._getManualRedactionEntry(); + this.manualAnnotationRequested.emit(new ManualRedactionEntryWrapper(mre, 'HINT')); + } + }); this.instance.textPopup.add({ type: 'actionButton', img: '/assets/icons/general/add-redaction.svg', @@ -160,20 +198,28 @@ export class PdfViewerComponent implements OnInit, AfterViewInit, OnChanges { 'pdf-viewer.text-popup.actions.suggestion-redaction.label' ), onClick: () => { - const selectedQuads = this.instance.docViewer.getSelectedTextQuads(); - const text = this.instance.docViewer.getSelectedText(); - const entry: ManualRedactionEntry = { positions: [] }; - for (const key of Object.keys(selectedQuads)) { - for (const quad of selectedQuads[key]) { - entry.positions.push(this.toPosition(parseInt(key, 10), quad)); - } - } - entry.value = text; - this.manualAnnotationRequested.emit(entry); + const mre = this._getManualRedactionEntry(); + this.manualAnnotationRequested.emit( + new ManualRedactionEntryWrapper(mre, 'REDACTION') + ); } }); } + private _getManualRedactionEntry(): ManualRedactionEntry { + const selectedQuads = this.instance.docViewer.getSelectedTextQuads(); + const text = this.instance.docViewer.getSelectedText(); + const entry: ManualRedactionEntry = { positions: [] }; + for (const key of Object.keys(selectedQuads)) { + for (const quad of selectedQuads[key]) { + entry.positions.push(this.toPosition(parseInt(key, 10), quad)); + } + } + entry.quads = selectedQuads; + entry.value = text; + return entry; + } + private toPosition(page: number, selectedQuad: any): Rectangle { const pageHeight = this.instance.docViewer.getPageHeight(page); const height = selectedQuad.y2 - selectedQuad.y4; diff --git a/apps/red-ui/src/app/state/app-state.service.ts b/apps/red-ui/src/app/state/app-state.service.ts index 5d6d55b6b..1d7cbe942 100644 --- a/apps/red-ui/src/app/state/app-state.service.ts +++ b/apps/red-ui/src/app/state/app-state.service.ts @@ -103,7 +103,7 @@ export class AppStateService { } getDictionaryLabel(type: string) { - return this._dictionaryData[type]['label']; + return this._dictionaryData[type].label; } get isActiveFileDocumentReviewer() { @@ -362,30 +362,42 @@ export class AppStateService { tap((colors) => { this._dictionaryData['request'] = { hexColor: colors.requestAdd, - type: 'request' + type: 'request', + virtual: true }; this._dictionaryData['ignore'] = { hexColor: colors.notRedacted, - type: 'ignore' + type: 'ignore', + virtual: true }; this._dictionaryData['default'] = { hexColor: colors.defaultColor, - type: 'default' + type: 'default', + virtual: true + }; + this._dictionaryData['add'] = { + hexColor: colors.requestAdd, + type: 'add', + virtual: true }; - this._dictionaryData['add'] = { hexColor: colors.requestAdd, type: 'add' }; this._dictionaryData['remove'] = { hexColor: colors.requestRemove, - type: 'remove' + type: 'remove', + virtual: true }; }) ); const result = await forkJoin([typeObs, colorsObs]).toPromise(); - this._dictionaryData['hint'] = { hexColor: '#283241', type: 'hint' }; - this._dictionaryData['redaction'] = { hexColor: '#283241', type: 'redaction' }; + this._dictionaryData['hint'] = { hexColor: '#283241', type: 'hint', virtual: true }; + this._dictionaryData['redaction'] = { + hexColor: '#283241', + type: 'redaction', + virtual: true + }; for (const key of Object.keys(this._dictionaryData)) { - this._dictionaryData[key]['label'] = humanize(key); + this._dictionaryData[key].label = humanize(key); } } else { return this._dictionaryData; diff --git a/apps/red-ui/src/app/utils/humanize.pipe.ts b/apps/red-ui/src/app/utils/humanize.pipe.ts new file mode 100644 index 000000000..38292071d --- /dev/null +++ b/apps/red-ui/src/app/utils/humanize.pipe.ts @@ -0,0 +1,11 @@ +import { Pipe, PipeTransform } from '@angular/core'; +import { humanize } from './functions'; + +@Pipe({ + name: 'humanize' +}) +export class HumanizePipe implements PipeTransform { + transform(item: string): any { + return humanize(item); + } +} diff --git a/apps/red-ui/src/assets/i18n/en.json b/apps/red-ui/src/assets/i18n/en.json index 9689658ca..b1def7be3 100644 --- a/apps/red-ui/src/assets/i18n/en.json +++ b/apps/red-ui/src/assets/i18n/en.json @@ -35,7 +35,18 @@ }, "dialog": { "header": { - "label": "Add Manual Redaction" + "hint": { + "label": "Add Hint" + }, + "redaction": { + "label": "Add Redaction" + }, + "request-hint": { + "label": "Request Hint" + }, + "request-redaction": { + "label": "Request Redaction" + } }, "add-redaction": { "success": { @@ -47,7 +58,7 @@ }, "actions": { "save": { - "label": "Save Manual Redaction" + "label": "Save" } }, "content": { @@ -92,6 +103,12 @@ "actions": { "suggestion-redaction": { "label": "Suggest Redaction" + }, + "suggestion-hint": { + "label": "Suggest Hint" + }, + "search": { + "label": "Search for selection" } } } diff --git a/apps/red-ui/src/assets/icons/general/add-hint.svg b/apps/red-ui/src/assets/icons/general/add-hint.svg new file mode 100644 index 000000000..4715b80c1 --- /dev/null +++ b/apps/red-ui/src/assets/icons/general/add-hint.svg @@ -0,0 +1,4 @@ + + + diff --git a/apps/red-ui/src/assets/icons/general/search-viewer.svg b/apps/red-ui/src/assets/icons/general/search-viewer.svg new file mode 100644 index 000000000..9fdd792d5 --- /dev/null +++ b/apps/red-ui/src/assets/icons/general/search-viewer.svg @@ -0,0 +1,11 @@ + + + + + + + diff --git a/libs/red-ui-http/src/lib/model/manualRedactionEntry.ts b/libs/red-ui-http/src/lib/model/manualRedactionEntry.ts index c77c8a6f9..9ceebfdca 100644 --- a/libs/red-ui-http/src/lib/model/manualRedactionEntry.ts +++ b/libs/red-ui-http/src/lib/model/manualRedactionEntry.ts @@ -22,6 +22,7 @@ export interface ManualRedactionEntry { type?: string; user?: string; value?: string; + [key: string]: any; } export namespace ManualRedactionEntry { diff --git a/libs/red-ui-http/src/lib/model/typeValue.ts b/libs/red-ui-http/src/lib/model/typeValue.ts index 3375e2f61..425705e78 100644 --- a/libs/red-ui-http/src/lib/model/typeValue.ts +++ b/libs/red-ui-http/src/lib/model/typeValue.ts @@ -30,4 +30,6 @@ export interface TypeValue { * The nonnull entry type. */ type?: string; + + [key: string]: any; }