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,