diff --git a/apps/red-ui/src/app/modules/admin/screens/component-mappings/add-edit-component-mapping-dialog/add-edit-component-mapping-dialog.component.ts b/apps/red-ui/src/app/modules/admin/screens/component-mappings/add-edit-component-mapping-dialog/add-edit-component-mapping-dialog.component.ts index 680565a72..4175a68f4 100644 --- a/apps/red-ui/src/app/modules/admin/screens/component-mappings/add-edit-component-mapping-dialog/add-edit-component-mapping-dialog.component.ts +++ b/apps/red-ui/src/app/modules/admin/screens/component-mappings/add-edit-component-mapping-dialog/add-edit-component-mapping-dialog.component.ts @@ -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; diff --git a/apps/red-ui/src/app/modules/file-preview/components/annotation-card/annotation-card.component.html b/apps/red-ui/src/app/modules/file-preview/components/annotation-card/annotation-card.component.html index 50a482417..e017c7e41 100644 --- a/apps/red-ui/src/app/modules/file-preview/components/annotation-card/annotation-card.component.html +++ b/apps/red-ui/src/app/modules/file-preview/components/annotation-card/annotation-card.component.html @@ -6,7 +6,7 @@ class="mt-6 mr-10" > -
+
{{ annotation.superTypeLabel | translate }}   @@ -15,7 +15,7 @@
-
+
{{ annotation.descriptor | translate }}: diff --git a/apps/red-ui/src/app/modules/file-preview/components/annotation-card/annotation-card.component.scss b/apps/red-ui/src/app/modules/file-preview/components/annotation-card/annotation-card.component.scss index 79b889eaf..6be337063 100644 --- a/apps/red-ui/src/app/modules/file-preview/components/annotation-card/annotation-card.component.scss +++ b/apps/red-ui/src/app/modules/file-preview/components/annotation-card/annotation-card.component.scss @@ -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 { diff --git a/apps/red-ui/src/app/modules/file-preview/components/annotation-card/annotation-card.component.ts b/apps/red-ui/src/app/modules/file-preview/components/annotation-card/annotation-card.component.ts index 322593075..20c573ffd 100644 --- a/apps/red-ui/src/app/modules/file-preview/components/annotation-card/annotation-card.component.ts +++ b/apps/red-ui/src/app/modules/file-preview/components/annotation-card/annotation-card.component.ts @@ -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; diff --git a/apps/red-ui/src/app/modules/file-preview/components/annotations-list/annotations-list.component.html b/apps/red-ui/src/app/modules/file-preview/components/annotations-list/annotations-list.component.html index 1d960a7ab..c62243644 100644 --- a/apps/red-ui/src/app/modules/file-preview/components/annotations-list/annotations-list.component.html +++ b/apps/red-ui/src/app/modules/file-preview/components/annotations-list/annotations-list.component.html @@ -8,6 +8,7 @@ (click)="annotationClicked(annotation.item, $event)" [annotation]="annotation" [id]="'annotation-' + annotation.item.id" + [class.documine-wrapper]="isDocumine" > diff --git a/apps/red-ui/src/app/modules/file-preview/components/annotations-list/annotations-list.component.scss b/apps/red-ui/src/app/modules/file-preview/components/annotations-list/annotations-list.component.scss index 5f41248b8..1623da0aa 100644 --- a/apps/red-ui/src/app/modules/file-preview/components/annotations-list/annotations-list.component.scss +++ b/apps/red-ui/src/app/modules/file-preview/components/annotations-list/annotations-list.component.scss @@ -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; } diff --git a/apps/red-ui/src/app/modules/file-preview/components/annotations-list/annotations-list.component.ts b/apps/red-ui/src/app/modules/file-preview/components/annotations-list/annotations-list.component.ts index e1a7a033b..0ef4a6d48 100644 --- a/apps/red-ui/src/app/modules/file-preview/components/annotations-list/annotations-list.component.ts +++ b/apps/red-ui/src/app/modules/file-preview/components/annotations-list/annotations-list.component.ts @@ -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, diff --git a/apps/red-ui/src/app/modules/file-preview/components/documine-export/documine-export.component.html b/apps/red-ui/src/app/modules/file-preview/components/documine-export/documine-export.component.html new file mode 100644 index 000000000..7dc0226f5 --- /dev/null +++ b/apps/red-ui/src/app/modules/file-preview/components/documine-export/documine-export.component.html @@ -0,0 +1,17 @@ + + + + + + + + diff --git a/apps/red-ui/src/app/modules/file-preview/components/documine-export/documine-export.component.ts b/apps/red-ui/src/app/modules/file-preview/components/documine-export/documine-export.component.ts new file mode 100644 index 000000000..f4e5ad2cb --- /dev/null +++ b/apps/red-ui/src/app/modules/file-preview/components/documine-export/documine-export.component.ts @@ -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)); + } +} diff --git a/apps/red-ui/src/app/modules/file-preview/components/editable-structured-component-value/editable-structured-component-value.component.html b/apps/red-ui/src/app/modules/file-preview/components/editable-structured-component-value/editable-structured-component-value.component.html new file mode 100644 index 000000000..48037c021 --- /dev/null +++ b/apps/red-ui/src/app/modules/file-preview/components/editable-structured-component-value/editable-structured-component-value.component.html @@ -0,0 +1,64 @@ +
+
{{ entryLabel }}
+
+
+ + +
+
+ +
+
+
+ +
+ + +
+
+ +
+ +
+ +
+
+
+ +
+
+ + +
+
+
diff --git a/apps/red-ui/src/app/modules/file-preview/components/editable-structured-component-value/editable-structured-component-value.component.scss b/apps/red-ui/src/app/modules/file-preview/components/editable-structured-component-value/editable-structured-component-value.component.scss new file mode 100644 index 000000000..288ed4125 --- /dev/null +++ b/apps/red-ui/src/app/modules/file-preview/components/editable-structured-component-value/editable-structured-component-value.component.scss @@ -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); + } +} diff --git a/apps/red-ui/src/app/modules/file-preview/components/editable-structured-component-value/editable-structured-component-value.component.ts b/apps/red-ui/src/app/modules/file-preview/components/editable-structured-component-value/editable-structured-component-value.component.ts new file mode 100644 index 000000000..88122f1dd --- /dev/null +++ b/apps/red-ui/src/app/modules/file-preview/components/editable-structured-component-value/editable-structured-component-value.component.ts @@ -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(); + @Output() readonly revertOverride = new EventEmitter(); + + 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) { + moveItemInArray(this.entry.componentValues, event.previousIndex, event.currentIndex); + } + + parseName(name: string) { + return name.replaceAll('_', ' '); + } + + transformNewLines(value: string) { + return value.replace(/\n/g, '
'); + } + + 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; + } + } + } +} diff --git a/apps/red-ui/src/app/modules/file-preview/components/file-header/file-header.component.html b/apps/red-ui/src/app/modules/file-preview/components/file-header/file-header.component.html new file mode 100644 index 000000000..95882b63c --- /dev/null +++ b/apps/red-ui/src/app/modules/file-preview/components/file-header/file-header.component.html @@ -0,0 +1,81 @@ + diff --git a/apps/red-ui/src/app/modules/file-preview/components/file-header/file-header.component.scss b/apps/red-ui/src/app/modules/file-preview/components/file-header/file-header.component.scss new file mode 100644 index 000000000..2c45650f8 --- /dev/null +++ b/apps/red-ui/src/app/modules/file-preview/components/file-header/file-header.component.scss @@ -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; +} diff --git a/apps/red-ui/src/app/modules/file-preview/components/file-header/file-header.component.ts b/apps/red-ui/src/app/modules/file-preview/components/file-header/file-header.component.ts new file mode 100644 index 000000000..9e9870857 --- /dev/null +++ b/apps/red-ui/src/app/modules/file-preview/components/file-header/file-header.component.ts @@ -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(); + } +} diff --git a/apps/red-ui/src/app/modules/file-preview/components/file-workload/file-workload.component.html b/apps/red-ui/src/app/modules/file-preview/components/file-workload/file-workload.component.html index 2e1fabc17..3fb20fb62 100644 --- a/apps/red-ui/src/app/modules/file-preview/components/file-workload/file-workload.component.html +++ b/apps/red-ui/src/app/modules/file-preview/components/file-workload/file-workload.component.html @@ -1,50 +1,47 @@ -
-
- -
-
- - -
- {{ title() | translate }} + +
-
- - +
- + + +
+ {{ title() | translate }} +
+
+ + +
+
+
+
- - - - + + + + + +
@@ -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" >
- +
@@ -111,7 +106,7 @@
-
+
- + + - + +
@@ -231,3 +228,19 @@ iqserPreventDefault > + + + + + + + + + diff --git a/apps/red-ui/src/app/modules/file-preview/components/file-workload/file-workload.component.scss b/apps/red-ui/src/app/modules/file-preview/components/file-workload/file-workload.component.scss index 5ca2bfd51..86cb091fa 100644 --- a/apps/red-ui/src/app/modules/file-preview/components/file-workload/file-workload.component.scss +++ b/apps/red-ui/src/app/modules/file-preview/components/file-workload/file-workload.component.scss @@ -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); } diff --git a/apps/red-ui/src/app/modules/file-preview/components/file-workload/file-workload.component.ts b/apps/red-ui/src/app/modules/file-preview/components/file-workload/file-workload.component.ts index 612a81a33..a7cc66f3f 100644 --- a/apps/red-ui/src/app/modules/file-preview/components/file-workload/file-workload.component.ts +++ b/apps/red-ui/src/app/modules/file-preview/components/file-workload/file-workload.component.ts @@ -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(); 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; } } diff --git a/apps/red-ui/src/app/modules/file-preview/components/pages/pages.component.ts b/apps/red-ui/src/app/modules/file-preview/components/pages/pages.component.ts index 892636f9a..99e5dfd88 100644 --- a/apps/red-ui/src/app/modules/file-preview/components/pages/pages.component.ts +++ b/apps/red-ui/src/app/modules/file-preview/components/pages/pages.component.ts @@ -20,7 +20,6 @@ export class PagesComponent implements AfterViewInit { readonly #listingService = inject(AnnotationsListingService); protected readonly _pdf = inject(PdfViewer); @Input({ required: true }) pages: List; - @Input({ required: true }) displayedAnnotations: Map; readonly viewedPages$ = inject(ViewedPagesMapService).get$(this.#state.fileId); ngAfterViewInit() { diff --git a/apps/red-ui/src/app/modules/file-preview/components/structured-component-management/structured-component-management.component.html b/apps/red-ui/src/app/modules/file-preview/components/structured-component-management/structured-component-management.component.html new file mode 100644 index 000000000..fff85d262 --- /dev/null +++ b/apps/red-ui/src/app/modules/file-preview/components/structured-component-management/structured-component-management.component.html @@ -0,0 +1,26 @@ +
+ + +
+ +
+
+
+
{{ 'component-management.table-header.component' | translate }}
+
{{ 'component-management.table-header.value' | translate }}
+
+
+
+ +
+ +
+
+
diff --git a/apps/red-ui/src/app/modules/file-preview/components/structured-component-management/structured-component-management.component.scss b/apps/red-ui/src/app/modules/file-preview/components/structured-component-management/structured-component-management.component.scss new file mode 100644 index 000000000..ca68bc707 --- /dev/null +++ b/apps/red-ui/src/app/modules/file-preview/components/structured-component-management/structured-component-management.component.scss @@ -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; + } + } +} diff --git a/apps/red-ui/src/app/modules/file-preview/components/structured-component-management/structured-component-management.component.ts b/apps/red-ui/src/app/modules/file-preview/components/structured-component-management/structured-component-management.component.ts new file mode 100644 index 000000000..0b37379fb --- /dev/null +++ b/apps/red-ui/src/app/modules/file-preview/components/structured-component-management/structured-component-management.component.ts @@ -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; + + protected readonly componentLogData = signal(undefined); + protected readonly componentLogData$ = toObservable(this.componentLogData); + protected readonly openScmDialogByDefault = signal(this.userPreferences.getOpenScmDialogByDefault()); + protected readonly iconButtonTypes = IconButtonTypes; + protected displayedComponents$: Observable; + + 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 { + 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 { + 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); + } +} diff --git a/apps/red-ui/src/app/modules/file-preview/dialogs/docu-mine/revert-value-dialog/revert-value-dialog.component.html b/apps/red-ui/src/app/modules/file-preview/dialogs/docu-mine/revert-value-dialog/revert-value-dialog.component.html new file mode 100644 index 000000000..e46ec3998 --- /dev/null +++ b/apps/red-ui/src/app/modules/file-preview/dialogs/docu-mine/revert-value-dialog/revert-value-dialog.component.html @@ -0,0 +1,39 @@ +
+
+ +
+ + {{ value.valueDescription }} + +
+
+ + {{ 'revert-value-dialog.original-values' | translate }} + +
+

+ {{ index + 1 + '. ' + (value.originalValue ?? '') }} +

+
+
+
+ + {{ 'revert-value-dialog.current-values' | translate }} + +
+

+ {{ index + 1 + '. ' + value.value }} +

+
+
+
+
+ +
+ + +
+
+ + +
diff --git a/apps/red-ui/src/app/modules/file-preview/dialogs/docu-mine/revert-value-dialog/revert-value-dialog.component.scss b/apps/red-ui/src/app/modules/file-preview/dialogs/docu-mine/revert-value-dialog/revert-value-dialog.component.scss new file mode 100644 index 000000000..a599bff82 --- /dev/null +++ b/apps/red-ui/src/app/modules/file-preview/dialogs/docu-mine/revert-value-dialog/revert-value-dialog.component.scss @@ -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; + } + } + } + } +} diff --git a/apps/red-ui/src/app/modules/file-preview/dialogs/docu-mine/revert-value-dialog/revert-value-dialog.component.ts b/apps/red-ui/src/app/modules/file-preview/dialogs/docu-mine/revert-value-dialog/revert-value-dialog.component.ts new file mode 100644 index 000000000..1959fbe07 --- /dev/null +++ b/apps/red-ui/src/app/modules/file-preview/dialogs/docu-mine/revert-value-dialog/revert-value-dialog.component.ts @@ -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 { + protected readonly entry = this.data.entry; + + constructor() { + super(); + } + save() { + this.close(ConfirmOptions.CONFIRM); + } +} diff --git a/apps/red-ui/src/app/modules/file-preview/dialogs/structured-component-management-dialog/structured-component-management-dialog.component.html b/apps/red-ui/src/app/modules/file-preview/dialogs/structured-component-management-dialog/structured-component-management-dialog.component.html deleted file mode 100644 index fd8552a97..000000000 --- a/apps/red-ui/src/app/modules/file-preview/dialogs/structured-component-management-dialog/structured-component-management-dialog.component.html +++ /dev/null @@ -1,97 +0,0 @@ -
-
- -
-
-
-
{{ 'component-log-dialog.table-header.component' | translate }}
-
{{ 'component-log-dialog.table-header.value' | translate }}
-
{{ 'component-log-dialog.table-header.transformation-rule' | translate }}
-
{{ 'component-log-dialog.table-header.annotation-references' | translate }}
- - -
{{ entry.name }}
-
- - - - - -
-
{{ entry.componentValues[0].valueDescription }}
-
-
    -
  • -
- - - -
-
-
-
- -
- - - - - - -
- {{ 'component-log-dialog.actions.display-by-default' | translate }} - -
- - -
diff --git a/apps/red-ui/src/app/modules/file-preview/dialogs/structured-component-management-dialog/structured-component-management-dialog.component.scss b/apps/red-ui/src/app/modules/file-preview/dialogs/structured-component-management-dialog/structured-component-management-dialog.component.scss deleted file mode 100644 index c75e9dc4a..000000000 --- a/apps/red-ui/src/app/modules/file-preview/dialogs/structured-component-management-dialog/structured-component-management-dialog.component.scss +++ /dev/null @@ -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; -} diff --git a/apps/red-ui/src/app/modules/file-preview/dialogs/structured-component-management-dialog/structured-component-management-dialog.component.ts b/apps/red-ui/src/app/modules/file-preview/dialogs/structured-component-management-dialog/structured-component-management-dialog.component.ts deleted file mode 100644 index f8193d7c2..000000000 --- a/apps/red-ui/src/app/modules/file-preview/dialogs/structured-component-management-dialog/structured-component-management-dialog.component.ts +++ /dev/null @@ -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(undefined); - readonly openScmDialogByDefault = signal(this.userPreferences.getOpenScmDialogByDefault()); - - constructor( - protected readonly _dialogRef: MatDialogRef, - 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 { - 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 { - 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; - } - } - } -} diff --git a/apps/red-ui/src/app/modules/file-preview/file-preview-providers.ts b/apps/red-ui/src/app/modules/file-preview/file-preview-providers.ts index e809b6ab5..e4abcaae4 100644 --- a/apps/red-ui/src/app/modules/file-preview/file-preview-providers.ts +++ b/apps/red-ui/src/app/modules/file-preview/file-preview-providers.ts @@ -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, ]; diff --git a/apps/red-ui/src/app/modules/file-preview/file-preview-screen.component.html b/apps/red-ui/src/app/modules/file-preview/file-preview-screen.component.html index 1e4eea900..41b9e50a3 100644 --- a/apps/red-ui/src/app/modules/file-preview/file-preview-screen.component.html +++ b/apps/red-ui/src/app/modules/file-preview/file-preview-screen.component.html @@ -1,100 +1,21 @@
- +
- +
-
- +
+
diff --git a/apps/red-ui/src/app/modules/file-preview/file-preview-screen.component.scss b/apps/red-ui/src/app/modules/file-preview/file-preview-screen.component.scss index bdae60f2f..b708a86bd 100644 --- a/apps/red-ui/src/app/modules/file-preview/file-preview-screen.component.scss +++ b/apps/red-ui/src/app/modules/file-preview/file-preview-screen.component.scss @@ -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 { diff --git a/apps/red-ui/src/app/modules/file-preview/file-preview-screen.component.ts b/apps/red-ui/src/app/modules/file-preview/file-preview-screen.component.ts index ea41e52b8..8cfc9074c 100644 --- a/apps/red-ui/src/app/modules/file-preview/file-preview-screen.component.ts +++ b/apps/red-ui/src/app/modules/file-preview/file-preview-screen.component.ts @@ -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; #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 { + 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 }); } diff --git a/apps/red-ui/src/app/modules/file-preview/file-preview.module.ts b/apps/red-ui/src/app/modules/file-preview/file-preview.module.ts index 95379d171..f21e31e81 100644 --- a/apps/red-ui/src/app/modules/file-preview/file-preview.module.ts +++ b/apps/red-ui/src/app/modules/file-preview/file-preview.module.ts @@ -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], }) diff --git a/apps/red-ui/src/app/modules/file-preview/services/component-log-filter.service.ts b/apps/red-ui/src/app/modules/file-preview/services/component-log-filter.service.ts new file mode 100644 index 000000000..8f58531d7 --- /dev/null +++ b/apps/red-ui/src/app/modules/file-preview/services/component-log-filter.service.ts @@ -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(); + + 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)); + } +} diff --git a/apps/red-ui/src/app/modules/file-preview/services/file-preview-dialog.service.ts b/apps/red-ui/src/app/modules/file-preview/services/file-preview-dialog.service.ts index 283eed614..59af5fef2 100644 --- a/apps/red-ui/src/app/modules/file-preview/services/file-preview-dialog.service.ts +++ b/apps/red-ui/src/app/modules/file-preview/services/file-preview-dialog.service.ts @@ -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 { @@ -41,10 +33,6 @@ export class FilePreviewDialogService extends DialogService { highlightAction: { component: HighlightActionDialogComponent, }, - componentLog: { - component: StructuredComponentManagementDialogComponent, - dialogConfig: { width: '90vw' }, - }, }; constructor(protected readonly _dialog: MatDialog) { diff --git a/apps/red-ui/src/app/modules/file-preview/services/pdf-proxy.service.ts b/apps/red-ui/src/app/modules/file-preview/services/pdf-proxy.service.ts index 2d0096bfc..3f7d3b0ef 100644 --- a/apps/red-ui/src/app/modules/file-preview/services/pdf-proxy.service.ts +++ b/apps/red-ui/src/app/modules/file-preview/services/pdf-proxy.service.ts @@ -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) { diff --git a/apps/red-ui/src/app/modules/icons/icons.module.ts b/apps/red-ui/src/app/modules/icons/icons.module.ts index ebd3203ad..1576fccb4 100644 --- a/apps/red-ui/src/app/modules/icons/icons.module.ts +++ b/apps/red-ui/src/app/modules/icons/icons.module.ts @@ -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', diff --git a/apps/red-ui/src/app/modules/pdf-viewer/pdf-viewer.component.html b/apps/red-ui/src/app/modules/pdf-viewer/pdf-viewer.component.html index 3fd4bcd55..c64f8a8ed 100644 --- a/apps/red-ui/src/app/modules/pdf-viewer/pdf-viewer.component.html +++ b/apps/red-ui/src/app/modules/pdf-viewer/pdf-viewer.component.html @@ -1,3 +1,3 @@ - + diff --git a/apps/red-ui/src/app/modules/pdf-viewer/pdf-viewer.component.ts b/apps/red-ui/src/app/modules/pdf-viewer/pdf-viewer.component.ts index a9e9f6442..34f006113 100644 --- a/apps/red-ui/src/app/modules/pdf-viewer/pdf-viewer.component.ts +++ b/apps/red-ui/src/app/modules/pdf-viewer/pdf-viewer.component.ts @@ -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; +} diff --git a/apps/red-ui/src/app/modules/pdf-viewer/services/viewer-header.service.ts b/apps/red-ui/src/app/modules/pdf-viewer/services/viewer-header.service.ts index 1a97af173..5c35cc60c 100644 --- a/apps/red-ui/src/app/modules/pdf-viewer/services/viewer-header.service.ts +++ b/apps/red-ui/src/app/modules/pdf-viewer/services/viewer-header.service.ts @@ -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); }); diff --git a/apps/red-ui/src/app/modules/shared-dossiers/dialogs/edit-dictionary-dialog/edit-dictionary-dialog.component.ts b/apps/red-ui/src/app/modules/shared-dossiers/dialogs/edit-dictionary-dialog/edit-dictionary-dialog.component.ts index 1f31b8b2d..52328de72 100644 --- a/apps/red-ui/src/app/modules/shared-dossiers/dialogs/edit-dictionary-dialog/edit-dictionary-dialog.component.ts +++ b/apps/red-ui/src/app/modules/shared-dossiers/dialogs/edit-dictionary-dialog/edit-dictionary-dialog.component.ts @@ -82,6 +82,4 @@ export class EditDictionaryDialogComponent extends IqserDialogComponent { @@ -34,21 +35,32 @@ export class ComponentLogService extends GenericService { ); } - getComponentLogData(dossierTemplateId: string, dossierId: string, fileId: string): Observable { + getComponentLogData( + dossierTemplateId: string, + dossierId: string, + fileId: string, + dictionaries: Dictionary[], + ): Observable { 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): Observable { - 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 { - 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 { @@ -91,4 +103,17 @@ export class ComponentLogService extends GenericService { #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; + } } diff --git a/apps/red-ui/src/app/utils/filter-utils.ts b/apps/red-ui/src/app/utils/filter-utils.ts index 68bf58595..c7b1439fd 100644 --- a/apps/red-ui/src/app/utils/filter-utils.ts +++ b/apps/red-ui/src/app/utils/filter-utils.ts @@ -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 = {}; diff --git a/apps/red-ui/src/assets/i18n/redact/de.json b/apps/red-ui/src/assets/i18n/redact/de.json index 95df3a6e8..a5f042e1f 100644 --- a/apps/red-ui/src/assets/i18n/redact/de.json +++ b/apps/red-ui/src/assets/i18n/redact/de.json @@ -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": "{type} 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" -} \ No newline at end of file +} diff --git a/apps/red-ui/src/assets/i18n/redact/en.json b/apps/red-ui/src/assets/i18n/redact/en.json index 42bb0de03..de7165b4d 100644 --- a/apps/red-ui/src/assets/i18n/redact/en.json +++ b/apps/red-ui/src/assets/i18n/redact/en.json @@ -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": "{type} 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" -} \ No newline at end of file +} diff --git a/apps/red-ui/src/assets/i18n/scm/de.json b/apps/red-ui/src/assets/i18n/scm/de.json index 3545578c6..6dc3c29a1 100644 --- a/apps/red-ui/src/assets/i18n/scm/de.json +++ b/apps/red-ui/src/assets/i18n/scm/de.json @@ -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": "{type} 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" -} \ No newline at end of file +} diff --git a/apps/red-ui/src/assets/i18n/scm/en.json b/apps/red-ui/src/assets/i18n/scm/en.json index 495860a61..d9066d318 100644 --- a/apps/red-ui/src/assets/i18n/scm/en.json +++ b/apps/red-ui/src/assets/i18n/scm/en.json @@ -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": "{type} 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" -} \ No newline at end of file +} diff --git a/apps/red-ui/src/assets/icons/general/arrow-right.svg b/apps/red-ui/src/assets/icons/general/arrow-right.svg new file mode 100644 index 000000000..d6cb9a8ce --- /dev/null +++ b/apps/red-ui/src/assets/icons/general/arrow-right.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/apps/red-ui/src/assets/icons/general/draggable-dots.svg b/apps/red-ui/src/assets/icons/general/draggable-dots.svg new file mode 100644 index 000000000..d2b1d8468 --- /dev/null +++ b/apps/red-ui/src/assets/icons/general/draggable-dots.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + diff --git a/apps/red-ui/src/assets/pdftron/stylesheet.css b/apps/red-ui/src/assets/pdftron/stylesheet.css index e62137e5a..a6df83f25 100644 --- a/apps/red-ui/src/assets/pdftron/stylesheet.css +++ b/apps/red-ui/src/assets/pdftron/stylesheet.css @@ -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); +} diff --git a/apps/red-ui/src/styles.scss b/apps/red-ui/src/styles.scss index a09022d4c..699dc8d73 100644 --- a/apps/red-ui/src/styles.scss +++ b/apps/red-ui/src/styles.scss @@ -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; +} diff --git a/libs/common-ui b/libs/common-ui index 04eaca160..748cce403 160000 --- a/libs/common-ui +++ b/libs/common-ui @@ -1 +1 @@ -Subproject commit 04eaca1600149e3f4e803fd2334c25465197dbf6 +Subproject commit 748cce403285c97e14cd3115a828c94c9d5d4520 diff --git a/libs/red-domain/src/lib/component-log/component-log-entry.ts b/libs/red-domain/src/lib/component-log/component-log-entry.ts index 6616c843f..1a53a3bb5 100644 --- a/libs/red-domain/src/lib/component-log/component-log-entry.ts +++ b/libs/red-domain/src/lib/component-log/component-log-entry.ts @@ -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; } } diff --git a/libs/red-domain/src/lib/component-log/component-value.ts b/libs/red-domain/src/lib/component-log/component-value.ts index e9d680f29..a5d4c776d 100644 --- a/libs/red-domain/src/lib/component-log/component-value.ts +++ b/libs/red-domain/src/lib/component-log/component-value.ts @@ -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 {