RED-3800 rss editing

This commit is contained in:
Timo Bejan 2022-12-13 13:50:58 +02:00
commit 4555f596bb
38 changed files with 482 additions and 150 deletions

View File

@ -149,9 +149,8 @@ export class ReportsScreenComponent implements OnInit {
}
private async _loadReportTemplates() {
this.availableTemplates$.next(
await firstValueFrom(this._reportTemplateService.getAvailableReportTemplates(this.#dossierTemplateId)),
);
const reportTemplates = await this._reportTemplateService.getAvailableReportTemplates(this.#dossierTemplateId);
this.availableTemplates$.next(reportTemplates);
}
private async _loadPlaceholders() {

View File

@ -214,18 +214,19 @@ export class FileWorkloadComponent extends AutoUnsubscribe implements OnDestroy
return;
}
if ($event.key === 'ArrowLeft') {
if (!$event.metaKey && !$event.ctrlKey && $event.key === 'ArrowLeft') {
this.pagesPanelActive = true;
this._changeDetectorRef.markForCheck();
return;
}
if ($event.key === 'ArrowRight') {
if (!$event.metaKey && !$event.ctrlKey && $event.key === 'ArrowRight') {
this.pagesPanelActive = false;
// if we activated annotationsPanel -
// select first annotation from this page in case there is no
// selected annotation on this page and not in multi select mode
if (!this.pagesPanelActive && !this.multiSelectService.isActive) {
this._documentViewer.clearSelection();
this._selectFirstAnnotationOnCurrentPageIfNecessary();
}
this._changeDetectorRef.markForCheck();

View File

@ -1,57 +1,72 @@
<section class="dialog">
<div translate="rss-dialog.title" class="dialog-header heading-l"></div>
<div class="dialog-header heading-l" translate="rss-dialog.title"></div>
<hr />
<div class="dialog-content">
<div class="table output-data" *ngIf="rssData$ | async as rssEntry">
<div *ngIf="rssData$ | async as rssEntry" class="table output-data">
<div class="table-header">Component</div>
<div class="table-header">Value</div>
<div class="table-header">Transformation</div>
<div class="table-header">Annotations</div>
<div class="table-header">
Annotations
<hr />
<div class="annotation-grid">
<div>Type</div>
<div>Rule</div>
<div>Pages</div>
<div>Reason</div>
</div>
</div>
<ng-container *ngFor="let entry of rssEntry.result | keyvalue: originalOrder">
<div class="bold">{{ entry.key }}</div>
<div>
<div class="value-content">
<iqser-editable-input
(save)="saveEdit($event, entry.value.originalKey)"
[buttonsType]="iconButtonTypes.dark"
[cancelTooltip]="'rss-dialog.actions.cancel-edit-name' | translate"
[class]="'w-200'"
[editTooltip]="'rss-dialog.actions.edit-name' | translate"
[saveTooltip]="'rss-dialog.actions.save-name' | translate"
[value]="entry.value.value ?? entry.value.originalValue"
></iqser-editable-input>
<div class="actions">
<iqser-editable-input
(save)="saveEdit($event, entry)"
[buttonsType]="iconButtonTypes.dark"
[cancelTooltip]="'rss-dialog.actions.cancel-edit' | translate"
[editTooltip]="'rss-dialog.actions.edit' | translate"
[saveTooltip]="'rss-dialog.actions.save' | translate"
[value]="entry.value.value ?? entry.value.originalValue"
>
<ng-container slot="editing">
<iqser-circle-button
(action)="undo()"
icon="red:undo"
[showDot]="false"
(action)="undo(entry)"
*ngIf="entry.value.value"
[showDot]="true"
[tooltip]="'rss-dialog.actions.undo' | translate"
[type]="iconButtonTypes.dark"
class="ml-2"
icon="red:undo"
></iqser-circle-button>
</div>
</div>
</ng-container>
</iqser-editable-input>
</div>
<div>{{ entry.value.transformation }}</div>
<div>{{ entry.value.scmAnnotations | json }}</div>
<div class="annotation-grid">
<ng-container *ngFor="let annotation of entry.value.scmAnnotations">
<div>{{ annotation.type }}</div>
<div>{{ annotation.ruleNumber }}</div>
<div>{{ annotation.pages.join(',') }}</div>
<div>{{ annotation.reason }}</div>
</ng-container>
</div>
</ng-container>
</div>
</div>
<div class="dialog-actions">
<button color="primary" mat-flat-button type="submit" (click)="exportJSON()">
<button (click)="exportJSON()" color="primary" mat-flat-button type="submit">
{{ 'rss-dialog.actions.export-json' | translate }}
</button>
<button color="primary" mat-flat-button type="button" (click)="exportXML()">
<button (click)="exportXML()" color="primary" mat-flat-button type="button">
{{ 'rss-dialog.actions.export-xml' | translate }}
</button>
<button color="primary" mat-flat-button type="button" (click)="exportAllInDossier()" *ngIf="userPreferences.areDevFeaturesEnabled">
<button (click)="exportAllInDossier()" *ngIf="userPreferences.areDevFeaturesEnabled" color="primary" mat-flat-button type="button">
{{ 'Export All' }}
</button>
<div class="all-caps-label cancel" mat-dialog-close translate="rss-dialog.actions.close"></div>
</div>
<iqser-circle-button class="dialog-close" icon="iqser:close" (action)="close()"></iqser-circle-button>
<iqser-circle-button (action)="close()" class="dialog-close" icon="iqser:close"></iqser-circle-button>
</section>

View File

@ -47,3 +47,8 @@
font-weight: 600;
}
}
.annotation-grid {
display: grid;
grid-template-columns: 3fr 1fr 1fr 5fr;
}

View File

@ -1,9 +1,9 @@
import { Component, Inject } from '@angular/core';
import { Component, Inject, OnInit } from '@angular/core';
import { BaseDialogComponent, IconButtonTypes } from '@iqser/common-ui';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { RssService } from '@services/files/rss.service';
import { IFile, IRssEntry } from '@red/domain';
import { firstValueFrom, Observable } from 'rxjs';
import { BehaviorSubject, firstValueFrom } from 'rxjs';
import { map } from 'rxjs/operators';
import { FilesMapService } from '@services/files/files-map.service';
import { UserPreferenceService } from '@users/user-preference.service';
@ -17,12 +17,10 @@ interface RssData {
templateUrl: './rss-dialog.component.html',
styleUrls: ['./rss-dialog.component.scss'],
})
export class RssDialogComponent extends BaseDialogComponent {
export class RssDialogComponent extends BaseDialogComponent implements OnInit {
readonly iconButtonTypes = IconButtonTypes;
rssData$: Observable<IRssEntry>;
originalOrder = (a: KeyValue<string, any>, b: KeyValue<string, any>): number => 0;
rssData$ = new BehaviorSubject<IRssEntry>(null);
constructor(
protected readonly _dialogRef: MatDialogRef<RssDialogComponent>,
@ -32,22 +30,14 @@ export class RssDialogComponent extends BaseDialogComponent {
@Inject(MAT_DIALOG_DATA) readonly data: RssData,
) {
super(_dialogRef);
this.rssData$ = this._rssService.getRSSData(this.data.file.dossierId, this.data.file.fileId).pipe(
map(entry => {
const mapped = {};
for (const key of Object.keys(entry.result)) {
const newKey = key.replace(new RegExp('_', 'g'), ' ');
(<any>entry.result[key]).originalKey = key;
mapped[newKey] = entry.result[key];
}
return {
filaName: entry.filaName,
result: mapped,
};
}),
);
}
async ngOnInit(): Promise<void> {
await this.#loadData();
}
originalOrder = (a: KeyValue<string, any>, b: KeyValue<string, any>): number => 0;
exportJSON() {
this._rssService.exportJSON(this.data.file.dossierId, this.data.file.fileId, this.data.file.filename).subscribe();
}
@ -68,14 +58,39 @@ export class RssDialogComponent extends BaseDialogComponent {
this.exportJSON();
}
undo() {
console.log('Undo');
async undo(entry: KeyValue<string, any>) {
this._loadingService.start();
await firstValueFrom(this._rssService.revertOverride(this.data.file.dossierId, this.data.file.fileId, [entry.value.originalKey]));
await this.#loadData();
}
saveEdit(event: string, originalKey: string) {
console.log(event, originalKey);
/**
* https://qa2.iqser.cloud/redaction-gateway-v1/swagger-ui/index.html#/rss-controller/revertOverrides
*/
async saveEdit(event: string, entry: KeyValue<string, any>) {
this._loadingService.start();
await firstValueFrom(
this._rssService.override(this.data.file.dossierId, this.data.file.fileId, { [entry.value.originalKey]: event }),
);
await this.#loadData();
}
async #loadData(): Promise<void> {
this._loadingService.start();
const rssData = await firstValueFrom(
this._rssService.getRSSData(this.data.file.dossierId, this.data.file.fileId).pipe(
map(entry => {
const mapped = {};
for (const key of Object.keys(entry.result)) {
const newKey = key.replace(new RegExp('_', 'g'), ' ');
(<any>entry.result[key]).originalKey = key;
mapped[newKey] = entry.result[key];
}
return {
filaName: entry.filaName,
result: mapped,
};
}),
),
);
this.rssData$.next(rssData);
this._loadingService.stop();
}
}

View File

@ -12,7 +12,6 @@ import {
ViewChild,
} from '@angular/core';
import { ActivatedRoute, ActivatedRouteSnapshot, NavigationExtras, Router } from '@angular/router';
import { Core } from '@pdftron/webviewer';
import {
AutoUnsubscribe,
bool,
@ -26,7 +25,6 @@ import {
HelpModeService,
List,
LoadingService,
log,
NestedFilter,
OnAttach,
OnDetach,
@ -73,7 +71,6 @@ import { ConfigService } from '@services/config.service';
import { ReadableRedactionsService } from '../pdf-viewer/services/readable-redactions.service';
import { ROLES } from '@users/roles';
import { SuggestionsService } from './services/suggestions.service';
import Annotation = Core.Annotations.Annotation;
const textActions = [TextPopups.ADD_DICTIONARY, TextPopups.ADD_FALSE_POSITIVE];
@ -92,10 +89,7 @@ export class FilePreviewScreenComponent
fullScreen = false;
readonly fileId = this.state.fileId;
readonly dossierId = this.state.dossierId;
readonly file$ = this.state.file$.pipe(
tap(file => this._fileDataService.loadAnnotations(file)),
log('file'),
);
readonly file$ = this.state.file$.pipe(tap(file => this._fileDataService.loadAnnotations(file)));
width: number;
@ViewChild('annotationFilterTemplate', {
read: TemplateRef,
@ -180,6 +174,7 @@ export class FilePreviewScreenComponent
const earmarks$ = isEarmarksViewMode$.pipe(
tap(() => this._loadingService.start()),
switchMap(() => this._fileDataService.loadEarmarks()),
switchMap(() => this._fileDataService.earmarks$),
tap(() => this.updateViewMode().then(() => this._loadingService.stop())),
);
@ -223,7 +218,7 @@ export class FilePreviewScreenComponent
switch (this._viewModeService.viewMode) {
case ViewModes.STANDARD: {
this._setAnnotationsColor(redactions, 'annotationColor');
this._readableRedactionsService.setAnnotationsColor(redactions, 'annotationColor');
const wrappers = await this._fileDataService.annotations;
const ocrAnnotationIds = wrappers.filter(a => a.isOCR).map(a => a.id);
const standardEntries = annotations
@ -232,7 +227,7 @@ export class FilePreviewScreenComponent
const nonStandardEntries = annotations.filter(
a => bool(a.getCustomData('changeLogRemoved')) || this._annotationManager.isHidden(a.Id),
);
this._setAnnotationsOpacity(standardEntries, true);
this._readableRedactionsService.setAnnotationsOpacity(standardEntries, true);
this._annotationManager.show(standardEntries);
this._annotationManager.hide(nonStandardEntries);
break;
@ -240,8 +235,8 @@ export class FilePreviewScreenComponent
case ViewModes.DELTA: {
const changeLogEntries = annotations.filter(a => bool(a.getCustomData('changeLog')));
const nonChangeLogEntries = annotations.filter(a => !bool(a.getCustomData('changeLog')));
this._setAnnotationsColor(redactions, 'annotationColor');
this._setAnnotationsOpacity(changeLogEntries, true);
this._readableRedactionsService.setAnnotationsColor(redactions, 'annotationColor');
this._readableRedactionsService.setAnnotationsOpacity(changeLogEntries, true);
this._annotationManager.show(changeLogEntries);
this._annotationManager.hide(nonChangeLogEntries);
break;
@ -250,14 +245,8 @@ export class FilePreviewScreenComponent
const nonRedactionEntries = annotations.filter(
a => !bool(a.getCustomData('redaction')) || bool(a.getCustomData('changeLogRemoved')),
);
if (this._readableRedactionsService.active) {
this._setAnnotationsOpacity(redactions, true);
this._setAnnotationsColor(redactions, 'annotationColor');
} else {
this._setAnnotationsOpacity(redactions);
this._setAnnotationsColor(redactions, 'redactionColor');
}
this._readableRedactionsService.setPreviewAnnotationsOpacity(redactions);
this._readableRedactionsService.setPreviewAnnotationsColor(redactions);
this._annotationManager.show(redactions);
this._annotationManager.hide(nonRedactionEntries);
this._suggestionsService.hideSuggestionsInPreview(redactions);
@ -762,20 +751,6 @@ export class FilePreviewScreenComponent
}
}
private _setAnnotationsOpacity(annotations: Annotation[], restoreToOriginal = false) {
annotations.forEach(annotation => {
annotation['Opacity'] = restoreToOriginal ? parseFloat(annotation.getCustomData('opacity')) : 1;
});
}
private _setAnnotationsColor(annotations: Annotation[], customData: string) {
annotations.forEach(annotation => {
const color = this._annotationDrawService.convertColor(annotation.getCustomData(customData));
annotation['StrokeColor'] = color;
annotation['FillColor'] = color;
});
}
private _navigateToDossier() {
this._logger.info('Navigating to ', this.state.dossier.dossierName);
return this._router.navigate([this.state.dossier.routerLink]);

View File

@ -181,9 +181,10 @@ export class FileDataService extends EntitiesService<AnnotationWrapper, Annotati
async #buildRemovedRedactions(redactionLog: IRedactionLog, file: File): Promise<void> {
const redactionLogCopy = JSON.parse(JSON.stringify(redactionLog));
redactionLogCopy.redactionLogEntry = redactionLogCopy.redactionLogEntry.reduce((filtered, entry) => {
const isRemoveChange = entry.manualChanges.find(c => this.#isRemoveChange(c.manualRedactionType));
const lastChange = entry.manualChanges.at(-1);
const isRemoveChange = this.#isRemoveChange(lastChange?.manualRedactionType);
if (isRemoveChange) {
entry.manualChanges = entry.manualChanges.filter(c => !this.#isRemoveChange(c.manualRedactionType));
entry.manualChanges.pop();
filtered.push(entry);
}
return filtered;

View File

@ -6,6 +6,7 @@ import Annotation = Core.Annotations.Annotation;
import { REDAnnotationManager } from '../../pdf-viewer/services/annotation-manager.service';
import { UserPreferenceService } from '@users/user-preference.service';
import { AnnotationDrawService } from '../../pdf-viewer/services/annotation-draw.service';
import { ReadableRedactionsService } from '../../pdf-viewer/services/readable-redactions.service';
@Injectable()
export class SuggestionsService {
@ -15,6 +16,7 @@ export class SuggestionsService {
private readonly _annotationManager: REDAnnotationManager,
private readonly _userPreferenceService: UserPreferenceService,
private readonly _annotationDrawService: AnnotationDrawService,
private readonly _readableRedactionsService: ReadableRedactionsService,
) {}
set removedRedactions(removedRedactions: AnnotationWrapper[]) {
@ -39,14 +41,8 @@ export class SuggestionsService {
#convertRemoveSuggestionsToRedactions(suggestions: Annotation[]): void {
const removeSuggestions = suggestions.filter(a => bool(a.getCustomData('suggestionRemove')));
removeSuggestions.forEach(suggestion => {
const color = this._annotationDrawService.convertColor(suggestion.getCustomData('redactionColor'));
suggestion['Opacity'] = 1;
suggestion['StrokeColor'] = color;
suggestion['FillColor'] = color;
});
this._readableRedactionsService.setPreviewAnnotationsOpacity(removeSuggestions);
this._readableRedactionsService.setPreviewAnnotationsColor(removeSuggestions);
this._annotationManager.show(removeSuggestions);
}
}

View File

@ -19,6 +19,7 @@ import Quad = Core.Math.Quad;
const DEFAULT_TEXT_ANNOTATION_OPACITY = 1;
const DEFAULT_REMOVED_ANNOTATION_OPACITY = 0.2;
const FINAL_REDACTION_COLOR = '#000000';
@Injectable()
export class AnnotationDrawService {
@ -164,6 +165,7 @@ export class AnnotationDrawService {
? this._defaultColorsService.getColor(dossierTemplateId, 'requestAddColor')
: this._defaultColorsService.getColor(dossierTemplateId, 'previewColor');
annotation.setCustomData('redactionColor', String(redactionColor));
annotation.setCustomData('finalRedactionColor', FINAL_REDACTION_COLOR);
annotation.setCustomData('annotationColor', String(annotationWrapper.color));
return annotation;

View File

@ -80,6 +80,11 @@ export class REDDocumentViewer {
return merge(this.#documentUnloaded$, this.#documentLoaded$).pipe(shareLast());
}
clearSelection() {
this.#document.clearSelection();
this.#pdf.disable('textPopup');
}
close() {
this.#logger.info('[PDF] Closing document');
this.#document.closeDocument();

View File

@ -7,13 +7,15 @@ import { PdfViewer } from './pdf-viewer.service';
import { REDAnnotationManager } from './annotation-manager.service';
import { AnnotationDrawService } from './annotation-draw.service';
import { BehaviorSubject, Observable } from 'rxjs';
import { Core } from '@pdftron/webviewer';
import Annotation = Core.Annotations.Annotation;
@Injectable()
export class ReadableRedactionsService {
readonly active$: Observable<boolean>;
readonly #enableIcon = this._convertPath('/assets/icons/general/pdftron-action-enable-tooltips.svg');
readonly #disableIcon = this._convertPath('/assets/icons/general/pdftron-action-disable-tooltips.svg');
readonly #active$ = new BehaviorSubject<boolean>(false);
readonly active$: Observable<boolean>;
readonly #active$ = new BehaviorSubject<boolean>(true);
constructor(
@Inject(BASE_HREF_FN) private readonly _convertPath: BaseHrefFn,
@ -47,4 +49,27 @@ export class ReadableRedactionsService {
img: this.toggleReadableRedactionsBtnIcon,
});
}
setAnnotationsOpacity(annotations: Annotation[], restoreToOriginal = false) {
annotations.forEach(annotation => {
annotation['Opacity'] = restoreToOriginal ? parseFloat(annotation.getCustomData('opacity')) : 0.5;
});
}
setAnnotationsColor(annotations: Annotation[], customData: string) {
annotations.forEach(annotation => {
const color = this._annotationDrawService.convertColor(annotation.getCustomData(customData));
annotation['StrokeColor'] = color;
annotation['FillColor'] = color;
});
}
setPreviewAnnotationsOpacity(annotations: Annotation[]) {
this.setAnnotationsOpacity(annotations, !this.active);
}
setPreviewAnnotationsColor(annotations: Annotation[]) {
const color = this.active ? 'redactionColor' : 'finalRedactionColor';
this.setAnnotationsColor(annotations, color);
}
}

View File

@ -249,7 +249,7 @@ export class ViewerHeaderService {
header.getItems().splice(10, header.getItems().length - 14, ...enabledItems);
});
this._pdf.instance.UI.updateElement('selectToolButton', {
this._pdf.instance?.UI.updateElement('selectToolButton', {
img: this._convertPath('/assets/icons/general/pdftron-cursor.svg'),
});
}

View File

@ -106,8 +106,7 @@ export class EditDossierDownloadPackageComponent
existsWatermarks: this.#existsWatermarks$,
});
this.availableReportTypes =
(await firstValueFrom(this._reportTemplateController.getAvailableReportTemplates(dossierTemplateId))) || [];
this.availableReportTypes = (await this._reportTemplateController.getAvailableReportTemplates(dossierTemplateId)) || [];
this.form = this._getForm();
if (!this.canEditDossier) {

View File

@ -200,7 +200,7 @@ export class AddEditEntityComponent extends BaseFormComponent implements OnInit
private _toTechnicalName(value: string) {
const existingTechnicalNames = this._dictionariesMapService.get(this.dossierTemplateId).map(dict => dict.type);
const baseTechnicalName = toSnakeCase(value.trim());
let technicalName = baseTechnicalName;
let technicalName = baseTechnicalName.replaceAll(/[^A-Za-z0-9_-]/g, '');
let suffix = 1;
while (existingTechnicalNames.includes(technicalName)) {
technicalName = [baseTechnicalName, suffix++].join('_');

View File

@ -2,8 +2,14 @@ import { ChangeDetectionStrategy, Component, Input, OnChanges } from '@angular/c
import { PermissionsService } from '@services/permissions.service';
import { Dossier, File } from '@red/domain';
import { FileDownloadService } from '@upload-download/services/file-download.service';
import { CircleButtonType, CircleButtonTypes, Toaster } from '@iqser/common-ui';
import { CircleButtonType, CircleButtonTypes, defaultDialogConfig, Toaster } from '@iqser/common-ui';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { MatDialog } from '@angular/material/dialog';
import {
DownloadDialogComponent,
DownloadDialogData,
DownloadDialogResult,
} from '@shared/dialogs/download-dialog/download-dialog.component';
import { firstValueFrom } from 'rxjs';
@Component({
@ -25,6 +31,7 @@ export class FileDownloadBtnComponent implements OnChanges {
constructor(
private readonly _permissionsService: PermissionsService,
private readonly _fileDownloadService: FileDownloadService,
private readonly _dialog: MatDialog,
private readonly _toaster: Toaster,
) {}
@ -35,9 +42,20 @@ export class FileDownloadBtnComponent implements OnChanges {
async downloadFiles($event: MouseEvent) {
$event.stopPropagation();
const dossierId = this.files[0].dossierId;
const filesIds = this.files.map(f => f.id);
await firstValueFrom(this._fileDownloadService.downloadFiles(filesIds, dossierId));
const ref = this._dialog.open<DownloadDialogComponent, DownloadDialogData, DownloadDialogResult>(DownloadDialogComponent, {
...defaultDialogConfig,
data: { dossier: this.dossier, hasUnapprovedDocuments: this.files.some(file => !file.isApproved) },
});
const result = await firstValueFrom(ref.afterClosed());
if (!result) {
return;
}
await this._fileDownloadService.downloadFiles({
dossierId: this.dossier.id,
fileIds: this.files.map(f => f.id),
...result,
});
this._toaster.info(_('download-status.queued'));
}
}

View File

@ -1,7 +1,8 @@
import { ChangeDetectionStrategy, Component, Input, OnChanges, SimpleChanges, ViewChild } from '@angular/core';
import { Action, ActionTypes, File } from '@red/domain';
import { Action, ActionTypes, Dossier, File } from '@red/domain';
import {
CircleButtonType,
defaultDialogConfig,
IqserTooltipPosition,
OverlappingElements,
ScrollableParentView,
@ -13,6 +14,12 @@ import { FileDownloadService } from '@upload-download/services/file-download.ser
import { PermissionsService } from '@services/permissions.service';
import { firstValueFrom } from 'rxjs';
import { MatMenuTrigger } from '@angular/material/menu';
import {
DownloadDialogComponent,
DownloadDialogData,
DownloadDialogResult,
} from '@shared/dialogs/download-dialog/download-dialog.component';
import { MatDialog } from '@angular/material/dialog';
@Component({
selector: 'redaction-expandable-file-actions',
@ -39,8 +46,13 @@ export class ExpandableFileActionsComponent implements OnChanges {
private readonly _fileDownloadService: FileDownloadService,
private readonly _toaster: Toaster,
private readonly _permissionsService: PermissionsService,
private readonly _dialog: MatDialog,
) {}
get overlappingElement() {
return this.helpModeKey === 'document_features_in_editor' ? OverlappingElements.USER_MENU : undefined;
}
ngOnChanges(changes: SimpleChanges) {
if (changes.actions || changes.maxWidth || changes.minWidth) {
let count = 0;
@ -72,7 +84,7 @@ export class ExpandableFileActionsComponent implements OnChanges {
// Patch download button
const downloadBtn = this.actions.find(btn => btn.type === ActionTypes.downloadBtn);
if (downloadBtn) {
downloadBtn.action = ($event: MouseEvent) => this._downloadFiles($event, downloadBtn.files);
downloadBtn.action = ($event: MouseEvent) => this._downloadFiles($event, downloadBtn.files, downloadBtn.dossier);
downloadBtn.disabled = !this._permissionsService.canDownloadFiles(downloadBtn.files, downloadBtn.dossier);
}
}
@ -83,20 +95,27 @@ export class ExpandableFileActionsComponent implements OnChanges {
}
}
private async _downloadFiles($event: MouseEvent, files: File[]) {
$event.stopPropagation();
const dossierId = files[0].dossierId;
const filesIds = files.map(f => f.id);
await firstValueFrom(this._fileDownloadService.downloadFiles(filesIds, dossierId));
this._toaster.info(_('download-status.queued'));
}
onButtonClick(button: Action, $event: MouseEvent) {
button.action($event);
this.matMenu.closeMenu();
}
get overlappingElement() {
return this.helpModeKey === 'document_features_in_editor' ? OverlappingElements.USER_MENU : undefined;
private async _downloadFiles($event: MouseEvent, files: File[], dossier: Dossier) {
$event.stopPropagation();
const ref = this._dialog.open<DownloadDialogComponent, DownloadDialogData, DownloadDialogResult>(DownloadDialogComponent, {
...defaultDialogConfig,
data: { dossier, hasUnapprovedDocuments: files.some(file => !file.isApproved) },
});
const result = await firstValueFrom(ref.afterClosed());
if (!result) {
return;
}
await this._fileDownloadService.downloadFiles({
dossierId: dossier.id,
fileIds: files.map(f => f.id),
...result,
});
this._toaster.info(_('download-status.queued'));
}
}

View File

@ -25,6 +25,7 @@
flex-direction: column;
overflow-x: hidden;
overflow-y: auto;
flex-wrap: nowrap;
@include common-mixins.scroll-bar();
}
}

View File

@ -102,7 +102,7 @@ export class AddDossierDialogComponent extends BaseDialogComponent implements On
if (dossierTemplate) {
this.availableReportTypes =
(await firstValueFrom(this._reportTemplateController.getAvailableReportTemplates(dossierTemplate.dossierTemplateId))) || [];
(await this._reportTemplateController.getAvailableReportTemplates(dossierTemplate.dossierTemplateId)) || [];
// update dropdown values
this.form.patchValue(
{

View File

@ -0,0 +1,61 @@
<section class="dialog">
<form *ngIf="form" [formGroup]="form">
<div [translate]="'download-dialog.header'" class="dialog-header heading-l"></div>
<div *ngIf="data.hasUnapprovedDocuments" class="inline-dialog-toast toast-warning">
<div [translate]="'download-dialog.unapproved-files-warning'"></div>
</div>
<div class="dialog-content">
<redaction-select
*ngIf="availableReportTypes | async as reportTypes"
[label]="'report-type.label' | translate: { length: reportTypesLength }"
[optionTemplate]="reportTemplateOptionTemplate"
[options]="reportTypes"
[valueMapper]="reportTemplateValueMapper"
class="mb-16"
formControlName="reportTemplateIds"
></redaction-select>
<redaction-select
[label]="'download-type.label' | translate: { length: downloadFileTypesLength }"
[options]="downloadTypes"
formControlName="downloadFileTypes"
></redaction-select>
<div class="iqser-input-group required">
<label translate="download-dialog.form.redaction-preview-color"></label>
<input
[placeholder]="'download-dialog.form.redaction-preview-color-placeholder' | translate"
class="hex-color-input"
formControlName="redactionPreviewColor"
name="color"
type="text"
/>
<div
(colorPickerChange)="form.controls.redactionPreviewColor.setValue($event)"
[colorPicker]="form.controls.redactionPreviewColor.value"
[cpOutputFormat]="'hex'"
[style.background]="form.controls.redactionPreviewColor.value"
class="input-icon"
>
<mat-icon
*ngIf="!form.controls.redactionPreviewColor.value || form.controls.redactionPreviewColor.value?.length === 0"
svgIcon="red:color-picker"
></mat-icon>
</div>
</div>
</div>
<div class="dialog-actions">
<button (click)="save()" [disabled]="form.invalid" color="primary" mat-flat-button type="submit">
{{ 'download-dialog.actions.save' | translate }}
</button>
</div>
<iqser-circle-button (action)="close()" class="dialog-close" icon="iqser:close"></iqser-circle-button>
</form>
</section>
<ng-template #reportTemplateOptionTemplate let-option="option">
{{ option.fileName }} {{ option.multiFileReport ? ('reports-screen.multi-file-report' | translate) : '' }}
</ng-template>

View File

@ -0,0 +1,100 @@
import { Component, Inject } from '@angular/core';
import { Dossier, DownloadFileType, IReportTemplate } from '@red/domain';
import { downloadTypesForDownloadTranslations } from '@translations/download-types-translations';
import { ReportTemplateService } from '@services/report-template.service';
import { AbstractControl, FormBuilder } from '@angular/forms';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { DefaultColorsService } from '@services/entity-services/default-colors.service';
import { List } from '@iqser/common-ui';
export interface DownloadDialogData {
readonly dossier: Dossier;
readonly hasUnapprovedDocuments: boolean;
}
export interface DownloadDialogResult {
downloadFileTypes: List<DownloadFileType>;
reportTemplateIds: List;
redactionPreviewColor: string;
}
@Component({
selector: 'redaction-download-dialog',
templateUrl: './download-dialog.component.html',
styleUrls: ['./download-dialog.component.scss'],
})
export class DownloadDialogComponent {
readonly downloadTypes: { key: DownloadFileType; label: string }[] = ['ORIGINAL', 'PREVIEW', 'DELTA_PREVIEW', 'REDACTED'].map(
(type: DownloadFileType) => ({
key: type,
label: downloadTypesForDownloadTranslations[type],
}),
);
readonly availableReportTypes = this._availableReportTypes;
readonly form = this._getForm();
constructor(
private readonly _defaultColorsService: DefaultColorsService,
private readonly _reportTemplateController: ReportTemplateService,
private readonly _formBuilder: FormBuilder,
private readonly _dialogRef: MatDialogRef<DownloadDialogComponent, DownloadDialogResult>,
@Inject(MAT_DIALOG_DATA) readonly data: DownloadDialogData,
) {}
get reportTypesLength() {
return this.form.controls.reportTemplateIds?.value?.length || 0;
}
get downloadFileTypesLength() {
return this.form.controls.downloadFileTypes?.value?.length || 0;
}
private get _availableReportTypes() {
const dossierTemplateId = this.data.dossier.dossierTemplateId;
const result = this._reportTemplateController.getAvailableReportTemplates(dossierTemplateId);
return result.then(values => values ?? []);
}
reportTemplateValueMapper = (reportTemplate: IReportTemplate) => reportTemplate.templateId;
async save() {
const result: DownloadDialogResult = {
reportTemplateIds: this.form.controls.reportTemplateIds.value,
downloadFileTypes: this.form.controls.downloadFileTypes.value,
redactionPreviewColor: this.form.controls.redactionPreviewColor.value,
};
this._dialogRef.close(result);
}
close() {
this._dialogRef.close();
}
private _hasReportTemplateOrDownloadType(control: AbstractControl) {
const atLeastAReportSelected = control.get('reportTemplateIds')?.value.length > 0;
const atLeastATypeSelected = control.get('downloadFileTypes')?.value.length > 0;
return atLeastATypeSelected || atLeastAReportSelected ? null : { reportTemplateIds: true, downloadFileTypes: true };
}
private _getForm() {
const previewColor = this._defaultColorsService.getColor(this.data.dossier.dossierTemplateId, 'previewColor');
return this._formBuilder.group(
{
reportTemplateIds: [this.data.dossier.reportTemplateIds],
downloadFileTypes: [this.data.dossier.downloadFileTypes],
redactionPreviewColor: [previewColor],
},
{
validators: [control => this._hasReportTemplateOrDownloadType(control), control => this._isHexColor(control)],
},
);
}
private _isHexColor(control: AbstractControl) {
const color = control.get('redactionPreviewColor')?.value;
const isHexColor = /^#[0-9A-F]{6}$/i.test(color);
return isHexColor ? null : { redactionPreviewColor: true };
}
}

View File

@ -42,6 +42,7 @@ import { AddEditEntityComponent } from './components/add-edit-entity/add-edit-en
import { ColorPickerModule } from 'ngx-color-picker';
import { WatermarkSelectorComponent } from './components/dossier-watermark-selector/watermark-selector.component';
import { OcrProgressBarComponent } from './components/ocr-progress-bar/ocr-progress-bar.component';
import { DownloadDialogComponent } from './dialogs/download-dialog/download-dialog.component';
const buttons = [FileDownloadBtnComponent];
@ -76,7 +77,7 @@ const services = [SharedDialogService];
const modules = [MatConfigModule, ScrollingModule, IconsModule, FormsModule, ReactiveFormsModule, ColorPickerModule];
@NgModule({
declarations: [...components, ...utils, EditorComponent],
declarations: [...components, ...utils, EditorComponent, DownloadDialogComponent],
imports: [
CommonModule,
...modules,

View File

@ -29,11 +29,9 @@ export class FileDownloadService extends EntitiesService<IDownloadStatus, Downlo
super();
}
downloadFiles(fileIds: List, dossierId: string): Observable<DownloadStatus[]> {
return this.prepareDownload({
fileIds,
dossierId,
}).pipe(switchMap(() => this.loadAll()));
downloadFiles(request: IPrepareDownloadRequest): Promise<DownloadStatus[]> {
const prepare = this.prepareDownload(request).pipe(switchMap(() => this.loadAll()));
return firstValueFrom(prepare);
}
loadAll(): Observable<DownloadStatus[]> {
@ -61,7 +59,7 @@ export class FileDownloadService extends EntitiesService<IDownloadStatus, Downlo
@Validate()
prepareDownload(@RequiredParam() body: IPrepareDownloadRequest): Observable<IDownloadResponse> {
return this._post(body, `${this._defaultModelPath}/prepare`);
return this._post(body, `${this._defaultModelPath}/prepare-option`);
}
@Validate()

View File

@ -40,6 +40,20 @@ export class RssService extends GenericService<void> {
});
}
@Validate()
override(
@RequiredParam() dossierId: string,
@RequiredParam() fileId: string,
@RequiredParam() componentOverrides: Record<string, string>,
) {
return this._post({ componentOverrides }, `rss/override/${dossierId}/${fileId}`);
}
@Validate()
revertOverride(@RequiredParam() dossierId: string, @RequiredParam() fileId: string, @RequiredParam() components: string[]) {
return this._post({ components }, `rss/override/revert/${dossierId}/${fileId}`);
}
exportJSON(dossierId: string, fileId: string, name: string) {
return this.getRSSData(dossierId, fileId).pipe(
tap(data => {

View File

@ -261,7 +261,7 @@ export class PermissionsService {
if (files.length === 0) {
return false;
}
return this.isApprover(dossier) && files.reduce((prev, file) => prev && file.isApproved, true);
return this.isApprover(dossier) && files.reduce((prev, file) => prev && !file.isInitialProcessing, true);
}
canSoftDeleteDossier(dossier: IDossier): boolean {

View File

@ -1,7 +1,7 @@
import { Injectable } from '@angular/core';
import { GenericService, HeadersConfiguration, RequiredParam, Validate } from '@iqser/common-ui';
import { IPlaceholdersResponse, IReportTemplate } from '@red/domain';
import { Observable, of } from 'rxjs';
import { firstValueFrom, Observable, of } from 'rxjs';
import { HttpResponse } from '@angular/common/http';
import { catchError } from 'rxjs/operators';
@ -34,7 +34,8 @@ export class ReportTemplateService extends GenericService<unknown> {
@Validate()
getAvailableReportTemplates(@RequiredParam() dossierTemplateId: string) {
return this.getAll<IReportTemplate[]>(`${this._defaultModelPath}/${dossierTemplateId}`);
const request = this.getAll<IReportTemplate[]>(`${this._defaultModelPath}/${dossierTemplateId}`);
return firstValueFrom(request);
}
@Validate()

View File

@ -9,3 +9,12 @@ export const downloadTypesTranslations: { [key in DownloadFileType]: string } =
FLATTEN: _('download-type.flatten'),
DELTA_PREVIEW: _('download-type.delta-preview'),
} as const;
export const downloadTypesForDownloadTranslations: { [key in DownloadFileType]: string } = {
ORIGINAL: _('download-type.original'),
PREVIEW: _('download-type.preview'),
REDACTED: _('download-type.redacted-only'),
ANNOTATED: _('download-type.annotated'),
FLATTEN: _('download-type.flatten'),
DELTA_PREVIEW: _('download-type.delta-preview'),
} as const;

View File

@ -13,7 +13,7 @@ export class RedRoleGuard extends IqserRoleGuard {
async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<boolean> {
const currentUser = this._userService.currentUser;
if (!currentUser.hasAnyRole) {
if (!currentUser?.hasAnyRole) {
await this._router.navigate(['/auth-error']);
this._loadingService.stop();
return false;

View File

@ -1017,6 +1017,17 @@
"active": "",
"archive": ""
},
"download-dialog": {
"actions": {
"save": ""
},
"form": {
"redaction-preview-color": "",
"redaction-preview-color-placeholder": ""
},
"header": "",
"unapproved-files-warning": ""
},
"download-includes": "Wählen Sie die Dokumente für Ihr Download-Paket aus",
"download-status": {
"queued": "Ihr Download wurde zur Warteschlange hinzugefügt. Hier finden Sie alle angeforderten Downloads: <a href='/main/downloads'>My Downloads<a/>."
@ -1028,7 +1039,8 @@
"label": "{length} Dokumenten{length, plural, one{version} other{versionen}}",
"original": "Optimiertes PDF",
"preview": "PDF-Vorschau",
"redacted": "geschwärztes PDF"
"redacted": "geschwärztes PDF",
"redacted-only": ""
},
"downloads-list": {
"actions": {
@ -1941,9 +1953,13 @@
},
"rss-dialog": {
"actions": {
"cancel-edit": "",
"close": "",
"edit": "",
"export-json": "",
"export-xml": ""
"export-xml": "",
"save": "",
"undo": ""
},
"title": ""
},

View File

@ -849,7 +849,7 @@
}
},
"download-file": "Download",
"download-file-disabled": "You need to be approver in the dossier and the {count, plural, one{file needs} other{files need}} to be approved in order to download.",
"download-file-disabled": "You need to be approver in the dossier and the {count, plural, one{file needs} other{files need}} to be initially processed in order to download.",
"file-listing": {
"file-entry": {
"file-error": "Re-processing required",
@ -1017,6 +1017,17 @@
"active": "Active",
"archive": "Archived"
},
"download-dialog": {
"actions": {
"save": "Download"
},
"form": {
"redaction-preview-color": "Redaction preview color",
"redaction-preview-color-placeholder": "#000000"
},
"header": "Download options",
"unapproved-files-warning": "This download contains unapproved file(s)"
},
"download-includes": "Choose what is included at download:",
"download-status": {
"queued": "Your download has been queued, you can see all your requested downloads here: <a href='/ui/main/downloads'>My Downloads<a/>."
@ -1028,7 +1039,8 @@
"label": "{length} document {length, plural, one{version} other{versions}}",
"original": "Optimized PDF",
"preview": "Preview PDF",
"redacted": "Redacted PDF"
"redacted": "Redacted PDF",
"redacted-only": "Redacted PDF (approved documents only)"
},
"downloads-list": {
"actions": {
@ -1941,9 +1953,13 @@
},
"rss-dialog": {
"actions": {
"cancel-edit": "Cancel",
"close": "Close",
"edit": "Edit",
"export-json": "Export JSON",
"export-xml": "Export XML"
"export-xml": "Export XML",
"save": "Save",
"undo": "Undo"
},
"title": "Structured Component Management"
},

View File

@ -1017,6 +1017,17 @@
"active": "",
"archive": ""
},
"download-dialog": {
"actions": {
"save": ""
},
"form": {
"redaction-preview-color": "",
"redaction-preview-color-placeholder": ""
},
"header": "",
"unapproved-files-warning": ""
},
"download-includes": "Wählen Sie die Dokumente für Ihr Download-Paket aus",
"download-status": {
"queued": "Ihr Download wurde zur Warteschlange hinzugefügt. Hier finden Sie alle angeforderten Downloads: <a href='/main/downloads'>My Downloads<a/>."
@ -1028,7 +1039,8 @@
"label": "{length} Dokumenten{length, plural, one{version} other{versionen}}",
"original": "Optimiertes PDF",
"preview": "PDF-Vorschau",
"redacted": "geschwärztes PDF"
"redacted": "geschwärztes PDF",
"redacted-only": ""
},
"downloads-list": {
"actions": {
@ -1941,9 +1953,13 @@
},
"rss-dialog": {
"actions": {
"cancel-edit": "",
"close": "",
"edit": "",
"export-json": "",
"export-xml": ""
"export-xml": "",
"save": "",
"undo": ""
},
"title": ""
},

View File

@ -849,7 +849,7 @@
}
},
"download-file": "Download",
"download-file-disabled": "You need to be approver in the dossier and the {count, plural, one{file needs} other{files need}} to be approved in order to download.",
"download-file-disabled": "You need to be approver in the dossier and the {count, plural, one{file needs} other{files need}} to be initially processed in order to download.",
"file-listing": {
"file-entry": {
"file-error": "Re-processing required",
@ -1017,6 +1017,17 @@
"active": "Active",
"archive": "Archived"
},
"download-dialog": {
"actions": {
"save": "Download"
},
"form": {
"redaction-preview-color": "Redaction preview color",
"redaction-preview-color-placeholder": "#000000"
},
"header": "Download options",
"unapproved-files-warning": "This download contains unapproved file(s)"
},
"download-includes": "Choose what is included at download:",
"download-status": {
"queued": "Your download has been queued, you can see all your requested downloads here: <a href='/ui/main/downloads'>My Downloads<a/>."
@ -1028,7 +1039,8 @@
"label": "{length} document {length, plural, one{version} other{versions}}",
"original": "Optimized PDF",
"preview": "Preview PDF",
"redacted": "Redacted PDF"
"redacted": "Redacted PDF",
"redacted-only": "Redacted PDF (redacted documents only)"
},
"downloads-list": {
"actions": {
@ -1941,9 +1953,13 @@
},
"rss-dialog": {
"actions": {
"cancel-edit": "Cancel",
"close": "Close",
"edit": "Edit",
"export-json": "Export JSON",
"export-xml": "Export XML"
"export-xml": "Export XML",
"save": "Save",
"undo": "Undo"
},
"title": "Structured Component Management"
},

@ -1 +1 @@
Subproject commit 5f9c754abf0fb4fcc0606d811a66c5bbb88d164a
Subproject commit b58f1ca2fed8e8ddf2834c0fd15e48d00b6a1329

View File

@ -2,8 +2,12 @@
* Object containing information on which file and report types should be included in the download.
*/
import { List } from '@iqser/common-ui';
import { DownloadFileType } from '../shared';
export interface IPrepareDownloadRequest {
readonly dossierId?: string;
readonly fileIds?: List;
readonly dossierId: string;
readonly fileIds: List;
readonly reportTemplateIds: List;
readonly downloadFileTypes: List<DownloadFileType>;
readonly redactionPreviewColor: string;
}

View File

@ -146,7 +146,11 @@ export class File extends Entity<IFile> implements IFile {
this.isUnderApproval = this.workflowStatus === WorkflowFileStatuses.UNDER_APPROVAL;
this.canBeApproved = !this.hasSuggestions && !this.isProcessing && !this.isError;
this.canBeOpened = !this.isError && !this.isUnprocessed && this.numberOfAnalyses > 0;
this.canBeOCRed = !this.excluded && !this.lastOCRTime && (this.isNew || this.isUnderReview || this.isUnderApproval);
this.canBeOCRed =
!this.excluded &&
!this.lastOCRTime &&
this.numberOfAnalyses !== 0 &&
(this.isNew || this.isUnderReview || this.isUnderApproval);
this.fileAttributes =
file.fileAttributes && file.fileAttributes.attributeIdToValue ? file.fileAttributes : { attributeIdToValue: {} };

2
package-lock.json generated
View File

@ -1,6 +1,6 @@
{
"name": "redaction",
"version": "3.813.0",
"version": "3.828.0",
"lockfileVersion": 2,
"requires": true,
"packages": {

View File

@ -1,6 +1,6 @@
{
"name": "redaction",
"version": "3.813.0",
"version": "3.828.0",
"private": true,
"license": "MIT",
"scripts": {

Binary file not shown.