diff --git a/apps/red-ui/src/app/screens/base-screen/base-screen.component.html b/apps/red-ui/src/app/screens/base-screen/base-screen.component.html index da3ffd598..88da93e88 100644 --- a/apps/red-ui/src/app/screens/base-screen/base-screen.component.html +++ b/apps/red-ui/src/app/screens/base-screen/base-screen.component.html @@ -38,26 +38,28 @@ {{ 'top-bar.navigation-items.back-to-projects' | translate }} - - - - {{ appStateService.activeProject.project.projectName }} - - - - {{ appStateService.activeFile.filename }} - + + + + + {{ appStateService.activeProject.project.projectName }} + + + + {{ appStateService.activeFile.filename }} + +
diff --git a/apps/red-ui/src/app/screens/base-screen/base-screen.component.ts b/apps/red-ui/src/app/screens/base-screen/base-screen.component.ts index b3e164153..9f9e37fd8 100644 --- a/apps/red-ui/src/app/screens/base-screen/base-screen.component.ts +++ b/apps/red-ui/src/app/screens/base-screen/base-screen.component.ts @@ -5,7 +5,7 @@ import { LanguageService } from '../../i18n/language.service'; import { PermissionsService } from '../../common/service/permissions.service'; import { UserPreferenceService } from '../../common/service/user-preference.service'; import { Router } from '@angular/router'; -import { AppConfigKey, AppConfigService } from '../../app-config/app-config.service'; +import { AppConfigService } from '../../app-config/app-config.service'; import { Title } from '@angular/platform-browser'; import { FileDownloadService } from '../../upload-download/file-download.service'; import { StatusOverlayService } from '../../upload-download/status-overlay.service'; diff --git a/apps/red-ui/src/app/screens/file/model/annotation.wrapper.ts b/apps/red-ui/src/app/screens/file/model/annotation.wrapper.ts index bc0a1e50b..674093aca 100644 --- a/apps/red-ui/src/app/screens/file/model/annotation.wrapper.ts +++ b/apps/red-ui/src/app/screens/file/model/annotation.wrapper.ts @@ -180,6 +180,7 @@ export class AnnotationWrapper { annotationWrapper.userId = redactionLogEntry.userId; AnnotationWrapper._createContent(annotationWrapper, redactionLogEntry); AnnotationWrapper._setSuperType(annotationWrapper, redactionLogEntry); + AnnotationWrapper._handleSkippedState(annotationWrapper, redactionLogEntry); AnnotationWrapper._handleRecommendations(annotationWrapper, redactionLogEntry); annotationWrapper.typeLabel = 'annotation-type.' + annotationWrapper.superType; @@ -242,9 +243,18 @@ export class AnnotationWrapper { if (!annotationWrapper.superType) { annotationWrapper.superType = annotationWrapper.redaction ? 'redaction' : annotationWrapper.hint ? 'hint' : 'skipped'; } + } + private static _handleSkippedState(annotationWrapper: AnnotationWrapper, redactionLogEntryWrapper: RedactionLogEntryWrapper) { if (annotationWrapper.superType === 'skipped') { - if (!annotationWrapper.userId && annotationWrapper.content.indexOf('manual override') > 0) { + if (!annotationWrapper.userId) { + if (redactionLogEntryWrapper.manualRedactionType === 'REMOVE' || redactionLogEntryWrapper.manualRedactionType === 'UNDO') { + annotationWrapper.superType = 'pending-analysis'; + return; + } + } + + if (redactionLogEntryWrapper.actionPendingReanalysis) { annotationWrapper.superType = 'pending-analysis'; } } diff --git a/apps/red-ui/src/app/screens/file/model/file-data.model.ts b/apps/red-ui/src/app/screens/file/model/file-data.model.ts index 7413638a5..d008f6ef6 100644 --- a/apps/red-ui/src/app/screens/file/model/file-data.model.ts +++ b/apps/red-ui/src/app/screens/file/model/file-data.model.ts @@ -101,6 +101,25 @@ export class FileDataModel { result.push(redactionLogEntryWrapper); }); + this.manualRedactions.forceRedactions.forEach((forceRedaction) => { + if (forceRedaction.status === 'DECLINED') { + return; + } + + const relevantRedactionLogEntry = result.find((r) => r.id === forceRedaction.id); + // an entry for this request already exists in the redactionLog + if (!!relevantRedactionLogEntry) { + relevantRedactionLogEntry.userId = forceRedaction.user; + relevantRedactionLogEntry.dictionaryEntry = false; + + // if statuses differ + if (!forceRedaction.processedDate || forceRedaction.status !== relevantRedactionLogEntry.status) { + relevantRedactionLogEntry.actionPendingReanalysis = true; + relevantRedactionLogEntry.status = forceRedaction.status; + } + } + }); + this.manualRedactions.entriesToAdd.forEach((manual) => { const markedAsReasonRedactionLogEntry = result.find((r) => r.id === manual.reason); @@ -200,7 +219,7 @@ export class FileDataModel { // REMOVE has been undone - not yet processed if (!foundManualEntry) { redactionLogEntry.manual = false; - redactionLogEntry.manualRedactionType = null; + redactionLogEntry.manualRedactionType = 'UNDO'; redactionLogEntry.status = null; } } diff --git a/apps/red-ui/src/app/screens/file/model/redaction-log-entry.wrapper.ts b/apps/red-ui/src/app/screens/file/model/redaction-log-entry.wrapper.ts index 6ceb8b46a..1afadd4b1 100644 --- a/apps/red-ui/src/app/screens/file/model/redaction-log-entry.wrapper.ts +++ b/apps/red-ui/src/app/screens/file/model/redaction-log-entry.wrapper.ts @@ -7,7 +7,7 @@ export interface RedactionLogEntryWrapper { id?: string; legalBasis?: string; manual?: boolean; - manualRedactionType?: 'ADD' | 'REMOVE'; + manualRedactionType?: 'ADD' | 'REMOVE' | 'UNDO'; matchedRule?: number; positions?: Array; reason?: string; diff --git a/apps/red-ui/src/app/upload-download/file-download.service.ts b/apps/red-ui/src/app/upload-download/file-download.service.ts index 3794da88f..70956f07f 100644 --- a/apps/red-ui/src/app/upload-download/file-download.service.ts +++ b/apps/red-ui/src/app/upload-download/file-download.service.ts @@ -1,7 +1,7 @@ import { ApplicationRef, Injectable } from '@angular/core'; import { DownloadControllerService, FileManagementControllerService } from '@redaction/red-ui-http'; import { interval, Observable } from 'rxjs'; -import { AppConfigService } from '../app-config/app-config.service'; +import { AppConfigKey, AppConfigService } from '../app-config/app-config.service'; import { TranslateService } from '@ngx-translate/core'; import { DialogService } from '../dialogs/dialog.service'; import { ProjectWrapper } from '../state/model/project.wrapper'; @@ -10,7 +10,7 @@ import { mergeMap, tap } from 'rxjs/operators'; import { DownloadStatusWrapper } from './model/download-status.wrapper'; import { AppStateService } from '../state/app-state.service'; import { PermissionsService } from '../common/service/permissions.service'; -import { StreamDownloadService } from '../utils/stream-download.service'; +import { KeycloakService } from 'keycloak-angular'; @Injectable({ providedIn: 'root' @@ -27,9 +27,9 @@ export class FileDownloadService { private readonly _downloadControllerService: DownloadControllerService, private readonly _translateService: TranslateService, private readonly _appConfigService: AppConfigService, + private readonly _keycloakService: KeycloakService, private readonly _fileManagementControllerService: FileManagementControllerService, - private readonly _dialogService: DialogService, - private readonly _streamDownloadService: StreamDownloadService + private readonly _dialogService: DialogService ) { interval(5000).subscribe((val) => { if (_permissionsService.isUser()) { @@ -43,8 +43,8 @@ export class FileDownloadService { .prepareDownload({ fileIds: fileStatusWrappers.map((f) => f.fileId), projectId: project.projectId, - reportTypes: ['SINGLE_FILE_EFSA_TEMPLATE', 'SINGLE_FILE_SYNGENTA_TEMPLATE'], - downloadFileTypes: ['PREVIEW', 'REDACTED', 'FLATTEN'] + reportTypes: ['WORD_SINGLE_FILE_EFSA_TEMPLATE', 'WORD_SINGLE_FILE_SYNGENTA_TEMPLATE', 'EXCEL_MULTI_FILE'], + downloadFileTypes: ['PREVIEW', 'REDACTED'] }) .pipe( mergeMap(() => { @@ -62,7 +62,20 @@ export class FileDownloadService { ); } - public performDownload(status: DownloadStatusWrapper) { - this._streamDownloadService.performDownload(status); + public async performDownload(status: DownloadStatusWrapper) { + const token = await this._keycloakService.getToken(); + const anchor = document.createElement('a'); + anchor.href = + this._appConfigService.getConfig(AppConfigKey.API_URL) + + '/async/download?access_token=' + + encodeURIComponent(token) + + '&storageId=' + + encodeURIComponent(status.storageId); + anchor.download = status.filename; + anchor.target = '_blank'; + + document.body.appendChild(anchor); + anchor.click(); + document.body.removeChild(anchor); } } diff --git a/apps/red-ui/src/app/utils/stream-download.service.ts b/apps/red-ui/src/app/utils/stream-download.service.ts deleted file mode 100644 index a99e36742..000000000 --- a/apps/red-ui/src/app/utils/stream-download.service.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { Injectable } from '@angular/core'; -import streamSaver from 'streamsaver'; -import { TranslateService } from '@ngx-translate/core'; -import { AppConfigKey, AppConfigService } from '../app-config/app-config.service'; -import { DownloadStatusWrapper } from '../upload-download/model/download-status.wrapper'; -import { KeycloakService } from 'keycloak-angular'; -import { NotificationService, NotificationType } from '../notification/notification.service'; - -@Injectable({ - providedIn: 'root' -}) -export class StreamDownloadService { - private _activeDownloadCount = 0; - - constructor( - private readonly _appConfigService: AppConfigService, - private readonly _notificationService: NotificationService, - private readonly _keyCloakService: KeycloakService, - private readonly _translateService: TranslateService - ) { - streamSaver.mitm = '/assets/stream-saver/mitm.html'; - } - - async performDownload(downloadStatusWrapper: DownloadStatusWrapper) { - this._activeDownloadCount += 1; - const token = await this._keyCloakService.getToken(); - fetch(this._appConfigService.getConfig(AppConfigKey.API_URL) + '/async/download/get', { - method: 'POST', - mode: 'cors', - headers: { - 'Content-Type': 'application/json', - Authorization: 'bearer ' + token - }, - body: JSON.stringify({ - storageId: downloadStatusWrapper.storageId - }) - }) - .then((response) => { - // These code section is adapted from an example of the StreamSaver.js - // https://jimmywarting.github.io/StreamSaver.js/examples/fetch.html - - // If the WritableStream is not available (Firefox, Safari), take it from the ponyfill - if (!window.WritableStream) { - streamSaver.WritableStream = WritableStream; - window.WritableStream = WritableStream; - } - - const fileStream = streamSaver.createWriteStream(downloadStatusWrapper.filename); - const readableStream = response.body; - - // More optimized - if (readableStream.pipeTo) { - return readableStream.pipeTo(fileStream); - } - - const writer = fileStream.getWriter(); - this._registerUnload(writer); - - const reader = response.body.getReader(); - const pump = () => reader.read().then((res) => (res.done ? this._finalizeWriter(writer) : writer.write(res.value).then(pump))); - - pump(); - }) - .catch(() => { - this._notificationService.showToastNotification( - this._translateService.instant('stream-download.error', downloadStatusWrapper), - null, - NotificationType.ERROR - ); - this._decrementDownloadCnt(); - }); - } - - private _finalizeWriter(writer) { - writer.close(); - this._decrementDownloadCnt(); - } - - private _decrementDownloadCnt() { - this._activeDownloadCount -= 1; - if (this._activeDownloadCount <= 0) { - window.onunload = () => {}; - window.onbeforeunload = () => {}; - } - } - - private _registerUnload(writer) { - window.onunload = () => { - streamSaver.WritableStream.abort(); - // also possible to call abort on the writer you got from `getWriter()` - writer.abort(); - }; - - window.onbeforeunload = (evt) => { - if (this._activeDownloadCount > 0) { - return this._translateService.instant('stream-download.abort'); - } - }; - } -} diff --git a/apps/red-ui/src/app/utils/sync-width.directive.ts b/apps/red-ui/src/app/utils/sync-width.directive.ts index 66d88b16b..5605f433f 100644 --- a/apps/red-ui/src/app/utils/sync-width.directive.ts +++ b/apps/red-ui/src/app/utils/sync-width.directive.ts @@ -1,30 +1,25 @@ -import { AfterViewChecked, AfterViewInit, Directive, ElementRef, HostListener, Input } from '@angular/core'; +import { AfterViewInit, Directive, ElementRef, HostListener, Input, OnDestroy } from '@angular/core'; import { debounce } from './debounce'; @Directive({ selector: '[redactionSyncWidth]', exportAs: 'redactionSyncWidth' }) -export class SyncWidthDirective implements AfterViewInit { +export class SyncWidthDirective implements AfterViewInit, OnDestroy { @Input() redactionSyncWidth: string; + private _interval: number; constructor(private el: ElementRef) {} - private get _sampleRow(): { tableRow: Element; length: number } { - const tableRows = document.getElementsByClassName(this.redactionSyncWidth); - let length = 0; - let tableRow: Element; + ngAfterViewInit(): void { + this._interval = setInterval(() => { + this.matchWidth(); + }, 1000); + } - for (let idx = 0; idx < tableRows.length; ++idx) { - const row = tableRows.item(idx); - if (row.children.length > length) { - length = row.children.length; - tableRow = row; - } - } - - return { tableRow, length }; + ngOnDestroy(): void { + clearInterval(this._interval); } @debounce(10) @@ -55,7 +50,19 @@ export class SyncWidthDirective implements AfterViewInit { this.matchWidth(); } - ngAfterViewInit(): void { - this.matchWidth(); + private get _sampleRow(): { tableRow: Element; length: number } { + const tableRows = document.getElementsByClassName(this.redactionSyncWidth); + let length = 0; + let tableRow: Element; + + for (let idx = 0; idx < tableRows.length; ++idx) { + const row = tableRows.item(idx); + if (row.children.length > length) { + length = row.children.length; + tableRow = row; + } + } + + return { tableRow, length }; } } diff --git a/libs/red-ui-http/src/lib/api/downloadController.service.ts b/libs/red-ui-http/src/lib/api/downloadController.service.ts index a9b3ebd5e..6926e9026 100644 --- a/libs/red-ui-http/src/lib/api/downloadController.service.ts +++ b/libs/red-ui-http/src/lib/api/downloadController.service.ts @@ -15,8 +15,6 @@ import { HttpClient, HttpEvent, HttpHeaders, HttpParams, HttpResponse } from '@a import { CustomHttpUrlEncodingCodec } from '../encoder'; import { Observable } from 'rxjs'; - -import { DownloadRequest } from '../model/downloadRequest'; import { DownloadResponse } from '../model/downloadResponse'; import { DownloadStatusResponse } from '../model/downloadStatusResponse'; import { PrepareDownloadRequest } from '../model/prepareDownloadRequest'; @@ -57,20 +55,22 @@ export class DownloadControllerService { /** * Returns a downloadable byte stream of the requested file * Use the optional \"inline\" request parameter to select, if this report will be opened in the browser. - * @param body downloadRequest + * @param storageId storageId * @param inline inline * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. * @param reportProgress flag to report request and response progress. */ - public downloadFile(body: DownloadRequest, inline?: boolean, observe?: 'body', reportProgress?: boolean): Observable; - public downloadFile(body: DownloadRequest, inline?: boolean, observe?: 'response', reportProgress?: boolean): Observable>; - public downloadFile(body: DownloadRequest, inline?: boolean, observe?: 'events', reportProgress?: boolean): Observable>; - public downloadFile(body: DownloadRequest, inline?: boolean, observe: any = 'body', reportProgress: boolean = false): Observable { - if (body === null || body === undefined) { - throw new Error('Required parameter body was null or undefined when calling downloadFile.'); + public downloadFile(storageId: string, inline?: boolean, observe?: 'body', reportProgress?: boolean): Observable; + public downloadFile(storageId: string, inline?: boolean, observe?: 'response', reportProgress?: boolean): Observable>; + public downloadFile(storageId: string, inline?: boolean, observe?: 'events', reportProgress?: boolean): Observable>; + public downloadFile(storageId: string, inline?: boolean, observe: any = 'body', reportProgress: boolean = false): Observable { + if (storageId === null || storageId === undefined) { + throw new Error('Required parameter storageId was null or undefined when calling downloadFile.'); } let queryParameters = new HttpParams({ encoder: new CustomHttpUrlEncodingCodec() }); + queryParameters = queryParameters.set('storageId', storageId); + if (inline !== undefined && inline !== null) { queryParameters = queryParameters.set('inline', inline); } @@ -91,15 +91,9 @@ export class DownloadControllerService { } // to determine the Content-Type header - const consumes: string[] = ['application/json']; - const httpContentTypeSelected: string | undefined = this.configuration.selectHeaderContentType(consumes); - if (httpContentTypeSelected !== undefined) { - headers = headers.set('Content-Type', httpContentTypeSelected); - } + const consumes: string[] = []; - return this.httpClient.request('post', `${this.basePath}/async/download/get`, { - responseType: 'blob', - body: body, + return this.httpClient.request('get', `${this.basePath}/async/download`, { params: queryParameters, withCredentials: this.configuration.withCredentials, headers: headers,