This commit is contained in:
Timo 2021-02-24 11:35:27 +02:00
parent 7d2890af26
commit 8acaa88f34
9 changed files with 111 additions and 166 deletions

View File

@ -38,26 +38,28 @@
<mat-icon svgIcon="red:expand"></mat-icon>
{{ 'top-bar.navigation-items.back-to-projects' | translate }}
</a>
<mat-icon class="primary" *ngIf="!appStateService.activeProject && projectsView" svgIcon="red:arrow-down"></mat-icon>
<mat-icon *ngIf="appStateService.activeProject" svgIcon="red:arrow-right"></mat-icon>
<a
*ngIf="appStateService.activeProject"
class="breadcrumb"
[routerLink]="'/ui/projects/' + appStateService.activeProjectId"
routerLinkActive="active"
[routerLinkActiveOptions]="{ exact: true }"
>
{{ appStateService.activeProject.project.projectName }}
</a>
<mat-icon svgIcon="red:arrow-right" *ngIf="appStateService.activeFile"></mat-icon>
<a
*ngIf="appStateService.activeFile"
class="breadcrumb"
[routerLink]="'/ui/projects/' + appStateService.activeProjectId + '/file/' + appStateService.activeFile.fileId"
routerLinkActive="active"
>
{{ appStateService.activeFile.filename }}
</a>
<ng-container *ngIf="projectsView">
<mat-icon class="primary" *ngIf="!appStateService.activeProject" svgIcon="red:arrow-down"></mat-icon>
<mat-icon *ngIf="appStateService.activeProject" svgIcon="red:arrow-right"></mat-icon>
<a
*ngIf="appStateService.activeProject"
class="breadcrumb"
[routerLink]="'/ui/projects/' + appStateService.activeProjectId"
routerLinkActive="active"
[routerLinkActiveOptions]="{ exact: true }"
>
{{ appStateService.activeProject.project.projectName }}
</a>
<mat-icon svgIcon="red:arrow-right" *ngIf="appStateService.activeFile"></mat-icon>
<a
*ngIf="appStateService.activeFile"
class="breadcrumb"
[routerLink]="'/ui/projects/' + appStateService.activeProjectId + '/file/' + appStateService.activeFile.fileId"
routerLinkActive="active"
>
{{ appStateService.activeFile.filename }}
</a>
</ng-container>
</div>
<div class="center flex-1">
<redaction-hidden-action (action)="userPreferenceService.toggleDevFeatures()">

View File

@ -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';

View File

@ -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';
}
}

View File

@ -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;
}
}

View File

@ -7,7 +7,7 @@ export interface RedactionLogEntryWrapper {
id?: string;
legalBasis?: string;
manual?: boolean;
manualRedactionType?: 'ADD' | 'REMOVE';
manualRedactionType?: 'ADD' | 'REMOVE' | 'UNDO';
matchedRule?: number;
positions?: Array<Rectangle>;
reason?: string;

View File

@ -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);
}
}

View File

@ -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');
}
};
}
}

View File

@ -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 };
}
}

View File

@ -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 \&quot;inline\&quot; 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<any>;
public downloadFile(body: DownloadRequest, inline?: boolean, observe?: 'response', reportProgress?: boolean): Observable<HttpResponse<any>>;
public downloadFile(body: DownloadRequest, inline?: boolean, observe?: 'events', reportProgress?: boolean): Observable<HttpEvent<any>>;
public downloadFile(body: DownloadRequest, inline?: boolean, observe: any = 'body', reportProgress: boolean = false): Observable<any> {
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<any>;
public downloadFile(storageId: string, inline?: boolean, observe?: 'response', reportProgress?: boolean): Observable<HttpResponse<any>>;
public downloadFile(storageId: string, inline?: boolean, observe?: 'events', reportProgress?: boolean): Observable<HttpEvent<any>>;
public downloadFile(storageId: string, inline?: boolean, observe: any = 'body', reportProgress: boolean = false): Observable<any> {
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', <any>storageId);
if (inline !== undefined && inline !== null) {
queryParameters = queryParameters.set('inline', <any>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<any>('get', `${this.basePath}/async/download`, {
params: queryParameters,
withCredentials: this.configuration.withCredentials,
headers: headers,