Merge branch 'VM/RED-8748' into 'master'

RED-8748 - Integrated component view in DocuMine

Closes RED-8748

See merge request redactmanager/red-ui!449
This commit is contained in:
Dan Percic 2024-06-11 20:03:54 +02:00
commit 463fddefd3
54 changed files with 1458 additions and 767 deletions

View File

@ -48,7 +48,6 @@ export class AddEditComponentMappingDialogComponent
{
protected readonly encodingTypeOptions = Object.keys(FileAttributeEncodingTypes);
protected readonly translations = fileAttributeEncodingTypesTranslations;
protected readonly iconButtonTypes = IconButtonTypes;
activeFile: File;
form!: UntypedFormGroup;

View File

@ -6,7 +6,7 @@
class="mt-6 mr-10"
></redaction-annotation-icon>
<div class="flex-1">
<div [class.flex-1]="!isDocumine">
<div>
<strong>{{ annotation.superTypeLabel | translate }}</strong>
&nbsp;
@ -15,7 +15,7 @@
</strong>
</div>
<div *ngIf="annotation.typeLabel">
<div *ngIf="annotation.typeLabel" [class.type-label]="isDocumine">
<strong>
<span>{{ annotation.descriptor | translate }}</span
>:

View File

@ -3,6 +3,13 @@
position: relative;
font-size: 11px;
line-height: 14px;
.type-label {
width: 130px;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
}
.active-icon-marker-container {

View File

@ -3,8 +3,7 @@ import { AnnotationWrapper } from '@models/file/annotation.wrapper';
import { MultiSelectService } from '../../services/multi-select.service';
import { annotationTypesTranslations } from '@translations/annotation-types-translations';
import { Roles } from '@users/roles';
import { ImageCategory } from '../../utils/constants';
import { ManualRedactionTypes } from '@red/domain';
import { getConfig } from '@iqser/common-ui';
@Component({
selector: 'redaction-annotation-card',
@ -12,8 +11,9 @@ import { ManualRedactionTypes } from '@red/domain';
styleUrls: ['./annotation-card.component.scss'],
})
export class AnnotationCardComponent {
readonly roles = Roles;
readonly annotationTypesTranslations = annotationTypesTranslations;
protected readonly roles = Roles;
protected readonly annotationTypesTranslations = annotationTypesTranslations;
protected readonly isDocumine = getConfig().IS_DOCUMINE;
@Input() annotation: AnnotationWrapper;
@Input() isSelected = false;

View File

@ -8,6 +8,7 @@
(click)="annotationClicked(annotation.item, $event)"
[annotation]="annotation"
[id]="'annotation-' + annotation.item.id"
[class.documine-wrapper]="isDocumine"
></redaction-annotation-wrapper>
</ng-container>

View File

@ -10,7 +10,8 @@
@include common-mixins.scroll-bar;
}
&.has-scrollbar:hover redaction-annotation-wrapper::ng-deep {
&.has-scrollbar:hover redaction-annotation-wrapper::ng-deep,
&::ng-deep.documine-wrapper {
.annotation {
padding-right: 5px;
}

View File

@ -1,5 +1,5 @@
import { ChangeDetectorRef, Component, computed, ElementRef, EventEmitter, Input, Output } from '@angular/core';
import { HasScrollbarDirective } from '@iqser/common-ui';
import { getConfig, HasScrollbarDirective } from '@iqser/common-ui';
import { FilterService } from '@iqser/common-ui/lib/filtering';
import { IqserEventTarget } from '@iqser/common-ui/lib/utils';
import { AnnotationWrapper } from '@models/file/annotation.wrapper';
@ -26,6 +26,7 @@ export class AnnotationsListComponent extends HasScrollbarDirective {
}
return [] as EarmarkGroup[];
});
protected readonly isDocumine = getConfig().IS_DOCUMINE;
constructor(
protected readonly _elementRef: ElementRef,

View File

@ -0,0 +1,17 @@
<button [class.active]="true" [matTooltipPosition]="'above'" [matTooltip]="'documine-export.document-tooltip' | translate" class="red-tab">
{{ 'documine-export.document' | translate }}
</button>
<button
[matMenuTriggerFor]="bulkComponentDownloadMenu"
[matTooltipPosition]="'above'"
[matTooltip]="'documine-export.export-tooltip' | translate"
class="red-tab"
>
{{ 'documine-export.export' | translate }}
</button>
<mat-menu #bulkComponentDownloadMenu="matMenu">
<button (click)="downloadComponentAsJSON()" [innerHTML]="'component-download.json' | translate" mat-menu-item></button>
<button (click)="downloadComponentAsXML()" [innerHTML]="'component-download.xml' | translate" mat-menu-item></button>
</mat-menu>

View File

@ -0,0 +1,22 @@
import { Component, Input } from '@angular/core';
import { firstValueFrom } from 'rxjs';
import { Dossier } from '@red/domain';
import { ComponentLogService } from '@services/files/component-log.service';
@Component({
selector: 'redaction-documine-export',
templateUrl: './documine-export.component.html',
})
export class DocumineExportComponent {
@Input() dossier: Dossier;
constructor(private readonly _componentLogService: ComponentLogService) {}
downloadComponentAsJSON() {
return firstValueFrom(this._componentLogService.exportJSON(this.dossier.dossierTemplateId, this.dossier.dossierId));
}
async downloadComponentAsXML() {
return firstValueFrom(this._componentLogService.exportXML(this.dossier.dossierTemplateId, this.dossier.dossierId));
}
}

View File

@ -0,0 +1,64 @@
<div class="component-value" [ngClass]="{ selected: selected, editing: editing }" (click)="select()">
<div class="component">{{ entryLabel }}</div>
<div class="value" *ngIf="!editing; else editValue">
<div class="text">
<span
*ngFor="let componentValue of entry.componentValues"
[innerHTML]="transformNewLines(componentValue.value ?? componentValue.originalValue)"
>
</span>
</div>
<div class="actions">
<iqser-circle-button
(action)="edit()"
*ngIf="canEdit"
[tooltip]="'component-management.actions.edit' | translate"
icon="iqser:edit"
></iqser-circle-button>
<div class="changes-dot" *ngIf="hasUpdatedValues && canEdit"></div>
</div>
</div>
<mat-icon *ngIf="!editing" class="arrow-right" svgIcon="red:arrow-right"></mat-icon>
</div>
<ng-template #editValue>
<div cdkDropList (cdkDropListDropped)="drop($event)">
<div *ngFor="let value of entry.componentValues; let index = index" class="editing-value" cdkDrag>
<mat-icon class="draggable" svgIcon="red:draggable-dots" cdkDragHandle></mat-icon>
<div class="iqser-input-group w-full">
<textarea [(ngModel)]="value.value" rows="1" type="text"></textarea>
</div>
<iqser-circle-button
(action)="removeValue(index)"
[tooltip]="'component-management.actions.delete' | translate"
class="remove-value"
icon="iqser:trash"
></iqser-circle-button>
</div>
</div>
<div class="editing-actions">
<iqser-icon-button
(action)="save()"
[disabled]="disabled"
[label]="'component-management.actions.save' | translate"
[type]="iconButtonTypes.primary"
></iqser-icon-button>
<div (click)="deselect($event)" class="all-caps-label cancel" translate="component-management.actions.cancel"></div>
<div class="flex right">
<iqser-circle-button
*ngIf="hasUpdatedValues && canEdit"
(action)="undo()"
[showDot]="true"
[tooltip]="'component-management.actions.undo' | translate"
class="undo-value"
icon="red:undo"
></iqser-circle-button>
<iqser-circle-button
(action)="add()"
[tooltip]="'component-management.actions.add' | translate"
class="add-value"
icon="iqser:plus"
></iqser-circle-button>
</div>
</div>
</ng-template>

View File

@ -0,0 +1,158 @@
.component-value {
display: flex;
flex-direction: row;
padding: 10px 0 10px 0;
margin-left: 26px;
margin-right: 26px;
position: relative;
.component {
width: 40%;
}
.value {
width: 60%;
display: flex;
.text {
width: 80%;
display: flex;
flex-direction: column;
gap: 10px;
}
.actions {
display: flex;
justify-content: end;
align-items: center;
flex-grow: 1;
iqser-circle-button {
visibility: hidden;
}
.changes-dot {
height: 8px;
width: 8px;
background-color: var(--iqser-primary);
border-radius: 50%;
}
}
}
&.header {
font-weight: 600;
}
.arrow-right {
visibility: hidden;
position: absolute;
right: 0;
top: 50%;
transform: translateY(-50%) scale(0.7);
}
mat-icon {
transform: scale(0.7);
}
&:not(.header):hover,
&.selected {
background-color: var(--iqser-grey-8);
border-left: 4px solid var(--iqser-primary);
margin-left: 0;
margin-right: 0;
&:not(.editing) {
cursor: pointer;
}
.component {
margin-left: 22px;
}
.value {
margin-right: 26px;
.actions {
iqser-circle-button {
visibility: visible;
}
}
}
.arrow-right {
visibility: visible;
}
}
&.editing {
flex-direction: column;
.editing-value {
display: flex;
margin: 10px 0 10px 22px;
.iqser-input-group {
margin-top: 0;
textarea::-webkit-resizer {
display: none;
}
textarea::-moz-resizer {
display: none;
}
}
iqser-circle-button {
margin-top: 3px;
}
.draggable {
margin-top: 7px;
cursor: grab;
}
}
.editing-actions {
display: flex;
flex-direction: row;
align-items: center;
gap: 12px;
margin: 30px 10px 10px 22px;
.right {
margin-left: auto;
gap: 10px;
}
}
}
}
::ng-deep .add-value {
mat-icon {
transform: scale(2);
}
}
::ng-deep .undo-value {
mat-icon {
transform: scale(1.3);
}
}
::ng-deep .remove-value {
mat-icon {
transform: scale(1.2);
}
}
.cdk-drag-preview {
display: flex;
.draggable {
margin-top: 7px;
cursor: grab;
transform: scale(0.7);
}
}

View File

@ -0,0 +1,167 @@
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { IComponentLogEntry } from '@red/domain';
import { FormsModule } from '@angular/forms';
import { CircleButtonComponent, IconButtonComponent, IconButtonTypes, IqserDialog } from '@iqser/common-ui';
import { FilterService } from '@common-ui/filtering';
import { RevertValueDialogComponent } from '../../dialogs/docu-mine/revert-value-dialog/revert-value-dialog.component';
import { CdkDrag, CdkDragDrop, CdkDragHandle, CdkDropList, moveItemInArray } from '@angular/cdk/drag-drop';
import { KeyValuePipe, NgClass, NgForOf, NgIf } from '@angular/common';
import { MatIcon } from '@angular/material/icon';
import { TranslateModule } from '@ngx-translate/core';
@Component({
selector: 'redaction-editable-structured-component-value [entry] [canEdit]',
templateUrl: './editable-structured-component-value.component.html',
styleUrls: ['/editable-structured-component-value.component.scss'],
standalone: true,
imports: [
CircleButtonComponent,
NgClass,
TranslateModule,
KeyValuePipe,
CdkDropList,
MatIcon,
IconButtonComponent,
CdkDrag,
NgIf,
NgForOf,
CdkDragHandle,
FormsModule,
],
})
export class EditableStructuredComponentValueComponent implements OnInit {
@Input() entry: IComponentLogEntry;
@Input() canEdit: boolean;
@Output() readonly deselectLast = new EventEmitter();
@Output() readonly overrideValue = new EventEmitter<IComponentLogEntry>();
@Output() readonly revertOverride = new EventEmitter<string>();
selected = false;
protected entryLabel: string;
protected editing = false;
protected hasUpdatedValues = false;
protected initialEntry: IComponentLogEntry;
protected readonly iconButtonTypes = IconButtonTypes;
constructor(
private readonly _filtersService: FilterService,
private readonly _iqserDialog: IqserDialog,
) {}
ngOnInit() {
this.reset();
}
reset() {
this.initialEntry = this.#initialEntry;
this.hasUpdatedValues = this.#hasUpdatedValues;
this.entryLabel = this.parseName(this.entry.name);
this.deselect();
}
select() {
if (!this.editing) {
if (this.selected) {
this.deselect();
return;
}
this.deselectLast.emit();
this.selected = true;
this.#setWorkloadFilters();
}
}
edit() {
this.deselectLast.emit();
this.selected = true;
this.editing = true;
}
deselect($event?: MouseEvent) {
$event?.stopImmediatePropagation();
this.selected = false;
this.editing = false;
this._filtersService.deactivateFilters({ primaryFiltersSlug: 'primaryFilters' });
}
removeValue(index: number) {
this.entry.componentValues.splice(index, 1);
}
get disabled() {
for (let i = 0; i < this.entry.componentValues.length; i++) {
if (this.entry.componentValues[i].value !== this.initialEntry.componentValues[i]?.value) {
return false;
}
}
return this.entry.componentValues.length === this.initialEntry.componentValues.length;
}
save() {
this.entry.overridden = true;
this.overrideValue.emit(this.entry);
this.reset();
}
async undo() {
const dialog = this._iqserDialog.openDefault(RevertValueDialogComponent, { data: { entry: this.entry }, width: '800px' });
const result = await dialog.result();
if (result) {
this.revertOverride.emit(this.entry.name);
this.reset();
}
}
add() {
this.entry.componentValues.push({
componentRuleId: null,
entityReferences: [],
originalValue: null,
value: '',
valueDescription: '',
});
}
drop(event: CdkDragDrop<string>) {
moveItemInArray(this.entry.componentValues, event.previousIndex, event.currentIndex);
}
parseName(name: string) {
return name.replaceAll('_', ' ');
}
transformNewLines(value: string) {
return value.replace(/\n/g, '<br>');
}
get #hasUpdatedValues() {
for (const value of this.entry.componentValues) {
if (value.originalValue === null && value.value === '') {
continue;
}
if (value.originalValue !== value.value) {
return true;
}
}
return false;
}
get #initialEntry() {
return JSON.parse(JSON.stringify(this.entry));
}
#setWorkloadFilters() {
this._filtersService.deactivateFilters({ primaryFiltersSlug: 'primaryFilters' });
const filterGroup = this._filtersService.getGroup('primaryFilters');
for (const filter of filterGroup.filters) {
const nestedFilter = filter.children.find(f => f.label === this.entryLabel);
if (nestedFilter) {
this._filtersService.filterCheckboxClicked({ nestedFilter, filterGroup, primaryFiltersSlug: 'primaryFilters' });
return;
}
}
}
}

View File

@ -0,0 +1,81 @@
<div class="page-header">
<div class="flex">
<redaction-view-switch *ngIf="!isDocumine"></redaction-view-switch>
<redaction-documine-export *ngIf="isDocumine" [dossier]="state.dossier()"></redaction-documine-export>
</div>
<!-- TODO: mode this file preview header to a separate component-->
<div #actionsWrapper class="flex-2 actions-container">
<div class="assignee" [class.documine]="isDocumine">
<div class="vertical-line" *ngIf="isDocumine"></div>
<redaction-processing-indicator [file]="file" class="mr-16"></redaction-processing-indicator>
<redaction-user-management></redaction-user-management>
<ng-container *ngIf="permissionsService.isApprover(state.dossier()) && !!file.lastReviewer">
<div class="vertical-line"></div>
<div class="all-caps-label mr-16 ml-8 label">
{{ 'file-preview.last-assignee' | translate }}
</div>
<iqser-initials-avatar [user]="lastAssignee()" [withName]="true"></iqser-initials-avatar>
</ng-container>
</div>
<div class="vertical-line" *ngIf="!isDocumine"></div>
<!-- TODO: mode these actions to a separate component -->
<redaction-file-actions
[dossier]="state.dossier()"
[file]="file"
[helpModeKeyPrefix]="'editor'"
[minWidth]="width"
type="file-preview"
iqserDisableStopPropagation
></redaction-file-actions>
<iqser-circle-button
(action)="getTables()"
*allow="roles.getTables"
[icon]="'red:csv'"
[tooltip]="'file-preview.get-tables' | translate"
class="ml-2"
iqserDisableStopPropagation
></iqser-circle-button>
<iqser-circle-button
(action)="toggleFullScreen()"
[attr.help-mode-key]="'editor_full_screen'"
[icon]="fullScreen ? 'red:exit-fullscreen' : 'red:fullscreen'"
[tooltip]="'file-preview.fullscreen' | translate"
class="ml-2"
iqserDisableStopPropagation
></iqser-circle-button>
<!-- Dev Mode Features-->
<iqser-circle-button
(action)="downloadOriginalFile(file)"
*ngIf="isIqserDevMode"
[tooltip]="'file-preview.download-original-file' | translate"
[type]="circleButtonTypes.primary"
class="ml-8"
icon="iqser:download"
iqserDisableStopPropagation
></iqser-circle-button>
<!-- End Dev Mode Features-->
<iqser-circle-button
*ngIf="!fullScreen"
[attr.help-mode-key]="'editor_close'"
[routerLink]="state.dossier().routerLink"
[tooltip]="'common.close' | translate"
class="ml-8"
icon="iqser:close"
iqserDisableStopPropagation
></iqser-circle-button>
</div>
</div>

View File

@ -0,0 +1,25 @@
.page-header {
max-width: 100vw;
}
.actions-container {
display: flex;
align-items: center;
justify-content: flex-end;
.assignee {
display: flex;
align-items: center;
&.documine {
flex: 1;
}
}
}
.vertical-line {
width: 1px;
height: 30px;
background-color: var(--iqser-separator);
margin: 0 16px;
}

View File

@ -0,0 +1,221 @@
import {
AfterViewInit,
ChangeDetectorRef,
Component,
computed,
ElementRef,
HostListener,
Input,
NgZone,
OnDestroy,
OnInit,
ViewChild,
} from '@angular/core';
import { Roles } from '@users/roles';
import {
CircleButtonTypes,
getConfig,
HelpModeService,
IqserDialog,
IqserPermissionsService,
isIqserDevMode,
LoadingService,
} from '@iqser/common-ui';
import { Bind, Debounce, OnDetach } from '@iqser/common-ui/lib/utils';
import { File } from '@red/domain';
import { PermissionsService } from '@services/permissions.service';
import { FilePreviewStateService } from '../../services/file-preview-state.service';
import { UserPreferenceService } from '@users/user-preference.service';
import JSZip from 'jszip';
import { saveAs } from 'file-saver';
import { PdfViewer } from '../../../pdf-viewer/services/pdf-viewer.service';
import { AnnotationDrawService } from '../../../pdf-viewer/services/annotation-draw.service';
import { TablesService } from '../../services/tables.service';
import { ALL_HOTKEYS } from '../../utils/constants';
import { Router } from '@angular/router';
import { AnnotationActionsService } from '../../services/annotation-actions.service';
import { FileDataService } from '../../services/file-data.service';
import { REDAnnotationManager } from '../../../pdf-viewer/services/annotation-manager.service';
import { MatDialog } from '@angular/material/dialog';
import { download } from '@utils/file-download-utils';
import { firstValueFrom } from 'rxjs';
import { FileManagementService } from '@services/files/file-management.service';
import { MultiSelectService } from '../../services/multi-select.service';
@Component({
selector: 'redaction-file-header',
templateUrl: './file-header.component.html',
styleUrls: ['/file-header.component.scss'],
})
export class FileHeaderComponent implements OnInit, AfterViewInit, OnDetach, OnDestroy {
@ViewChild('actionsWrapper', { static: false }) private readonly _actionsWrapper: ElementRef;
@Input() file: File;
protected readonly roles = Roles;
protected readonly circleButtonTypes = CircleButtonTypes;
readonly lastAssignee = computed(() => this.getLastAssignee());
readonly isIqserDevMode = isIqserDevMode();
readonly isDocumine = getConfig().IS_DOCUMINE;
width: number;
fullScreen = false;
constructor(
private readonly _changeRef: ChangeDetectorRef,
private readonly _iqserPermissionsService: IqserPermissionsService,
private readonly _loadingService: LoadingService,
private readonly _pdf: PdfViewer,
private readonly _annotationDrawService: AnnotationDrawService,
private readonly _tablesService: TablesService,
private readonly _router: Router,
private readonly _ngZone: NgZone,
private readonly _helpModeService: HelpModeService,
private readonly _annotationActionsService: AnnotationActionsService,
private readonly _fileDataService: FileDataService,
private readonly _annotationManager: REDAnnotationManager,
private readonly _dialog: MatDialog,
private readonly _fileManagementService: FileManagementService,
private readonly _multiSelectService: MultiSelectService,
readonly state: FilePreviewStateService,
readonly permissionsService: PermissionsService,
) {}
ngOnInit() {
document.documentElement.addEventListener('fullscreenchange', this.fullscreenListener);
}
ngAfterViewInit() {
const _observer = new ResizeObserver((entries: ResizeObserverEntry[]) => {
this._updateItemWidth(entries[0]);
});
_observer.observe(this._actionsWrapper.nativeElement);
}
ngOnDetach() {
document.documentElement.removeEventListener('fullscreenchange', this.fullscreenListener);
}
ngOnDestroy() {
document.documentElement.removeEventListener('fullscreenchange', this.fullscreenListener);
}
async downloadOriginalFile({ cacheIdentifier, dossierId, fileId, filename }: File) {
const originalFile = this._fileManagementService.downloadOriginal(dossierId, fileId, 'response', cacheIdentifier);
download(await firstValueFrom(originalFile), filename);
}
getLastAssignee() {
const { isApproved, lastReviewer, lastApprover } = this.state.file();
const isRss = this._iqserPermissionsService.has(this.roles.getRss);
return isApproved ? (isRss ? lastReviewer : lastApprover) : lastReviewer;
}
async getTables() {
this._loadingService.start();
const currentPage = this._pdf.currentPage();
const tables = await this._tablesService.get(this.state.dossierId, this.state.fileId, this._pdf.currentPage());
await this._annotationDrawService.drawTables(tables, currentPage, this.state.dossierTemplateId);
const filename = this.state.file().filename;
const zip = new JSZip();
tables.forEach((t, index) => {
const blob = new Blob([atob(t.csvAsBytes)], {
type: 'text/csv;charset=utf-8',
});
zip.file(filename + '_page' + currentPage + '_table' + (index + 1) + '.csv', blob);
});
saveAs(await zip.generateAsync({ type: 'blob' }), filename + '_tables.zip');
this._loadingService.stop();
}
toggleFullScreen() {
this.fullScreen = !this.fullScreen;
if (this.fullScreen) {
this.#openFullScreen();
} else {
this.closeFullScreen();
}
}
closeFullScreen() {
if (!!document.fullscreenElement && document.exitFullscreen) {
document.exitFullscreen().then();
}
}
@Bind()
fullscreenListener() {
if (!document.fullscreenElement) {
this.fullScreen = false;
}
}
@HostListener('document:keyup', ['$event'])
handleKeyEvent($event: KeyboardEvent) {
if (this._router.url.indexOf('/file/') < 0) {
return;
}
if (!ALL_HOTKEYS.includes($event.key) || this._dialog.openDialogs.length) {
return;
}
if (['Escape'].includes($event.key)) {
$event.preventDefault();
if (this._annotationManager.resizingAnnotationId) {
const resizedAnnotation = this._fileDataService
.annotations()
.find(annotation => annotation.id === this._annotationManager.resizingAnnotationId);
this._annotationActionsService.cancelResize(resizedAnnotation).then();
}
if (this._annotationManager.selected.length) {
this._annotationManager.deselectAll();
}
if (this._multiSelectService.active()) {
this._multiSelectService.deactivate();
}
this.fullScreen = false;
this.closeFullScreen();
this._changeRef.markForCheck();
}
if (!$event.ctrlKey && !$event.metaKey && ['f', 'F'].includes($event.key)) {
// if you type in an input, don't toggle full-screen
if ($event.target instanceof HTMLInputElement || $event.target instanceof HTMLTextAreaElement) {
return;
}
this.toggleFullScreen();
return;
}
if (['h', 'H'].includes($event.key)) {
if ($event.target instanceof HTMLInputElement || $event.target instanceof HTMLTextAreaElement) {
return;
}
this._ngZone.run(() => {
window.focus();
this._helpModeService.activateHelpMode(false);
});
return;
}
}
#openFullScreen() {
const documentElement = document.documentElement;
if (documentElement.requestFullscreen) {
documentElement.requestFullscreen().then();
}
}
@Debounce(30)
private _updateItemWidth(entry: ResizeObserverEntry): void {
this.width = entry.contentRect.width;
this._changeRef.detectChanges();
}
}

View File

@ -1,50 +1,47 @@
<div
*ngIf="excludedPagesService.shown(); else selectAndFilter"
class="right-title heading"
translate="file-preview.tabs.exclude-pages.label"
>
<div>
<iqser-circle-button
(action)="excludedPagesService.toggle()"
[tooltip]="'file-preview.tabs.exclude-pages.close' | translate"
icon="iqser:close"
tooltipPosition="before"
></iqser-circle-button>
</div>
</div>
<ng-template #selectAndFilter>
<div class="right-title heading">
{{ title() | translate }}
<ng-container *ngIf="!isDocumine">
<div
*ngIf="excludedPagesService.shown(); else selectAndFilter"
class="right-title heading"
translate="file-preview.tabs.exclude-pages.label"
>
<div>
<div
(click)="multiSelectService.activate()"
*ngIf="multiSelectService.enabled() && multiSelectService.inactive()"
[attr.help-mode-key]="'workload_bulk_selection'"
class="all-caps-label primary pointer"
translate="file-preview.tabs.annotations.select"
></div>
<iqser-popup-filter
*ngIf="documentInfoService.hidden()"
[actionsTemplate]="annotationFilterActionTemplate"
[attr.help-mode-key]="'workload_filter'"
[fileId]="state.file()?.id"
[primaryFiltersSlug]="'primaryFilters'"
[secondaryFiltersSlug]="'secondaryFilters'"
></iqser-popup-filter>
<iqser-circle-button
(action)="excludedPagesService.toggle()"
[tooltip]="'file-preview.tabs.exclude-pages.close' | translate"
icon="iqser:close"
tooltipPosition="before"
></iqser-circle-button>
</div>
</div>
</ng-template>
<ng-template #selectAndFilter>
<div class="right-title heading">
{{ title() | translate }}
<div>
<div
(click)="multiSelectService.activate()"
*ngIf="multiSelectService.enabled() && multiSelectService.inactive()"
[attr.help-mode-key]="'workload_bulk_selection'"
class="all-caps-label primary pointer"
translate="file-preview.tabs.annotations.select"
></div>
<ng-container *ngTemplateOutlet="annotationsFilter"></ng-container>
</div>
</div>
</ng-template>
</ng-container>
<div class="right-content">
<redaction-readonly-banner
*ngIf="showAnalysisDisabledBanner; else readOnlyBanner"
[customTranslation]="translations.analysisDisabled"
></redaction-readonly-banner>
<ng-template #readOnlyBanner>
<redaction-readonly-banner *ngIf="state.isReadonly()"></redaction-readonly-banner>
</ng-template>
<ng-container *ngIf="!isDocumine">
<redaction-readonly-banner
*ngIf="showAnalysisDisabledBanner; else readOnlyBanner"
[customTranslation]="translations.analysisDisabled"
></redaction-readonly-banner>
<ng-template #readOnlyBanner>
<redaction-readonly-banner *ngIf="state.isReadonly()"></redaction-readonly-banner>
</ng-template>
</ng-container>
<div *ngIf="multiSelectService.active()" class="multi-select">
<div class="selected-wrapper">
@ -88,22 +85,20 @@
(click)="scrollQuickNavFirst()"
[class.disabled]="pdf.currentPage() === 1"
[matTooltip]="'file-preview.quick-nav.jump-first' | translate"
[class.documine-height]="isDocumine"
class="jump"
matTooltipPosition="above"
>
<mat-icon svgIcon="iqser:nav-first"></mat-icon>
</div>
<redaction-pages
(click)="pagesPanelActive = true"
[displayedAnnotations]="displayedAnnotations"
[pages]="displayedPages"
></redaction-pages>
<redaction-pages (click)="pagesPanelActive = true" [pages]="displayedPages"></redaction-pages>
<div
(click)="scrollQuickNavLast()"
[class.disabled]="pdf.currentPage() === state.file()?.numberOfPages"
[matTooltip]="'file-preview.quick-nav.jump-last' | translate"
[class.documine-height]="isDocumine"
class="jump"
matTooltipPosition="above"
>
@ -111,7 +106,7 @@
</div>
</div>
<div class="content">
<div class="content" [class.documine-width]="isDocumine">
<div
*ngIf="!viewModeService.isEarmarks()"
[attr.anotation-page-header]="pdf.currentPage()"
@ -119,21 +114,23 @@
class="workload-separator"
>
<span *ngIf="!!pdf.currentPage()" class="flex-align-items-center">
<iqser-circle-button
(action)="excludedPagesService.toggle()"
*ngIf="currentPageIsExcluded()"
[size]="14"
[tooltip]="'file-preview.excluded-from-redaction' | translate | capitalize"
class="mr-10 primary"
icon="red:exclude-pages"
tooltipPosition="above"
></iqser-circle-button>
<ng-container *ngIf="!isDocumine; else documineHeader">
<iqser-circle-button
(action)="excludedPagesService.toggle()"
*ngIf="currentPageIsExcluded()"
[size]="14"
[tooltip]="'file-preview.excluded-from-redaction' | translate | capitalize"
class="mr-10 primary"
icon="red:exclude-pages"
tooltipPosition="above"
></iqser-circle-button>
<span
[translateParams]="{ page: pdf.currentPage(), count: activeAnnotations.length }"
[translate]="'page'"
class="all-caps-label"
></span>
<span
[translateParams]="{ page: pdf.currentPage(), count: activeAnnotations.length }"
[translate]="'page'"
class="all-caps-label"
></span>
</ng-container>
</span>
<div *ngIf="multiSelectService.active()">
@ -231,3 +228,19 @@
iqserPreventDefault
></iqser-circle-button>
</ng-template>
<ng-template #documineHeader>
<span [translate]="'annotations'"></span>
<ng-container *ngTemplateOutlet="annotationsFilter"></ng-container>
</ng-template>
<ng-template #annotationsFilter>
<iqser-popup-filter
*ngIf="documentInfoService.hidden()"
[actionsTemplate]="annotationFilterActionTemplate"
[attr.help-mode-key]="'workload_filter'"
[fileId]="state.file()?.id"
[primaryFiltersSlug]="'primaryFilters'"
[secondaryFiltersSlug]="'secondaryFilters'"
></iqser-popup-filter>
</ng-template>

View File

@ -32,7 +32,33 @@
width: 100%;
display: flex;
flex-direction: column;
&.documine-width {
width: calc(var(--documine-workload-content-width));
border-right: 1px solid var(--iqser-separator);
z-index: 1;
.workload-separator {
min-height: 37px;
background: var(--iqser-grey-8);
.flex-align-items-center {
width: 100%;
justify-content: space-between;
::ng-deep span {
color: var(--iqser-text);
font-size: var(--iqser-font-size);
line-height: 20px;
font-weight: 600;
}
}
}
}
}
flex-direction: row-reverse;
justify-content: space-between;
}
.quick-navigation,
@ -46,7 +72,8 @@
.quick-navigation {
border-right: 1px solid var(--iqser-separator);
min-width: 61px;
border-left: 1px solid var(--iqser-separator);
min-width: var(--qiuck-navigation-width);
overflow: hidden;
display: flex;
flex-direction: column;
@ -59,6 +86,10 @@
cursor: pointer;
transition: background-color 0.25s;
&.documine-height {
min-height: 37px;
}
&:not(.disabled):hover {
background-color: var(--iqser-tab-hover);
}

View File

@ -38,7 +38,6 @@ const ALL_HOTKEY_ARRAY = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'];
export class FileWorkloadComponent extends AutoUnsubscribe implements OnInit, OnDestroy {
@ViewChild('annotationsElement') private readonly _annotationsElement: ElementRef;
@ViewChild('quickNavigation') private readonly _quickNavigationElement: ElementRef;
readonly #isDocumine = getConfig().IS_DOCUMINE;
readonly #isIqserDevMode = this._userPreferenceService.isIqserDevMode;
displayedAnnotations = new Map<number, AnnotationWrapper[]>();
displayedPages: number[] = [];
@ -51,6 +50,7 @@ export class FileWorkloadComponent extends AutoUnsubscribe implements OnInit, On
);
protected readonly currentPageIsExcluded = computed(() => this.state.file().excludedPages.includes(this.pdf.currentPage()));
protected readonly translations = workloadTranslations;
protected readonly isDocumine = getConfig().IS_DOCUMINE;
constructor(
readonly filterService: FilterService,
@ -362,7 +362,7 @@ export class FileWorkloadComponent extends AutoUnsubscribe implements OnInit, On
annotations = annotations.filter(a => !a.isRemoved);
}
if (this.#isDocumine && !this.#isIqserDevMode) {
if (this.isDocumine && !this.#isIqserDevMode) {
annotations = annotations.filter(a => !a.isOCR);
}
@ -476,6 +476,6 @@ export class FileWorkloadComponent extends AutoUnsubscribe implements OnInit, On
get showAnalysisDisabledBanner() {
const file = this.state.file();
return this.#isDocumine && file.excludedFromAutomaticAnalysis && file.workflowStatus !== WorkflowFileStatuses.APPROVED;
return this.isDocumine && file.excludedFromAutomaticAnalysis && file.workflowStatus !== WorkflowFileStatuses.APPROVED;
}
}

View File

@ -20,7 +20,6 @@ export class PagesComponent implements AfterViewInit {
readonly #listingService = inject(AnnotationsListingService);
protected readonly _pdf = inject(PdfViewer);
@Input({ required: true }) pages: List<number>;
@Input({ required: true }) displayedAnnotations: Map<number, AnnotationWrapper[]>;
readonly viewedPages$ = inject(ViewedPagesMapService).get$(this.#state.fileId);
ngAfterViewInit() {

View File

@ -0,0 +1,26 @@
<div class="components-header">
<span [translate]="'component-management.components'"></span>
<iqser-popup-filter [primaryFiltersSlug]="'componentLogFilters'"></iqser-popup-filter>
</div>
<div *ngIf="displayedComponents$ | async as displayedComponents" class="components-container">
<div class="component-row">
<div class="header">
<div class="component">{{ 'component-management.table-header.component' | translate }}</div>
<div class="value">{{ 'component-management.table-header.value' | translate }}</div>
</div>
<div class="row-separator"></div>
</div>
<div *ngFor="let entry of displayedComponents" class="component-row">
<redaction-editable-structured-component-value
#editableComponent
[entry]="entry"
[canEdit]="canEdit"
(deselectLast)="deselectLast()"
(revertOverride)="revertOverride($event)"
(overrideValue)="overrideValue($event)"
></redaction-editable-structured-component-value>
<div class="row-separator"></div>
</div>
</div>

View File

@ -0,0 +1,55 @@
.components-header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
min-height: 36px;
background: var(--iqser-grey-8);
border-bottom: 1px solid var(--iqser-separator);
padding: 0 10px 0 26px;
::ng-deep span {
color: var(--iqser-text);
font-size: var(--iqser-font-size);
line-height: 20px;
font-weight: 600;
}
}
mat-icon {
transform: scale(0.8);
}
.components-container {
display: flex;
flex-direction: column;
font-size: 12px;
overflow: scroll;
height: calc(100% - 40px);
.component-row {
display: flex;
flex-direction: column;
margin-left: 13px;
margin-right: 13px;
.header {
display: flex;
padding: 10px 26px;
font-weight: 600;
:first-child {
width: 40%;
}
}
.row-separator {
&:not(:last-child) {
border-bottom: 1px solid var(--iqser-separator);
}
border-bottom: 1px solid var(--iqser-separator);
margin-left: 26px;
margin-right: 26px;
}
}
}

View File

@ -0,0 +1,105 @@
import { Component, Input, OnInit, signal, ViewChildren } from '@angular/core';
import { ComponentLogEntry, Dictionary, File, IComponentLogEntry, WorkflowFileStatuses } from '@red/domain';
import { IconButtonTypes, LoadingService } from '@iqser/common-ui';
import { ComponentLogService } from '@services/files/component-log.service';
import { FilesMapService } from '@services/files/files-map.service';
import { UserPreferenceService } from '@users/user-preference.service';
import { combineLatest, firstValueFrom, Observable } from 'rxjs';
import { List } from '@common-ui/utils';
import { EditableStructuredComponentValueComponent } from '../editable-structured-component-value/editable-structured-component-value.component';
import { FilterService } from '@common-ui/filtering';
import { ComponentLogFilterService } from '../../services/component-log-filter.service';
import { map } from 'rxjs/operators';
import { toObservable } from '@angular/core/rxjs-interop';
@Component({
selector: 'redaction-structured-component-management',
templateUrl: './structured-component-management.component.html',
styleUrls: ['/structured-component-management.component.scss'],
})
export class StructuredComponentManagementComponent implements OnInit {
@Input() file: File;
@Input() dictionaries: Dictionary[];
@ViewChildren('editableComponent') editableComponents: List<EditableStructuredComponentValueComponent>;
protected readonly componentLogData = signal<ComponentLogEntry[] | undefined>(undefined);
protected readonly componentLogData$ = toObservable(this.componentLogData);
protected readonly openScmDialogByDefault = signal(this.userPreferences.getOpenScmDialogByDefault());
protected readonly iconButtonTypes = IconButtonTypes;
protected displayedComponents$: Observable<ComponentLogEntry[]>;
constructor(
private readonly _componentLogService: ComponentLogService,
private readonly _filesMapService: FilesMapService,
private readonly _loadingService: LoadingService,
private readonly _componentLogFilterService: ComponentLogFilterService,
private readonly _filterService: FilterService,
readonly userPreferences: UserPreferenceService,
) {}
async ngOnInit(): Promise<void> {
await this.#loadData();
this.displayedComponents$ = this.#displayedComponents$();
}
#displayedComponents$() {
const componentLogFilters$ = this._filterService.getFilterModels$('componentLogFilters');
return combineLatest([this.componentLogData$, componentLogFilters$]).pipe(
map(([components, filters]) => this._componentLogFilterService.filterComponents(components, filters)),
);
}
deselectLast() {
const lastSelected = this.editableComponents.find(c => c.selected);
if (lastSelected) {
lastSelected.deselect();
}
}
get canEdit() {
return this.file.workflowStatus !== WorkflowFileStatuses.APPROVED;
}
async toggleOpenScmDialogByDefault() {
await this.userPreferences.toggleOpenScmDialogByDefault();
await this.userPreferences.reload();
this.openScmDialogByDefault.set(this.userPreferences.getOpenScmDialogByDefault());
}
async revertOverride(originalKey: string) {
this._loadingService.start();
await firstValueFrom(
this._componentLogService.revertOverride(this.file.dossierTemplateId, this.file.dossierId, this.file.fileId, [originalKey]),
);
await this.#loadData();
}
async overrideValue(componentLogEntry: IComponentLogEntry) {
this._loadingService.start();
await firstValueFrom(
this._componentLogService.override(this.file.dossierTemplateId, this.file.dossierId, this.file.fileId, componentLogEntry),
);
await this.#loadData();
}
async #loadData(): Promise<void> {
this._loadingService.start();
const componentLogData = await firstValueFrom(
this._componentLogService.getComponentLogData(
this.file.dossierTemplateId,
this.file.dossierId,
this.file.fileId,
this.dictionaries,
),
);
this.#computeFilters(componentLogData);
this.componentLogData.set(componentLogData);
this._loadingService.stop();
}
#computeFilters(componentLogs: ComponentLogEntry[]) {
const filterGroups = this._componentLogFilterService.filterGroups(componentLogs);
this._filterService.addFilterGroups(filterGroups);
}
}

View File

@ -0,0 +1,39 @@
<section class="dialog">
<div [translate]="'revert-value-dialog.title'" class="dialog-header heading-l"></div>
<div class="dialog-content">
<span *ngFor="let value of entry.componentValues">
{{ value.valueDescription }}
</span>
<div class="compare-values-container">
<div>
<span class="header">
{{ 'revert-value-dialog.original-values' | translate }}
</span>
<div class="values">
<p *ngFor="let value of entry.componentValues; let index = index">
{{ index + 1 + '. ' + (value.originalValue ?? '') }}
</p>
</div>
</div>
<div>
<span class="header">
{{ 'revert-value-dialog.current-values' | translate }}
</span>
<div class="values">
<p *ngFor="let value of entry.componentValues; let index = index">
{{ index + 1 + '. ' + value.value }}
</p>
</div>
</div>
</div>
</div>
<div class="dialog-actions">
<iqser-icon-button (click)="save()" [label]="'revert-value-dialog.actions.revert' | translate" [type]="iconButtonTypes.primary" />
<div [translate]="'revert-value-dialog.actions.cancel'" class="all-caps-label cancel" mat-dialog-close></div>
</div>
<iqser-circle-button (action)="close()" class="dialog-close" icon="iqser:close" />
</section>

View File

@ -0,0 +1,27 @@
.dialog-content {
padding-top: 12px;
.compare-values-container {
margin-top: 30px;
display: flex;
div {
flex-grow: 1;
.header {
font-weight: bold;
}
.values {
margin-top: 10px;
padding: 10px 0 10px 15px;
width: 90%;
background: var(--iqser-grey-8);
p {
margin: 0;
}
}
}
}
}

View File

@ -0,0 +1,28 @@
import { Component } from '@angular/core';
import { CircleButtonComponent, ConfirmOptions, IconButtonComponent, IqserDialogComponent } from '@iqser/common-ui';
import { MatDialogClose } from '@angular/material/dialog';
import { TranslateModule } from '@ngx-translate/core';
import { IComponentLogEntry } from '@red/domain';
import { NgFor } from '@angular/common';
interface RevertValueData {
entry: IComponentLogEntry;
}
interface RevertValueResult {}
@Component({
templateUrl: 'revert-value-dialog.component.html',
styleUrls: ['./revert-value-dialog.component.scss'],
standalone: true,
imports: [CircleButtonComponent, IconButtonComponent, MatDialogClose, TranslateModule, NgFor],
})
export class RevertValueDialogComponent extends IqserDialogComponent<RevertValueDialogComponent, RevertValueData, RevertValueResult> {
protected readonly entry = this.data.entry;
constructor() {
super();
}
save() {
this.close(ConfirmOptions.CONFIRM);
}
}

View File

@ -1,97 +0,0 @@
<section class="dialog">
<div [translate]="'component-log-dialog.title'" class="dialog-header heading-l"></div>
<hr />
<div class="dialog-content" id="scm-edit">
<div *ngIf="componentLogData() as componentLogEntries" class="table output-data">
<div class="table-header">{{ 'component-log-dialog.table-header.component' | translate }}</div>
<div class="table-header">{{ 'component-log-dialog.table-header.value' | translate }}</div>
<div class="table-header">{{ 'component-log-dialog.table-header.transformation-rule' | translate }}</div>
<div class="table-header">{{ 'component-log-dialog.table-header.annotation-references' | translate }}</div>
<ng-container *ngFor="let entry of componentLogEntries; let index = index">
<div class="bold">{{ entry.name }}</div>
<div [id]="getValueCellId(index)">
<iqser-editable-input
(save)="saveEdit($event, entry.originalKey)"
[canEdit]="canEdit"
[cancelTooltip]="'component-log-dialog.actions.cancel-edit' | translate"
[editTooltip]="'component-log-dialog.actions.edit' | translate"
[id]="'value-' + index"
[parentId]="getValueCellId(index)"
[saveTooltip]="'component-log-dialog.actions.save' | translate"
[value]="entry.componentValues[0].value ?? entry.componentValues[0].originalValue"
[attr.helpModeKey]="'scm_edit_DIALOG'"
>
<ng-container slot="editing">
<iqser-circle-button
(action)="undo(entry.originalKey)"
*ngIf="entry.componentValues[0].value !== entry.componentValues[0].originalValue && canEdit"
[showDot]="true"
[tooltip]="
'component-log-dialog.actions.undo'
| translate: { value: entry.componentValues[0].originalValue }
| replaceNbsp
"
[attr.help-mode-key]="'scm_undo_DIALOG'"
class="ml-2"
icon="red:undo"
></iqser-circle-button>
</ng-container>
</iqser-editable-input>
</div>
<div>{{ entry.componentValues[0].valueDescription }}</div>
<div>
<ul *ngIf="entry.componentValues[0].entityReferences; else noReferences" class="pl-0">
<li
*ngFor="let reference of entry.componentValues[0].entityReferences"
[innerHTML]="
'component-log-dialog.annotations'
| translate
: {
type: parseType(reference.displayValue),
page: reference.page,
ruleNumber: reference.entityRuleId
}
"
class="mb-8"
></li>
</ul>
<ng-template #noReferences>-</ng-template>
</div>
</ng-container>
</div>
</div>
<div class="dialog-actions">
<iqser-icon-button
(action)="exportJSON()"
[label]="'component-log-dialog.actions.export-json' | translate"
[submit]="true"
[type]="iconButtonTypes.primary"
[attr.help-mode-key]="'scm_export_DIALOG'"
></iqser-icon-button>
<iqser-icon-button
(action)="exportXML()"
[label]="'component-log-dialog.actions.export-xml' | translate"
[type]="iconButtonTypes.primary"
[attr.help-mode-key]="'scm_export_DIALOG'"
></iqser-icon-button>
<iqser-icon-button
(action)="exportAllInDossier()"
*ngIf="userPreferences.isIqserDevMode"
[type]="iconButtonTypes.primary"
label="Export All"
></iqser-icon-button>
<div [translate]="'component-log-dialog.actions.close'" class="all-caps-label cancel" mat-dialog-close></div>
<mat-checkbox (change)="toggleOpenScmDialogByDefault()" [checked]="openScmDialogByDefault()" class="ml-auto" color="primary"
>{{ 'component-log-dialog.actions.display-by-default' | translate }}
</mat-checkbox>
</div>
<iqser-circle-button (action)="close()" class="dialog-close" icon="iqser:close"></iqser-circle-button>
</section>

View File

@ -1,61 +0,0 @@
.rss-row {
display: flex;
flex-direction: row;
border-bottom: 1px solid var(--iqser-separator);
.rss-key {
font-weight: bold;
flex: 30;
text-align: right;
padding: 4px;
}
.rss-value {
padding: 4px;
flex: 70;
}
}
.dialog-content {
overflow: auto;
}
.table {
display: grid;
grid-template-columns: repeat(4, 1fr);
> div {
padding: 8px 10px;
}
.bold {
font-weight: 600;
}
.table-header {
margin: 10px 0;
border-bottom: 1px solid var(--iqser-separator);
background-color: var(--iqser-grey-2);
font-weight: 600;
}
}
.annotation-grid {
display: grid;
grid-template-columns: 3fr 1fr 1fr 5fr;
}
ul {
margin: 0;
}
.output-data > div:nth-child(8n + 9),
.output-data > div:nth-child(8n + 10),
.output-data > div:nth-child(8n + 11),
.output-data > div:nth-child(8n + 12) {
background: var(--iqser-grey-8);
}
.ml-auto {
margin-left: auto;
}

View File

@ -1,140 +0,0 @@
import { KeyValuePipe, NgForOf, NgIf } from '@angular/common';
import { ChangeDetectionStrategy, Component, Inject, OnInit, signal } from '@angular/core';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog';
import { ReplaceNbspPipe } from '@common-ui/pipes/replace-nbsp.pipe';
import { BaseDialogComponent, CircleButtonComponent, EditableInputComponent, IconButtonComponent } from '@iqser/common-ui';
import { TranslateModule } from '@ngx-translate/core';
import { ComponentLogEntry, Dictionary, IFile, WorkflowFileStatuses } from '@red/domain';
import { FilesMapService } from '@services/files/files-map.service';
import { UserPreferenceService } from '@users/user-preference.service';
import { firstValueFrom } from 'rxjs';
import { ComponentLogService } from '@services/files/component-log.service';
interface ScmData {
file: IFile;
dictionaries: Dictionary[];
}
@Component({
templateUrl: './structured-component-management-dialog.component.html',
styleUrls: ['./structured-component-management-dialog.component.scss'],
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
NgIf,
EditableInputComponent,
NgForOf,
KeyValuePipe,
TranslateModule,
CircleButtonComponent,
IconButtonComponent,
MatCheckboxModule,
MatDialogModule,
ReplaceNbspPipe,
],
})
export class StructuredComponentManagementDialogComponent extends BaseDialogComponent implements OnInit {
readonly componentLogData = signal<ComponentLogEntry[] | undefined>(undefined);
readonly openScmDialogByDefault = signal(this.userPreferences.getOpenScmDialogByDefault());
constructor(
protected readonly _dialogRef: MatDialogRef<StructuredComponentManagementDialogComponent>,
private readonly _componentLogService: ComponentLogService,
readonly userPreferences: UserPreferenceService,
private readonly _filesMapService: FilesMapService,
@Inject(MAT_DIALOG_DATA) readonly data: ScmData,
) {
super(_dialogRef);
}
get canEdit() {
return this.data.file.workflowStatus !== WorkflowFileStatuses.APPROVED;
}
async ngOnInit(): Promise<void> {
await this.#loadData();
}
getValueCellId(index: number) {
return `value-cell-${index}`;
}
originalOrder = (): number => 0;
exportJSON() {
return firstValueFrom(
this._componentLogService.exportJSON(this.data.file.dossierTemplateId, this.data.file.dossierId, this.data.file),
);
}
exportXML() {
return firstValueFrom(
this._componentLogService.exportXML(this.data.file.dossierTemplateId, this.data.file.dossierId, this.data.file),
);
}
async exportAllInDossier() {
const allFilesInDossier = this._filesMapService.get(this.data.file.dossierId);
for (const file of allFilesInDossier) {
await firstValueFrom(this._componentLogService.exportJSON(this.data.file.dossierTemplateId, file.dossierId, file));
await firstValueFrom(this._componentLogService.exportXML(this.data.file.dossierTemplateId, file.dossierId, file));
}
}
save() {
return this.exportJSON();
}
parseType(type: string) {
return type.replaceAll('_', ' ').replace(/(^\w{1})|(\s+\w{1})/g, letter => letter.toUpperCase());
}
async toggleOpenScmDialogByDefault() {
await this.userPreferences.toggleOpenScmDialogByDefault();
await this.userPreferences.reload();
this.openScmDialogByDefault.set(this.userPreferences.getOpenScmDialogByDefault());
}
async undo(originalKey: string) {
this._loadingService.start();
await firstValueFrom(this._componentLogService.revertOverride(this.data.file.dossierId, this.data.file.fileId, [originalKey]));
await this.#loadData();
}
async saveEdit(event: string, originalKey: string) {
this._loadingService.start();
await firstValueFrom(this._componentLogService.override(this.data.file.dossierId, this.data.file.fileId, { [originalKey]: event }));
await this.#loadData();
}
async #loadData(): Promise<void> {
this._loadingService.start();
const componentLogData = await firstValueFrom(
this._componentLogService.getComponentLogData(
this.data.file.dossierTemplateId,
this.data.file.dossierId,
this.data.file.fileId,
),
);
this.#updateDisplayValue(componentLogData);
this.componentLogData.set(componentLogData);
this._loadingService.stop();
}
#updateDisplayValue(componentLogs: ComponentLogEntry[]) {
const dictionaries = this.data.dictionaries;
for (const componentLog of componentLogs) {
let foundDictionary: Dictionary;
for (const reference of componentLog.componentValues[0].entityReferences) {
if (foundDictionary) {
reference.displayValue = foundDictionary.label;
continue;
}
foundDictionary = dictionaries.find(dict => dict.type === reference.type);
foundDictionary = foundDictionary ?? ({ label: reference.type } as Dictionary);
reference.displayValue = foundDictionary.label;
}
}
}
}

View File

@ -16,6 +16,7 @@ import { PdfProxyService } from './services/pdf-proxy.service';
import { SkippedService } from './services/skipped.service';
import { StampService } from './services/stamp.service';
import { ViewModeService } from './services/view-mode.service';
import { ComponentLogFilterService } from './services/component-log-filter.service';
export const filePreviewScreenProviders = [
FilterService,
@ -38,4 +39,5 @@ export const filePreviewScreenProviders = [
SearchService,
StampService,
PdfProxyService,
ComponentLogFilterService,
];

View File

@ -1,100 +1,21 @@
<section *ngIf="state.file() as file">
<div class="page-header">
<div class="flex flex-1">
<redaction-view-switch></redaction-view-switch>
</div>
<!-- TODO: mode this file preview header to a separate component-->
<div #actionsWrapper class="flex-2 actions-container">
<redaction-processing-indicator [file]="file" class="mr-16"></redaction-processing-indicator>
<redaction-user-management></redaction-user-management>
<ng-container *ngIf="permissionsService.isApprover(state.dossier()) && !!file.lastReviewer">
<div class="vertical-line"></div>
<div class="all-caps-label mr-16 ml-8 label">
{{ 'file-preview.last-assignee' | translate }}
</div>
<iqser-initials-avatar [user]="lastAssignee()" [withName]="true"></iqser-initials-avatar>
</ng-container>
<div class="vertical-line"></div>
<!-- TODO: mode these actions to a separate component -->
<iqser-circle-button
(action)="openComponentLogView()"
*allow="roles.getRss"
[attr.help-mode-key]="'editor_scm'"
[tooltip]="'file-preview.open-rss-view' | translate"
class="ml-8"
icon="red:extract"
tooltipPosition="below"
iqserDisableStopPropagation
></iqser-circle-button>
<redaction-file-actions
[dossier]="state.dossier()"
[file]="file"
[helpModeKeyPrefix]="'editor'"
[minWidth]="width"
type="file-preview"
iqserDisableStopPropagation
></redaction-file-actions>
<iqser-circle-button
(action)="getTables()"
*allow="roles.getTables"
[icon]="'red:csv'"
[tooltip]="'file-preview.get-tables' | translate"
class="ml-2"
iqserDisableStopPropagation
></iqser-circle-button>
<iqser-circle-button
(action)="toggleFullScreen()"
[attr.help-mode-key]="'editor_full_screen'"
[icon]="fullScreen ? 'red:exit-fullscreen' : 'red:fullscreen'"
[tooltip]="'file-preview.fullscreen' | translate"
class="ml-2"
iqserDisableStopPropagation
></iqser-circle-button>
<!-- Dev Mode Features-->
<iqser-circle-button
(action)="downloadOriginalFile(file)"
*ngIf="isIqserDevMode"
[tooltip]="'file-preview.download-original-file' | translate"
[type]="circleButtonTypes.primary"
class="ml-8"
icon="iqser:download"
iqserDisableStopPropagation
></iqser-circle-button>
<!-- End Dev Mode Features-->
<iqser-circle-button
*ngIf="!fullScreen"
[attr.help-mode-key]="'editor_close'"
[routerLink]="state.dossier().routerLink"
[tooltip]="'common.close' | translate"
class="ml-8"
icon="iqser:close"
iqserDisableStopPropagation
></iqser-circle-button>
</div>
</div>
<redaction-file-header [file]="file"></redaction-file-header>
<div class="overlay-shadow"></div>
<div class="content-inner">
<div class="content-container">
<!-- Here comes PDF Viewer-->
<redaction-structured-component-management
*ngIf="isDocumine"
[file]="file"
[dictionaries]="state.dictionaries"
></redaction-structured-component-management>
</div>
<div class="right-container">
<redaction-file-preview-right-container [iqserDisableStopPropagation]="state.isEditingReviewer()"></redaction-file-preview-right-container>
<div class="right-container" [class.documine-container]="isDocumine">
<redaction-file-preview-right-container
[iqserDisableStopPropagation]="state.isEditingReviewer()"
></redaction-file-preview-right-container>
</div>
</div>
</section>

View File

@ -1,20 +1,3 @@
.vertical-line {
width: 1px;
height: 30px;
background-color: var(--iqser-separator);
margin: 0 16px;
}
.page-header {
max-width: 100vw;
}
.actions-container {
display: flex;
justify-content: flex-end;
align-items: center;
}
.content-inner {
position: absolute;
}
@ -25,8 +8,8 @@
.right-container {
padding: 0;
width: 350px;
min-width: 350px;
width: var(--workload-width);
min-width: var(--workload-width);
position: relative;
display: flex;
flex-direction: column;
@ -54,6 +37,10 @@
display: flex;
flex: 1;
}
&.documine-container {
width: 70%;
}
}
.analysis-progress {

View File

@ -1,18 +1,4 @@
import {
AfterViewInit,
ChangeDetectorRef,
Component,
computed,
effect,
ElementRef,
HostListener,
NgZone,
OnDestroy,
OnInit,
TemplateRef,
ViewChild,
} from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { ChangeDetectorRef, Component, effect, NgZone, OnDestroy, OnInit, TemplateRef, ViewChild } from '@angular/core';
import { ActivatedRouteSnapshot, NavigationExtras, Router } from '@angular/router';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { ComponentCanDeactivate } from '@guards/can-deactivate.guard';
@ -23,16 +9,13 @@ import {
CustomError,
ErrorService,
getConfig,
HelpModeService,
IConfirmationDialogData,
IqserDialog,
IqserPermissionsService,
isIqserDevMode,
LoadingService,
Toaster,
} from '@iqser/common-ui';
import { copyLocalStorageFiltersValues, FilterService, NestedFilter, processFilters } from '@iqser/common-ui/lib/filtering';
import { AutoUnsubscribe, Bind, bool, Debounce, List, OnAttach, OnDetach } from '@iqser/common-ui/lib/utils';
import { AutoUnsubscribe, Bind, bool, List, OnAttach, OnDetach } from '@iqser/common-ui/lib/utils';
import { AnnotationWrapper } from '@models/file/annotation.wrapper';
import { ManualRedactionEntryTypes, ManualRedactionEntryWrapper } from '@models/file/manual-redaction-entry.wrapper';
import { Dictionary, File, ViewModes } from '@red/domain';
@ -46,8 +29,6 @@ import { PermissionsService } from '@services/permissions.service';
import { ReanalysisService } from '@services/reanalysis.service';
import { Roles } from '@users/roles';
import { PreferencesKeys, UserPreferenceService } from '@users/user-preference.service';
import { saveAs } from 'file-saver';
import JSZip from 'jszip';
import { NGXLogger } from 'ngx-logger';
import { combineLatest, first, firstValueFrom, Observable, of, pairwise } from 'rxjs';
import { catchError, filter, map, startWith, switchMap, tap } from 'rxjs/operators';
@ -73,11 +54,8 @@ import { ManualRedactionService } from './services/manual-redaction.service';
import { PdfProxyService } from './services/pdf-proxy.service';
import { SkippedService } from './services/skipped.service';
import { StampService } from './services/stamp.service';
import { TablesService } from './services/tables.service';
import { ViewModeService } from './services/view-mode.service';
import { ALL_HOTKEYS } from './utils/constants';
import { RedactTextData } from './utils/dialog-types';
import { AnnotationActionsService } from './services/annotation-actions.service';
import { MultiSelectService } from './services/multi-select.service';
@Component({
@ -85,26 +63,18 @@ import { MultiSelectService } from './services/multi-select.service';
styleUrls: ['./file-preview-screen.component.scss'],
providers: filePreviewScreenProviders,
})
export class FilePreviewScreenComponent
extends AutoUnsubscribe
implements AfterViewInit, OnInit, OnDestroy, OnAttach, OnDetach, ComponentCanDeactivate
{
export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnInit, OnDestroy, OnAttach, OnDetach, ComponentCanDeactivate {
readonly circleButtonTypes = CircleButtonTypes;
readonly roles = Roles;
fullScreen = false;
readonly fileId = this.state.fileId;
readonly dossierId = this.state.dossierId;
readonly lastAssignee = computed(() => this.getLastAssignee());
width: number;
readonly isIqserDevMode = isIqserDevMode();
@ViewChild('annotationFilterTemplate', {
read: TemplateRef,
static: false,
})
private readonly _filterTemplate: TemplateRef<unknown>;
#loadAllAnnotationsEnabled = false;
@ViewChild('actionsWrapper', { static: false }) private readonly _actionsWrapper: ElementRef;
readonly #isDocumine = getConfig().IS_DOCUMINE;
protected readonly isDocumine = getConfig().IS_DOCUMINE;
constructor(
readonly pdf: PdfViewer,
@ -113,7 +83,6 @@ export class FilePreviewScreenComponent
readonly userPreferenceService: UserPreferenceService,
readonly pdfProxyService: PdfProxyService,
readonly configService: ConfigService,
private readonly _iqserPermissionsService: IqserPermissionsService,
private readonly _listingService: AnnotationsListingService,
private readonly _router: Router,
private readonly _ngZone: NgZone,
@ -142,11 +111,7 @@ export class FilePreviewScreenComponent
private readonly _filesService: FilesService,
private readonly _fileManagementService: FileManagementService,
private readonly _readableRedactionsService: ReadableRedactionsService,
private readonly _helpModeService: HelpModeService,
private readonly _dossierTemplatesService: DossierTemplatesService,
private readonly _dialog: MatDialog,
private readonly _tablesService: TablesService,
private readonly _annotationActionsService: AnnotationActionsService,
private readonly _multiSelectService: MultiSelectService,
) {
super();
@ -216,12 +181,6 @@ export class FilePreviewScreenComponent
);
}
getLastAssignee() {
const { isApproved, lastReviewer, lastApprover } = this.state.file();
const isRss = this._iqserPermissionsService.has(this.roles.getRss);
return isApproved ? (isRss ? lastReviewer : lastApprover) : lastReviewer;
}
deleteEarmarksOnViewChange$() {
const isChangingFromEarmarksViewMode$ = this._viewModeService.viewMode$.pipe(
pairwise(),
@ -298,24 +257,15 @@ export class FilePreviewScreenComponent
this._viewerHeaderService.resetCompareButtons();
this._viewerHeaderService.enableLoadAllAnnotations(); // Reset the button state (since the viewer is reused between files)
super.ngOnDetach();
document.documentElement.removeEventListener('fullscreenchange', this.fullscreenListener);
this.pdf.instance.UI.hotkeys.off('esc');
this._changeRef.markForCheck();
}
ngOnDestroy() {
document.documentElement.removeEventListener('fullscreenchange', this.fullscreenListener);
this.pdf.instance.UI.hotkeys.off('esc');
super.ngOnDestroy();
}
@Bind()
fullscreenListener() {
if (!document.fullscreenElement) {
this.fullScreen = false;
}
}
@Bind()
handleEscInsideViewer($event: KeyboardEvent) {
$event.preventDefault();
@ -352,6 +302,7 @@ export class FilePreviewScreenComponent
}
async ngOnInit(): Promise<void> {
document.getElementById('viewer').classList.add(this.isDocumine ? 'documine-viewer' : 'redaction-viewer');
const file = this.state.file();
if (!file) {
@ -368,19 +319,10 @@ export class FilePreviewScreenComponent
this.pdfProxyService.configureElements();
this.#restoreOldFilters();
document.documentElement.addEventListener('fullscreenchange', this.fullscreenListener);
this.pdf.instance.UI.hotkeys.on('esc', this.handleEscInsideViewer);
this.#openComponentLogDialogIfDefault();
this._viewerHeaderService.resetLayers();
}
ngAfterViewInit() {
const _observer = new ResizeObserver((entries: ResizeObserverEntry[]) => {
this._updateItemWidth(entries[0]);
});
_observer.observe(this._actionsWrapper.nativeElement);
}
openManualAnnotationDialog(manualRedactionEntryWrapper: ManualRedactionEntryWrapper) {
const file = this.state.file();
@ -406,68 +348,6 @@ export class FilePreviewScreenComponent
);
}
toggleFullScreen() {
this.fullScreen = !this.fullScreen;
if (this.fullScreen) {
this.#openFullScreen();
} else {
this.closeFullScreen();
}
}
@HostListener('document:keyup', ['$event'])
handleKeyEvent($event: KeyboardEvent) {
if (this._router.url.indexOf('/file/') < 0) {
return;
}
if (!ALL_HOTKEYS.includes($event.key) || this._dialog.openDialogs.length) {
return;
}
if (['Escape'].includes($event.key)) {
$event.preventDefault();
if (this._annotationManager.resizingAnnotationId) {
const resizedAnnotation = this._fileDataService
.annotations()
.find(annotation => annotation.id === this._annotationManager.resizingAnnotationId);
this._annotationActionsService.cancelResize(resizedAnnotation).then();
}
if (this._annotationManager.selected.length) {
this._annotationManager.deselectAll();
}
if (this._multiSelectService.active()) {
this._multiSelectService.deactivate();
}
this.fullScreen = false;
this.closeFullScreen();
this._changeRef.markForCheck();
}
if (!$event.ctrlKey && !$event.metaKey && ['f', 'F'].includes($event.key)) {
// if you type in an input, don't toggle full-screen
if ($event.target instanceof HTMLInputElement || $event.target instanceof HTMLTextAreaElement) {
return;
}
this.toggleFullScreen();
return;
}
if (['h', 'H'].includes($event.key)) {
if ($event.target instanceof HTMLInputElement || $event.target instanceof HTMLTextAreaElement) {
return;
}
this._ngZone.run(() => {
window.focus();
this._helpModeService.activateHelpMode(false);
});
return;
}
}
async viewerReady(pageNumber?: string) {
if (pageNumber) {
const file = this.state.file();
@ -489,21 +369,6 @@ export class FilePreviewScreenComponent
this._changeRef.markForCheck();
}
closeFullScreen() {
if (!!document.fullscreenElement && document.exitFullscreen) {
document.exitFullscreen().then();
}
}
async downloadOriginalFile({ cacheIdentifier, dossierId, fileId, filename }: File) {
const originalFile = this._fileManagementService.downloadOriginal(dossierId, fileId, 'response', cacheIdentifier);
download(await firstValueFrom(originalFile), filename);
}
openComponentLogView() {
this._dialogService.openDialog('componentLog', { file: this.state.file(), dictionaries: this.state.dictionaries });
}
loadAnnotations$() {
const annotations$ = this._fileDataService.annotations$.pipe(
startWith([] as AnnotationWrapper[]),
@ -549,28 +414,6 @@ export class FilePreviewScreenComponent
return this.#cleanupAndRedrawAnnotations(annotationsToDraw);
}
async getTables() {
this._loadingService.start();
const currentPage = this.pdf.currentPage();
const tables = await this._tablesService.get(this.state.dossierId, this.state.fileId, this.pdf.currentPage());
await this._annotationDrawService.drawTables(tables, currentPage, this.state.dossierTemplateId);
const filename = this.state.file().filename;
const zip = new JSZip();
tables.forEach((t, index) => {
const blob = new Blob([atob(t.csvAsBytes)], {
type: 'text/csv;charset=utf-8',
});
zip.file(filename + '_page' + currentPage + '_table' + (index + 1) + '.csv', blob);
});
saveAs(await zip.generateAsync({ type: 'blob' }), filename + '_tables.zip');
this._loadingService.stop();
}
async #openRedactTextDialog(manualRedactionEntryWrapper: ManualRedactionEntryWrapper) {
const file = this.state.file();
@ -594,12 +437,6 @@ export class FilePreviewScreenComponent
return firstValueFrom(addAndReload$.pipe(catchError(() => of(undefined))));
}
@Debounce(30)
private _updateItemWidth(entry: ResizeObserverEntry): void {
this.width = entry.contentRect.width;
this._changeRef.detectChanges();
}
#getAnnotationsToDraw(oldAnnotations: AnnotationWrapper[], newAnnotations: AnnotationWrapper[]) {
const currentPage = this.pdf.currentPage();
const currentPageAnnotations = this._annotationManager.get(a => a.getPageNumber() === currentPage);
@ -733,10 +570,6 @@ export class FilePreviewScreenComponent
.pipe(tap(() => this.#handleDeletedFile()))
.subscribe();
this.addActiveScreenSubscription = this._documentViewer.keyUp$.subscribe($event => {
this.handleKeyEvent($event);
});
this.addActiveScreenSubscription = this.#earmarks$.subscribe();
this.addActiveScreenSubscription = this.deleteEarmarksOnViewChange$().subscribe();
@ -867,13 +700,6 @@ export class FilePreviewScreenComponent
});
}
#openFullScreen() {
const documentElement = document.documentElement;
if (documentElement.requestFullscreen) {
documentElement.requestFullscreen().then();
}
}
#navigateToDossier() {
this._logger.info('Navigating to ', this.state.dossier().dossierName);
return this._router.navigate([this.state.dossier().routerLink]);
@ -900,14 +726,8 @@ export class FilePreviewScreenComponent
});
}
#openComponentLogDialogIfDefault() {
if (this.permissionsService.canViewRssDialog() && this.userPreferenceService.getOpenScmDialogByDefault()) {
this.openComponentLogView();
}
}
#getRedactTextDialog(data: RedactTextData) {
if (this.#isDocumine) {
if (this.isDocumine) {
return this._iqserDialog.openDefault(AddAnnotationDialogComponent, { data });
}

View File

@ -72,6 +72,10 @@ import { ManualRedactionService } from './services/manual-redaction.service';
import { TablesService } from './services/tables.service';
import { SelectedAnnotationsTableComponent } from './components/selected-annotations-table/selected-annotations-table.component';
import { SelectedAnnotationsListComponent } from './components/selected-annotations-list/selected-annotations-list.component';
import { FileHeaderComponent } from './components/file-header/file-header.component';
import { DocumineExportComponent } from './components/documine-export/documine-export.component';
import { StructuredComponentManagementComponent } from './components/structured-component-management/structured-component-management.component';
import { EditableStructuredComponentValueComponent } from './components/editable-structured-component-value/editable-structured-component-value.component';
const routes: IqserRoutes = [
{
@ -121,6 +125,9 @@ const components = [
FilePreviewScreenComponent,
FilePreviewRightContainerComponent,
ReadonlyBannerComponent,
FileHeaderComponent,
DocumineExportComponent,
StructuredComponentManagementComponent,
];
@NgModule({
@ -156,6 +163,7 @@ const components = [
DisableStopPropagationDirective,
SelectedAnnotationsTableComponent,
SelectedAnnotationsListComponent,
EditableStructuredComponentValueComponent,
],
providers: [FilePreviewDialogService, ManualRedactionService, DocumentUnloadedGuard, TablesService],
})

View File

@ -0,0 +1,43 @@
import { Injectable } from '@angular/core';
import { ComponentLogEntry } from '@red/domain';
import { INestedFilter, NestedFilter } from '@common-ui/filtering';
@Injectable()
export class ComponentLogFilterService {
filterGroups(entities: ComponentLogEntry[]) {
const allDistinctComponentLogs = new Set<string>();
entities?.forEach(entry => allDistinctComponentLogs.add(entry.name));
const componentLogFilters = [...allDistinctComponentLogs].map(
id =>
new NestedFilter({
id: id,
label: id.replaceAll('_', ' '),
}),
);
return [
{
slug: 'componentLogFilters',
filters: componentLogFilters,
},
];
}
filterComponents(components: ComponentLogEntry[], filters: INestedFilter[]) {
const someFiltersChecked = !!filters.find(f => f.checked);
if (!someFiltersChecked) {
return components;
}
const checkedFiltersIds = filters.reduce((ids, f) => {
if (f.checked) {
ids.push(f.id);
}
return ids;
}, []);
return components.filter(c => checkedFiltersIds.includes(c.name));
}
}

View File

@ -6,16 +6,8 @@ import { DocumentInfoDialogComponent } from '../dialogs/document-info-dialog/doc
import { ForceAnnotationDialogComponent } from '../dialogs/force-redaction-dialog/force-annotation-dialog.component';
import { HighlightActionDialogComponent } from '../dialogs/highlight-action-dialog/highlight-action-dialog.component';
import { ManualAnnotationDialogComponent } from '../dialogs/manual-redaction-dialog/manual-annotation-dialog.component';
import { StructuredComponentManagementDialogComponent } from '../dialogs/structured-component-management-dialog/structured-component-management-dialog.component';
type DialogType =
| 'confirm'
| 'documentInfo'
| 'componentLog'
| 'changeLegalBasis'
| 'forceAnnotation'
| 'manualAnnotation'
| 'highlightAction';
type DialogType = 'confirm' | 'documentInfo' | 'changeLegalBasis' | 'forceAnnotation' | 'manualAnnotation' | 'highlightAction';
@Injectable()
export class FilePreviewDialogService extends DialogService<DialogType> {
@ -41,10 +33,6 @@ export class FilePreviewDialogService extends DialogService<DialogType> {
highlightAction: {
component: HighlightActionDialogComponent,
},
componentLog: {
component: StructuredComponentManagementDialogComponent,
dialogConfig: { width: '90vw' },
},
};
constructor(protected readonly _dialog: MatDialog) {

View File

@ -135,9 +135,11 @@ export class PdfProxyService {
}
configureElements() {
const hexColor = this._dictionariesMapService.get(this._state.dossierTemplateId, 'manual').hexColor;
const color = this._annotationDrawService.convertColor(hexColor);
this._documentViewer.setRectangleToolStyles(color);
const hexColor = this._dictionariesMapService.get(this._state.dossierTemplateId, 'manual')?.hexColor;
if (hexColor) {
const color = this._annotationDrawService.convertColor(hexColor);
this._documentViewer.setRectangleToolStyles(color);
}
}
#configureRectangleAnnotationPopup(annotation: Annotation) {

View File

@ -29,6 +29,7 @@ export class IconsModule {
'archive',
'arrow-up',
'arrow-down',
'arrow-right',
'assign',
'assign-me',
'attribute',
@ -41,6 +42,7 @@ export class IconsModule {
'denied',
'disable-analysis',
'double-chevron-right',
'draggable-dots',
'enable-analysis',
'enter',
'entries',

View File

@ -1,3 +1,3 @@
<redaction-compare-file-input></redaction-compare-file-input>
<redaction-paginator></redaction-paginator>
<redaction-paginator *ngIf="!isDocumine"></redaction-paginator>

View File

@ -1,8 +1,11 @@
import { Component } from '@angular/core';
import { getConfig } from '@iqser/common-ui';
@Component({
selector: 'redaction-pdf-viewer',
templateUrl: './pdf-viewer.component.html',
styleUrls: ['./pdf-viewer.component.scss'],
})
export class PdfViewerComponent {}
export class PdfViewerComponent {
protected readonly isDocumine = getConfig().IS_DOCUMINE;
}

View File

@ -275,6 +275,15 @@ export class ViewerHeaderService {
updateElements(): void {
this._pdf.instance?.UI.setHeaderItems(header => {
let deletedDividers = 0;
if (this.#isDocumine) {
const secondHeaderElement = header.getItems()[1] as IHeaderElement;
if (secondHeaderElement.type === 'divider') {
header.getItems().splice(1, 1);
deletedDividers = 1;
}
}
const enabledItems: IHeaderElement[] = [];
const groups: HeaderElementType[][] = [
[HeaderElements.COMPARE_BUTTON, HeaderElements.CLOSE_COMPARE_BUTTON],
@ -293,18 +302,19 @@ export class ViewerHeaderService {
groups.forEach(group => this.#pushGroup(enabledItems, group));
const loadAllAnnotationsButton = this.#buttons.get(HeaderElements.LOAD_ALL_ANNOTATIONS);
let startButtons = 11;
let deleteCount = 15;
let startButtons = 10 - deletedDividers;
let deleteCount = 14 - deletedDividers;
if (this.#isEnabled(HeaderElements.LOAD_ALL_ANNOTATIONS)) {
if (!header.getItems().includes(loadAllAnnotationsButton)) {
header.get('leftPanelButton').insertAfter(loadAllAnnotationsButton);
}
startButtons = 12;
deleteCount = 16;
startButtons = 11 - deletedDividers;
deleteCount = 15 - deletedDividers;
} else {
header.delete(HeaderElements.LOAD_ALL_ANNOTATIONS);
}
header.getItems().splice(startButtons, header.getItems().length - deleteCount, ...enabledItems);
});

View File

@ -82,6 +82,4 @@ export class EditDictionaryDialogComponent extends IqserDialogComponent<EditDict
this._loadingService.stop();
}
protected readonly iconButtonTypes = IconButtonTypes;
}

View File

@ -4,8 +4,9 @@ import { catchError, map, tap } from 'rxjs/operators';
import { Observable, of } from 'rxjs';
import { HttpHeaders } from '@angular/common/http';
import { saveAs } from 'file-saver';
import { ComponentDetails, ComponentLogEntry, IComponentLogData, IComponentLogEntry, IFile } from '@red/domain';
import { ComponentDetails, ComponentLogEntry, Dictionary, IComponentLogData, IComponentLogEntry, IFile } from '@red/domain';
import { mapEach } from '@common-ui/utils';
import { FilePreviewStateService } from '../../modules/file-preview/services/file-preview-state.service';
@Injectable({ providedIn: 'root' })
export class ComponentLogService extends GenericService<void> {
@ -34,21 +35,32 @@ export class ComponentLogService extends GenericService<void> {
);
}
getComponentLogData(dossierTemplateId: string, dossierId: string, fileId: string): Observable<ComponentLogEntry[]> {
getComponentLogData(
dossierTemplateId: string,
dossierId: string,
fileId: string,
dictionaries: Dictionary[],
): Observable<ComponentLogEntry[]> {
return this.#componentLogRequest(dossierTemplateId, dossierId, fileId).pipe(
map(data => data.componentDetails),
catchError(() => of({} as ComponentDetails)),
map(componentDetails => this.#mapComponentDetails(componentDetails)),
mapEach(log => new ComponentLogEntry(log)),
map(log => this.#updateDisplayValue(log, dictionaries)),
);
}
override(dossierId: string, fileId: string, componentOverrides: Record<string, string>): Observable<void> {
return this._post({ componentOverrides }, `componentLog/override/${dossierId}/${fileId}`);
override(dossierTemplateId: string, dossierId: string, fileId: string, componentLogEntry: IComponentLogEntry) {
return this._http.post(
`/api/dossier-templates/${dossierTemplateId}/dossiers/${dossierId}/files/${fileId}/overrides`,
componentLogEntry,
);
}
revertOverride(dossierId: string, fileId: string, components: string[]): Observable<void> {
return this._post({ components }, `componentLog/override/revert/${dossierId}/${fileId}`);
revertOverride(dossierTemplateId: string, dossierId: string, fileId: string, components: string[]) {
return this._http.post(`/api/dossier-templates/${dossierTemplateId}/dossiers/${dossierId}/files/${fileId}/overrides/revert`, {
components,
});
}
exportJSON(dossierTemplateId: string, dossierId: string, file?: IFile): Observable<IComponentLogData> {
@ -91,4 +103,17 @@ export class ComponentLogService extends GenericService<void> {
#mapComponentDetails(componentDetails: ComponentDetails): IComponentLogEntry[] {
return Object.keys(componentDetails).reduce((res, key) => (res.push(componentDetails[key]), res), []);
}
#updateDisplayValue(componentLogs: ComponentLogEntry[], dictionaries: Dictionary[]): ComponentLogEntry[] {
for (const componentLog of componentLogs) {
for (const componentValue of componentLog.componentValues) {
for (const reference of componentValue.entityReferences) {
const foundDictionary =
dictionaries.find(dict => dict.type === reference.type) ?? ({ label: reference.type } as Dictionary);
reference.displayValue = foundDictionary.label;
}
}
}
return componentLogs;
}
}

View File

@ -1,5 +1,5 @@
import { handleCheckedValue, INestedFilter } from '@iqser/common-ui/lib/filtering';
import { Dossier, File, User, UserType } from '@red/domain';
import { ComponentLogEntry, Dossier, File, User, UserType } from '@red/domain';
export function handleFilterDelta(oldFilters: INestedFilter[], newFilters: INestedFilter[], allFilters: INestedFilter[]) {
const newFiltersDelta = {};

View File

@ -515,7 +515,7 @@
"tooltip": "",
"xml": ""
},
"component-log-dialog": {
"component-management": {
"actions": {
"cancel-edit": "Abbrechen",
"close": "Close",
@ -526,14 +526,11 @@
"save": "Save",
"undo": "Undo to: {value}"
},
"annotations": "<strong>{type}</strong> found on page {page} by rule #{ruleNumber}",
"components": "",
"table-header": {
"annotation-references": "Annotation references",
"component": "Component",
"transformation-rule": "Transformation rule",
"value": "Value"
},
"title": "Component view"
"component": "",
"value": ""
}
},
"component-mappings-screen": {
"action": {
@ -826,6 +823,12 @@
"save": "Dokumenteninformation speichern",
"title": "Datei-Attribute anlegen"
},
"documine-export": {
"document": "",
"document-tooltip": "",
"export": "",
"export-tooltip": ""
},
"dossier-attribute-types": {
"date": "Datum",
"image": "Bild",
@ -1506,7 +1509,6 @@
"no-data": {
"title": "Auf dieser Seite gibt es keine Anmerkungen."
},
"open-rss-view": "Open Structured Component Management View",
"quick-nav": {
"jump-first": "Zur ersten Seite springen",
"jump-last": "Zur letzten Seite springen"
@ -2249,6 +2251,15 @@
"header": "Resize {type}"
}
},
"revert-value-dialog": {
"actions": {
"cancel": "",
"revert": ""
},
"current-values": "",
"original-values": "",
"title": ""
},
"roles": {
"inactive": "Inaktiv",
"manager-admin": "Manager & Admin",
@ -2542,4 +2553,4 @@
}
},
"yesterday": "Gestern"
}
}

View File

@ -390,6 +390,7 @@
"annotation": {
"pending": "(Pending analysis)"
},
"annotations": "Annotations",
"archived-dossiers-listing": {
"no-data": {
"title": "No archived dossiers."
@ -515,25 +516,20 @@
"tooltip": "",
"xml": ""
},
"component-log-dialog": {
"component-management": {
"actions": {
"cancel-edit": "Cancel",
"close": "Close",
"display-by-default": "Display by default when opening documents",
"add": "Add",
"cancel": "Cancel",
"delete": "Remove value",
"edit": "Edit",
"export-json": "Export JSON",
"export-xml": "Export XML",
"save": "Save",
"undo": "Undo to: {value}"
"undo": "Undo"
},
"annotations": "<strong>{type}</strong> found on page {page} by rule #{ruleNumber}",
"components": "Components",
"table-header": {
"annotation-references": "Annotation references",
"component": "Component",
"transformation-rule": "Transformation rule",
"value": "Value"
},
"title": "Component view"
}
},
"component-mappings-screen": {
"action": {
@ -826,6 +822,12 @@
"save": "Save document info",
"title": "Enter file attributes"
},
"documine-export": {
"document": "Document",
"document-tooltip": "Document",
"export": "Export",
"export-tooltip": "Export"
},
"dossier-attribute-types": {
"date": "Date",
"image": "Image",
@ -1506,7 +1508,6 @@
"no-data": {
"title": "There have been no changes to this page."
},
"open-rss-view": "Open Structured Component Management View",
"quick-nav": {
"jump-first": "Jump to first page",
"jump-last": "Jump to last page"
@ -2249,6 +2250,15 @@
"header": "Resize {type}"
}
},
"revert-value-dialog": {
"actions": {
"cancel": "Cancel",
"revert": "Revert to original values"
},
"current-values": "Current values",
"original-values": "Original values",
"title": "Revert to the original values?"
},
"roles": {
"inactive": "Inactive",
"manager-admin": "Manager & admin",
@ -2542,4 +2552,4 @@
}
},
"yesterday": "Yesterday"
}
}

View File

@ -515,25 +515,20 @@
"tooltip": "Component download",
"xml": "Download as XML"
},
"component-log-dialog": {
"component-management": {
"actions": {
"cancel-edit": "Cancel",
"close": "Close",
"display-by-default": "Display by default when opening documents",
"edit": "Edit",
"export-json": "Export JSON",
"export-xml": "Export XML",
"save": "Save",
"undo": "Undo"
"add": "",
"cancel": "",
"delete": "",
"edit": "",
"save": "",
"undo": ""
},
"annotations": "<strong>{type}</strong> found on page {page} by rule #{ruleNumber}",
"components": "",
"table-header": {
"annotation-references": "Annotation references",
"component": "Component",
"transformation-rule": "Transformation rule",
"value": "Value"
},
"title": "Structured Component Management"
"component": "",
"value": ""
}
},
"component-mappings-screen": {
"action": {
@ -826,6 +821,12 @@
"save": "Dokumenteninformation speichern",
"title": "Datei-Attribute anlegen"
},
"documine-export": {
"document": "",
"document-tooltip": "",
"export": "",
"export-tooltip": ""
},
"dossier-attribute-types": {
"date": "Datum",
"image": "Bild",
@ -1506,7 +1507,6 @@
"no-data": {
"title": "Auf dieser Seite gibt es keine Anmerkungen."
},
"open-rss-view": "Open component view",
"quick-nav": {
"jump-first": "Zur ersten Seite springen",
"jump-last": "Zur letzten Seite springen"
@ -2249,6 +2249,15 @@
"header": "Resize {type}"
}
},
"revert-value-dialog": {
"actions": {
"cancel": "",
"revert": ""
},
"current-values": "",
"original-values": "",
"title": ""
},
"roles": {
"inactive": "Inaktiv",
"manager-admin": "Manager & admin",
@ -2542,4 +2551,4 @@
}
},
"yesterday": "Gestern"
}
}

View File

@ -390,6 +390,7 @@
"annotation": {
"pending": "(Pending analysis)"
},
"annotations": "Annotations",
"archived-dossiers-listing": {
"no-data": {
"title": "No archived dossiers."
@ -515,25 +516,20 @@
"tooltip": "Component download",
"xml": "Download as XML"
},
"component-log-dialog": {
"component-management": {
"actions": {
"cancel-edit": "Cancel",
"close": "Close",
"display-by-default": "Display by default when opening documents",
"add": "Add",
"cancel": "Cancel",
"delete": "Remove value",
"edit": "Edit",
"export-json": "Export JSON",
"export-xml": "Export XML",
"save": "Save",
"undo": "Undo to: {value}"
"undo": "Undo"
},
"annotations": "<strong>{type}</strong> found on page {page} by rule #{ruleNumber}",
"components": "Components",
"table-header": {
"annotation-references": "Annotation references",
"component": "Component",
"transformation-rule": "Transformation rule",
"value": "Value"
},
"title": "Component view"
}
},
"component-mappings-screen": {
"action": {
@ -826,6 +822,12 @@
"save": "Save document info",
"title": "Enter file attributes"
},
"documine-export": {
"document": "Document",
"document-tooltip": "Document",
"export": "Export",
"export-tooltip": "Export"
},
"dossier-attribute-types": {
"date": "Date",
"image": "Image",
@ -1506,7 +1508,6 @@
"no-data": {
"title": "There have been no changes to this page."
},
"open-rss-view": "Open component view",
"quick-nav": {
"jump-first": "Jump to first page",
"jump-last": "Jump to last page"
@ -2249,6 +2250,15 @@
"header": "Resize {type}"
}
},
"revert-value-dialog": {
"actions": {
"cancel": "Cancel",
"revert": "Revert to original values"
},
"current-values": "Current values",
"original-values": "Original values",
"title": "Revert to the original values?"
},
"roles": {
"inactive": "Inactive",
"manager-admin": "Manager & admin",
@ -2542,4 +2552,4 @@
}
},
"yesterday": "Yesterday"
}
}

View File

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" x="0px" y="0px" viewBox="0 0 100 100" style="enable-background:new 0 0 100 100;" xml:space="preserve">
<g>
<path d="M31.1,85c0.9,0.9,2,1.3,3.2,1.3s2.3-0.4,3.2-1.3l31.8-31.8c0.8-0.8,1.3-2,1.3-3.2s-0.5-2.3-1.3-3.2L37.4,15.5 c-1.8-1.7-4.6-1.7-6.4,0.1c-1.7,1.8-1.7,4.6,0.1,6.4L59.6,50L31.1,78.6C29.3,80.4,29.3,83.2,31.1,85z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 450 B

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg fill="#000000" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
width="600px" height="800px" viewBox="0 0 276.167 276.167"
xml:space="preserve">
<g>
<g>
<path d="M33.144,2.471C15.336,2.471,0.85,16.958,0.85,34.765s14.48,32.293,32.294,32.293s32.294-14.486,32.294-32.293
S50.951,2.471,33.144,2.471z"/>
<path d="M137.663,2.471c-17.807,0-32.294,14.487-32.294,32.294s14.487,32.293,32.294,32.293c17.808,0,32.297-14.486,32.297-32.293
S155.477,2.471,137.663,2.471z"/>
<path d="M32.3,170.539c17.807,0,32.297-14.483,32.297-32.293c0-17.811-14.49-32.297-32.297-32.297S0,120.436,0,138.246
C0,156.056,14.493,170.539,32.3,170.539z"/>
<path d="M136.819,170.539c17.804,0,32.294-14.483,32.294-32.293c0-17.811-14.478-32.297-32.294-32.297
c-17.813,0-32.294,14.486-32.294,32.297C104.525,156.056,119.012,170.539,136.819,170.539z"/>
<path d="M33.039,209.108c-17.807,0-32.3,14.483-32.3,32.294c0,17.804,14.493,32.293,32.3,32.293s32.293-14.482,32.293-32.293
S50.846,209.108,33.039,209.108z"/>
<path d="M137.564,209.108c-17.808,0-32.3,14.483-32.3,32.294c0,17.804,14.487,32.293,32.3,32.293
c17.804,0,32.293-14.482,32.293-32.293S155.368,209.108,137.564,209.108z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -55,3 +55,11 @@ button.Button[data-element='LOAD_ALL_ANNOTATIONS'] > img[src='/ui/assets/icons/g
.HeaderItems .Button.active > img {
filter: invert(19%) sepia(100%) saturate(791%) hue-rotate(175deg) brightness(89%) contrast(85%);
}
.MainHeader {
height: 36px !important;
}
.view-header-border {
background: rgba(226, 228, 233, 0.9);
}

View File

@ -163,12 +163,15 @@ $dark-accent-10: darken(vars.$accent, 10%);
body {
--workload-width: 350px;
--documine-workload-content-width: 200px;
--structured-component-management-width: 30%;
--qiuck-navigation-width: 61px;
--iqser-app-name-font-family: OpenSans Extrabold, sans-serif;
--iqser-app-name-font-size: 13px;
--iqser-logo-size: 28px;
}
#viewer {
.redaction-viewer {
visibility: hidden;
width: calc(100% - var(--workload-width));
height: calc(100% - calc(var(--iqser-top-bar-height) + 50px));
@ -176,3 +179,14 @@ body {
left: 0;
position: absolute;
}
.documine-viewer {
visibility: hidden;
width: calc(
100% - var(--structured-component-management-width) - var(--documine-workload-content-width) - var(--qiuck-navigation-width) - 3px
);
height: calc(100% - calc(var(--iqser-top-bar-height) + 50px));
bottom: 0;
right: calc(var(--qiuck-navigation-width) + 2px);
position: absolute;
}

@ -1 +1 @@
Subproject commit 04eaca1600149e3f4e803fd2334c25465197dbf6
Subproject commit 748cce403285c97e14cd3115a828c94c9d5d4520

View File

@ -6,16 +6,19 @@ export interface IComponentLogEntry {
name: string;
originalKey: string;
componentValues: IComponentValue[];
overridden?: boolean;
}
export class ComponentLogEntry implements IComponentLogEntry {
readonly name: string;
readonly originalKey: string;
readonly componentValues: ComponentValue[];
readonly overridden: boolean;
constructor(entry: IComponentLogEntry) {
this.name = entry.name.replaceAll('_', ' ');
this.name = entry.name;
this.originalKey = entry.name;
this.componentValues = entry.componentValues;
this.overridden = !!entry.overridden;
}
}

View File

@ -1,17 +1,17 @@
export interface EntityReference {
readonly id: string;
readonly type: string;
readonly entityRuleId: string;
readonly page: number;
id: string;
type: string;
entityRuleId: string;
page: number;
displayValue?: string;
}
export interface IComponentValue {
readonly value: string;
readonly originalValue: string;
readonly valueDescription: string;
readonly componentRuleId: string;
readonly entityReferences: EntityReference[];
value: string;
originalValue: string;
valueDescription: string;
componentRuleId: string;
entityReferences: EntityReference[];
}
export class ComponentValue implements IComponentValue {