diff --git a/apps/red-ui/src/app/components/annotation-icon/annotation-icon.component.scss b/apps/red-ui/src/app/components/annotation-icon/annotation-icon.component.scss index c3269744d..21166dce4 100644 --- a/apps/red-ui/src/app/components/annotation-icon/annotation-icon.component.scss +++ b/apps/red-ui/src/app/components/annotation-icon/annotation-icon.component.scss @@ -47,6 +47,10 @@ background-color: $grey-1; } +.request{ + background-color: $blue-1; +} + .ignore { background-color: $grey-5; } diff --git a/apps/red-ui/src/app/components/annotation-icon/annotation-icon.component.ts b/apps/red-ui/src/app/components/annotation-icon/annotation-icon.component.ts index fbc913620..0c50865e6 100644 --- a/apps/red-ui/src/app/components/annotation-icon/annotation-icon.component.ts +++ b/apps/red-ui/src/app/components/annotation-icon/annotation-icon.component.ts @@ -6,7 +6,7 @@ import { Component, Input, OnInit } from '@angular/core'; styleUrls: ['./annotation-icon.component.scss'] }) export class AnnotationIconComponent implements OnInit { - @Input() public type: 'hint' | 'redaction' | 'suggestion' | 'ignore' | 'comment'; + @Input() public type: 'hint' | 'redaction' | 'suggestion' | 'ignore' | 'comment' | 'request'; constructor() { } diff --git a/apps/red-ui/src/app/icons/icons.module.ts b/apps/red-ui/src/app/icons/icons.module.ts index b9f2651fe..9e14e6299 100644 --- a/apps/red-ui/src/app/icons/icons.module.ts +++ b/apps/red-ui/src/app/icons/icons.module.ts @@ -1,7 +1,7 @@ -import { NgModule } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { MatIconModule, MatIconRegistry } from '@angular/material/icon'; -import { DomSanitizer } from '@angular/platform-browser'; +import {NgModule} from '@angular/core'; +import {CommonModule} from '@angular/common'; +import {MatIconModule, MatIconRegistry} from '@angular/material/icon'; +import {DomSanitizer} from '@angular/platform-browser'; @NgModule({ imports: [CommonModule, MatIconModule], @@ -18,7 +18,7 @@ export class IconsModule { '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' + 'status', 'trash', 'user', 'check-alt' ]; for (const icon of icons) { diff --git a/apps/red-ui/src/app/screens/file/file-preview-screen/file-preview-screen.component.html b/apps/red-ui/src/app/screens/file/file-preview-screen/file-preview-screen.component.html index 92f683077..5359e029d 100644 --- a/apps/red-ui/src/app/screens/file/file-preview-screen/file-preview-screen.component.html +++ b/apps/red-ui/src/app/screens/file/file-preview-screen/file-preview-screen.component.html @@ -128,7 +128,10 @@ {{annotation.getPageNumber()}} -
+
+ diff --git a/apps/red-ui/src/app/screens/file/file-preview-screen/file-preview-screen.component.scss b/apps/red-ui/src/app/screens/file/file-preview-screen/file-preview-screen.component.scss index 7b53dc8a5..f778e58d1 100644 --- a/apps/red-ui/src/app/screens/file/file-preview-screen/file-preview-screen.component.scss +++ b/apps/red-ui/src/app/screens/file/file-preview-screen/file-preview-screen.component.scss @@ -101,7 +101,16 @@ redaction-pdf-viewer { background-color: #F9FAFB; .annotation-actions { + background: linear-gradient(to right, transparent 0%, #F9FAFB, #F9FAFB, #F9FAFB); display: flex; + align-items: center; + justify-content: flex-end; + width: 120px; + padding-right: 16px; + + .confirm{ + color: $green-2; + } } } 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 b5c4014b0..7093bc6ac 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 @@ -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,25 +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 { 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'; +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', @@ -48,7 +48,7 @@ export class FilePreviewScreenComponent implements OnInit { public displayedAnnotations: { [key: number]: { annotations: Annotations.Annotation[] } } = {}; public selectedAnnotation: Annotations.Annotation; public filters: AnnotationFilters; - public expandedFilters: AnnotationFilters = { hint: false }; + public expandedFilters: AnnotationFilters = {hint: false}; public activeViewerPage: number; constructor( @@ -228,26 +228,26 @@ export class FilePreviewScreenComponent implements OnInit { return AnnotationUtils.getDictionary(annotation); } - // async getText(pageNumber: number, rect) { - // const viewerObject = this._viewerSyncService.activeViewerObject; - // - // const { PDFNet } = viewerObject; - // await PDFNet.initialize(); - // const pdfDoc = await viewerObject.docViewer.getDocument().getPDFDoc(); - // const txt = await PDFNet.TextExtractor.create(); - // await txt.begin(await pdfDoc.getPage(pageNumber), rect); - // - // // Extract words one by one. - // let text = ''; - // let line = await txt.getFirstLine(); - // for (; (await line.isValid()); line = (await line.getNextLine())) { - // for (let word = await line.getFirstWord(); (await word.isValid()); word = (await word.getNextWord())) { - // await word.getString(); - // text += word; - // } - // } - // console.log(text); - // } + acceptSuggestionAnnotation($event: MouseEvent, annotation: Annotations.Annotation) { + $event.stopPropagation(); + const dialogRef = this._dialog.open(ConfirmationDialogComponent, { + width: '400px', + maxWidth: '90vw' + }); + + const parts = annotation.Id.split(':'); + const annotationId = parts[parts.length - 1]; + + dialogRef.afterClosed().subscribe(result => { + if (result) { + this._manualRedactionControllerService.approveRequest(this.appStateService.activeProjectId, this.appStateService.activeFile.fileId, annotationId).subscribe(ok => { + this._notificationService.showToastNotification(this._translateService.instant('manual-redaction.confirm-annotation.success.label'), null, NotificationType.SUCCESS); + }, (err) => { + this._notificationService.showToastNotification(this._translateService.instant('manual-redaction.confirm-annotation.failed.label', err), null, NotificationType.ERROR); + }); + } + }); + } suggestRemoveAnnotation($event: MouseEvent, annotation: Annotations.Annotation) { $event.stopPropagation(); @@ -256,10 +256,8 @@ export class FilePreviewScreenComponent implements OnInit { maxWidth: '90vw' }); - console.log(annotation); - const parts = annotation.Id.split(':'); - const annotationId = parts[parts.length-1]; + const annotationId = parts[parts.length - 1]; dialogRef.afterClosed().subscribe(result => { if (result) { @@ -315,4 +313,9 @@ export class FilePreviewScreenComponent implements OnInit { $event.stopPropagation(); this.expandedFilters[key] = value; } + + isManuallyAddedAnnotation(annotation: Annotations.Annotation) { + return annotation.Id.indexOf('request:') >= 0; + } + } diff --git a/apps/red-ui/src/app/screens/file/manual-redaction-dialog/manual-redaction-dialog.component.html b/apps/red-ui/src/app/screens/file/manual-redaction-dialog/manual-redaction-dialog.component.html index b208929cf..0be3a13ba 100644 --- a/apps/red-ui/src/app/screens/file/manual-redaction-dialog/manual-redaction-dialog.component.html +++ b/apps/red-ui/src/app/screens/file/manual-redaction-dialog/manual-redaction-dialog.component.html @@ -1,5 +1,5 @@
-
+
@@ -15,13 +15,15 @@
- +
+
- {{'manual-redaction.dialog.content.dictionary.add.label' | translate}} + +
- + + {{'manual-redaction.dialog.content.dictionary.label' | translate}} @@ -30,6 +32,12 @@ +
+ {{'manual-redaction.dialog.content.dictionary.add.label' | translate}} +
+ +
diff --git a/apps/red-ui/src/app/screens/file/manual-redaction-dialog/manual-redaction-dialog.component.ts b/apps/red-ui/src/app/screens/file/manual-redaction-dialog/manual-redaction-dialog.component.ts index bcba85da9..401a8b701 100644 --- a/apps/red-ui/src/app/screens/file/manual-redaction-dialog/manual-redaction-dialog.component.ts +++ b/apps/red-ui/src/app/screens/file/manual-redaction-dialog/manual-redaction-dialog.component.ts @@ -6,13 +6,13 @@ import { AddRedactionRequest, 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"; +import {UserService} from "../../../user/user.service"; @Component({ @@ -24,9 +24,11 @@ export class ManualRedactionDialogComponent implements OnInit { redactionForm: FormGroup; dictionaries: Observable>; + isDocumentAdmin: boolean; constructor( private readonly _appStateService: AppStateService, + private readonly _userService: UserService, private readonly _formBuilder: FormBuilder, private readonly _notificationService: NotificationService, private readonly _translateService: TranslateService, @@ -37,38 +39,56 @@ export class ManualRedactionDialogComponent implements OnInit { } async ngOnInit() { + this.isDocumentAdmin = (this._appStateService.isActiveProjectOwner || this._appStateService.isActiveFileDocumentReviewer) && this._userService.user.isManager; + const commentField = this.isDocumentAdmin ? [null] : [null, Validators.required]; this.redactionForm = this._formBuilder.group({ addToDictionary: [false], reason: [null, Validators.required], - dictionary: null, + dictionary: [null, Validators.required], + comment: commentField, }); this.dictionaries = this._dictionaryControllerService.getAllTypes().pipe(map(r => r.types)); } - saveManualRedaction() { + handleAddRedaction() { + if (this.isDocumentAdmin) { + this.addManualRedaction(); + } else { + this.suggestManualRedaction() + } + } + + suggestManualRedaction() { const mre = Object.assign({}, this.addRedactionRequest); this._enhanceManualRedaction(mre); - mre.comment = { - text: 'Lorem ipsum' - }; this._manualRedactionControllerService.requestAddRedaction(mre, this._appStateService.activeProject.project.projectId, this._appStateService.activeFile.fileId).subscribe(ok => { + this._appStateService.reanalyseActiveFile(); 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.ERROR); }); + } + addManualRedaction() { + const mre = Object.assign({}, this.addRedactionRequest); + this._enhanceManualRedaction(mre); + this._manualRedactionControllerService.addRedaction(mre, this._appStateService.activeProject.project.projectId, this._appStateService.activeFile.fileId).subscribe(ok => { + this._appStateService.reanalyseActiveFile(); + 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.ERROR); + }); } private _enhanceManualRedaction(addRedactionRequest: AddRedactionRequest) { addRedactionRequest.type = this.redactionForm.get('dictionary').value; addRedactionRequest.addToDictionary = this.redactionForm.get('addToDictionary').value; addRedactionRequest.reason = this.redactionForm.get('reason').value; - } - - get showDictionary() { - return !this.redactionForm.get('addToDictionary').value + const commentValue = this.redactionForm.get('comment').value; + addRedactionRequest.comment = commentValue ? {text: commentValue} : null; } } 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 3316acf47..4d8476564 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 @@ -17,6 +17,7 @@ import {ViewerSyncService} from '../service/viewer-sync.service'; import {MatDialog} from "@angular/material/dialog"; import {FileDownloadService} from "../service/file-download.service"; import {FileType} from "../model/file-type"; +import {AppStateService} from "../../../state/app-state.service"; @Component({ @@ -41,6 +42,7 @@ export class PdfViewerComponent implements OnInit, AfterViewInit, OnDestroy { _fileData: Blob; constructor( + private readonly _appStateService: AppStateService, private readonly _viewerSyncService: ViewerSyncService, private readonly _translateService: TranslateService, private readonly _fileDownloadService: FileDownloadService, @@ -65,7 +67,6 @@ export class PdfViewerComponent implements OnInit, AfterViewInit, OnDestroy { }) } - private _loadViewer(pdfBlob: any) { const license = this._appConfigService.getConfig(AppConfigKey.PDFTRON_LICENSE); WebViewer({ @@ -97,7 +98,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 : 'document.pdf'}); }); } @@ -122,7 +123,7 @@ export class PdfViewerComponent implements OnInit, AfterViewInit, OnDestroy { private _configureTextPopup() { this.wvInstance.textPopup.add({ type: 'actionButton', - img: '/assets/icons/general/add.svg', + img: '/assets/icons/general/add-redaction.svg', title: this._translateService.instant('pdf-viewer.text-popup.actions.suggestion-redaction.label'), onClick: () => { const selectedQuads = this.wvInstance.docViewer.getSelectedTextQuads(); 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 8d7e78a02..a3554f127 100644 --- a/apps/red-ui/src/app/state/app-state.service.ts +++ b/apps/red-ui/src/app/state/app-state.service.ts @@ -1,8 +1,15 @@ import {Injectable} from "@angular/core"; -import {FileStatus, Project, ProjectControllerService, StatusControllerService} from "@redaction/red-ui-http"; +import { + FileStatus, + Project, + ProjectControllerService, + ReanalysisControllerService, + StatusControllerService +} from "@redaction/red-ui-http"; import {NotificationService, NotificationType} from "../notification/notification.service"; import {TranslateService} from "@ngx-translate/core"; import {Router} from "@angular/router"; +import {UserService} from "../user/user.service"; export interface AppState { @@ -35,8 +42,10 @@ export class AppStateService { constructor( private readonly _router: Router, + private readonly _userService: UserService, private readonly _projectControllerService: ProjectControllerService, private readonly _notificationService: NotificationService, + private readonly _reanalysisControllerService: ReanalysisControllerService, private readonly _translateService: TranslateService, private readonly _statusControllerService: StatusControllerService) { this._appState = { @@ -46,6 +55,20 @@ export class AppStateService { } } + + get isActiveProjectOwner() { + return this._appState.activeProject.project.ownerId == this._userService.userId + } + + get isActiveProjectMember() { + return this._appState.activeProject.project.memberIds.indexOf(this._userService.userId) >= 0; + } + + get isActiveFileDocumentReviewer() { + return false; + } + + get aggregatedFiles(): FileStatus[] { const result: FileStatus[] = []; this._appState.projects.forEach(p => { @@ -187,4 +210,9 @@ export class AppStateService { await this.loadAllProjects(); } } + + async reanalyseActiveFile() { + await this._reanalysisControllerService.reanalyzeFile(this._appState.activeProject.project.projectId, this._appState.activeFile.fileId).toPromise(); + await this.reloadActiveProjectFiles(); + } } diff --git a/apps/red-ui/src/app/user/user.service.ts b/apps/red-ui/src/app/user/user.service.ts index 94f0de68a..d33d6626f 100644 --- a/apps/red-ui/src/app/user/user.service.ts +++ b/apps/red-ui/src/app/user/user.service.ts @@ -1,8 +1,8 @@ -import { Injectable } from '@angular/core'; -import { KeycloakService } from 'keycloak-angular'; -import { KeycloakProfile } from 'keycloak-js'; +import {Injectable} from '@angular/core'; +import {KeycloakService} from 'keycloak-angular'; +import {KeycloakProfile} from 'keycloak-js'; import jwt_decode from 'jwt-decode'; -import { User, UserControllerService } from '@redaction/red-ui-http'; +import {User, UserControllerService} from '@redaction/red-ui-http'; export class UserWrapper { @@ -48,6 +48,9 @@ export class UserService { this._keycloakService.logout(window.location.origin); } + get userId() { + return this._currentUser.id; + } get allUsers() { return this._allUsers; diff --git a/apps/red-ui/src/assets/i18n/en.json b/apps/red-ui/src/assets/i18n/en.json index c853030be..eede097e5 100644 --- a/apps/red-ui/src/assets/i18n/en.json +++ b/apps/red-ui/src/assets/i18n/en.json @@ -17,6 +17,14 @@ } }, "manual-redaction": { + "confirm-annotation": { + "success": { + "label": "Annotation Confirmed!" + }, + "failed": { + "label": "Error confirming Annotation removal!" + } + }, "remove-annotation": { "success": { "label": "Annotation Suggested for removal!" @@ -50,10 +58,13 @@ "add": { "label": "Add to dictionary" }, - "label": "Dictionary" + "label": "Type" }, "reason": { "label": "Reason" + }, + "comment": { + "label": "Comment" } } } diff --git a/apps/red-ui/src/assets/icons/general/add-redaction.svg b/apps/red-ui/src/assets/icons/general/add-redaction.svg new file mode 100644 index 000000000..31af7cd97 --- /dev/null +++ b/apps/red-ui/src/assets/icons/general/add-redaction.svg @@ -0,0 +1,13 @@ + + + + + + + + + + diff --git a/apps/red-ui/src/assets/icons/general/add.svg b/apps/red-ui/src/assets/icons/general/add.svg deleted file mode 100644 index b38938947..000000000 --- a/apps/red-ui/src/assets/icons/general/add.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/apps/red-ui/src/assets/icons/general/check-alt.svg b/apps/red-ui/src/assets/icons/general/check-alt.svg new file mode 100644 index 000000000..eff76ca47 --- /dev/null +++ b/apps/red-ui/src/assets/icons/general/check-alt.svg @@ -0,0 +1,10 @@ + + + + + + + diff --git a/apps/red-ui/src/assets/icons/general/document.svg b/apps/red-ui/src/assets/icons/general/document.svg index 946686b4f..09cf6cdd9 100644 --- a/apps/red-ui/src/assets/icons/general/document.svg +++ b/apps/red-ui/src/assets/icons/general/document.svg @@ -1,7 +1,7 @@ document - - + + - \ No newline at end of file +