diff --git a/apps/red-ui/src/app/modules/projects/components/file-workload/file-workload.component.html b/apps/red-ui/src/app/modules/projects/components/file-workload/file-workload.component.html index 44f74eff2..4f4b27476 100644 --- a/apps/red-ui/src/app/modules/projects/components/file-workload/file-workload.component.html +++ b/apps/red-ui/src/app/modules/projects/components/file-workload/file-workload.component.html @@ -49,15 +49,15 @@ tabindex="0" >
-
+
(); @Output() toggleSkipped = new EventEmitter(); @Output() annotationsChanged = new EventEmitter(); - quickScrollFirstEnabled = false; - quickScrollLastEnabled = false; displayedPages: number[] = []; pagesPanelActive = true; @ViewChild('annotationsElement') private _annotationsElement: ElementRef; @@ -129,20 +127,9 @@ export class FileWorkloadComponent { filters.secondary ); this.displayedPages = Object.keys(this.displayedAnnotations).map((key) => Number(key)); - this.computeQuickNavButtonsState(); this._changeDetectorRef.markForCheck(); } - computeQuickNavButtonsState() { - setTimeout(() => { - const element: HTMLElement = - this._quickNavigationElement.nativeElement.querySelector(`#pages`); - const { scrollTop, scrollHeight, clientHeight } = element; - this.quickScrollFirstEnabled = scrollTop !== 0; - this.quickScrollLastEnabled = scrollHeight !== scrollTop + clientHeight; - }, 0); - } - annotationClicked(annotation: AnnotationWrapper, $event: MouseEvent) { this.pagesPanelActive = false; if (this.annotationIsSelected(annotation)) { @@ -234,15 +221,11 @@ export class FileWorkloadComponent { } scrollQuickNavFirst() { - if (this.displayedPages.length > 0) { - this._scrollQuickNavigationToPage(this.displayedPages[0]); - } + this.selectPage.emit(1); } scrollQuickNavLast() { - if (this.displayedPages.length > 0) { - this._scrollQuickNavigationToPage(this.displayedPages[this.displayedPages.length - 1]); - } + this.selectPage.emit(this.fileData.fileStatus.numberOfPages); } pageSelectedByClick($event: number) { diff --git a/apps/red-ui/src/app/modules/projects/dialogs/remove-annotations-dialog/remove-annotations-dialog.component.html b/apps/red-ui/src/app/modules/projects/dialogs/remove-annotations-dialog/remove-annotations-dialog.component.html index 56d67ca60..9b9f80f93 100644 --- a/apps/red-ui/src/app/modules/projects/dialogs/remove-annotations-dialog/remove-annotations-dialog.component.html +++ b/apps/red-ui/src/app/modules/projects/dialogs/remove-annotations-dialog/remove-annotations-dialog.component.html @@ -35,7 +35,7 @@
  • - {{ annotation.value }} + {{ printable(annotation) }}
diff --git a/apps/red-ui/src/app/modules/projects/dialogs/remove-annotations-dialog/remove-annotations-dialog.component.ts b/apps/red-ui/src/app/modules/projects/dialogs/remove-annotations-dialog/remove-annotations-dialog.component.ts index 8917354f3..f4cdc488b 100644 --- a/apps/red-ui/src/app/modules/projects/dialogs/remove-annotations-dialog/remove-annotations-dialog.component.ts +++ b/apps/red-ui/src/app/modules/projects/dialogs/remove-annotations-dialog/remove-annotations-dialog.component.ts @@ -2,6 +2,7 @@ import { Component, Inject } from '@angular/core'; import { AnnotationWrapper } from '@models/file/annotation.wrapper'; import { TranslateService } from '@ngx-translate/core'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { humanize } from '../../../../utils/functions'; export interface RemoveAnnotationsDialogInput { annotationsToRemove: AnnotationWrapper[]; @@ -27,4 +28,14 @@ export class RemoveAnnotationsDialogComponent { confirm() { this.dialogRef.close(true); } + + printable(annotation: AnnotationWrapper) { + if (annotation.isImage) { + return this._translateService.instant('remove-annotations-dialog.image-type', { + typeLabel: humanize(annotation.dictionary) + }); + } else { + return annotation.value; + } + } } diff --git a/apps/red-ui/src/app/modules/projects/projects.module.ts b/apps/red-ui/src/app/modules/projects/projects.module.ts index a268ba0a2..f615d9c1a 100644 --- a/apps/red-ui/src/app/modules/projects/projects.module.ts +++ b/apps/red-ui/src/app/modules/projects/projects.module.ts @@ -37,6 +37,7 @@ import { AnnotationDrawService } from './services/annotation-draw.service'; import { AnnotationProcessingService } from './services/annotation-processing.service'; import { AnnotationRemoveActionsComponent } from './components/annotation-remove-actions/annotation-remove-actions.component'; import { DossierDictionaryDialogComponent } from './dialogs/dossier-dictionary-dialog/dossier-dictionary-dialog.component'; +import { UserPreferenceControllerService } from '@redaction/red-ui-http'; const screens = [ ProjectListingScreenComponent, @@ -83,7 +84,8 @@ const services = [ ManualAnnotationService, PdfViewerDataService, AnnotationDrawService, - AnnotationProcessingService + AnnotationProcessingService, + UserPreferenceControllerService ]; @NgModule({ diff --git a/apps/red-ui/src/app/modules/projects/screens/file-preview-screen/file-preview-screen.component.ts b/apps/red-ui/src/app/modules/projects/screens/file-preview-screen/file-preview-screen.component.ts index 15119b540..2b606320f 100644 --- a/apps/red-ui/src/app/modules/projects/screens/file-preview-screen/file-preview-screen.component.ts +++ b/apps/red-ui/src/app/modules/projects/screens/file-preview-screen/file-preview-screen.component.ts @@ -30,7 +30,11 @@ import { handleFilterDelta, processFilters } from '@shared/components/filter/uti import { UserPreferenceService } from '@services/user-preference.service'; import { UserService } from '@services/user.service'; import { FormBuilder, FormGroup } from '@angular/forms'; -import { FileManagementControllerService, StatusControllerService } from '@redaction/red-ui-http'; +import { + FileManagementControllerService, + StatusControllerService, + UserPreferenceControllerService +} from '@redaction/red-ui-http'; import { PdfViewerDataService } from '../../services/pdf-viewer-data.service'; import { download } from '@utils/file-download-utils'; import { ViewMode } from '@models/file/view-mode'; @@ -79,6 +83,7 @@ export class FilePreviewScreenComponent implements OnInit, OnDestroy, OnAttach, readonly permissionsService: PermissionsService, readonly userPreferenceService: UserPreferenceService, readonly userService: UserService, + private readonly _userPreferenceControllerService: UserPreferenceControllerService, private readonly _changeDetectorRef: ChangeDetectorRef, private readonly _activatedRoute: ActivatedRoute, private readonly _dialogService: ProjectsDialogService, @@ -96,6 +101,16 @@ export class FilePreviewScreenComponent implements OnInit, OnDestroy, OnAttach, this.reviewerForm = this._formBuilder.group({ reviewer: [this.appStateService.activeFile.currentReviewer] }); + + this._loadFileData().subscribe(() => { + this._updateCanPerformActions(); + }); + + document.documentElement.addEventListener('fullscreenchange', () => { + if (!document.fullscreenElement) { + this.fullScreen = false; + } + }); } get singleUsersSelectOptions() { @@ -208,23 +223,18 @@ export class FilePreviewScreenComponent implements OnInit, OnDestroy, OnAttach, } ngOnAttach(previousRoute: ActivatedRouteSnapshot) { - this.displayPDFViewer = true; + this.ngOnInit(); this._lastPage = previousRoute.queryParams.page; - this._subscribeToFileUpdates(); } ngOnInit(): void { this.displayPDFViewer = true; - document.documentElement.addEventListener('fullscreenchange', () => { - if (!document.fullscreenElement) { - this.fullScreen = false; - } - }); - - this._loadFileData().subscribe(() => { - this._updateCanPerformActions(); - }); + const key = 'Project-Recent-' + this.projectId; + this._userPreferenceControllerService + .savePreferences([this.fileId], key) + .toPromise() + .then(); this._subscribeToFileUpdates(); } @@ -348,6 +358,10 @@ export class FilePreviewScreenComponent implements OnInit, OnDestroy, OnAttach, @HostListener('window:keyup', ['$event']) handleKeyEvent($event: KeyboardEvent) { + if (this._router.url.indexOf('/file/') < 0) { + return; + } + if ( !ALL_HOTKEY_ARRAY.includes($event.key) || this.dialogRef?.getState() === MatDialogState.OPEN diff --git a/apps/red-ui/src/app/modules/projects/screens/project-overview-screen/project-overview-screen.component.html b/apps/red-ui/src/app/modules/projects/screens/project-overview-screen/project-overview-screen.component.html index 1e6ba2781..1cf231e76 100644 --- a/apps/red-ui/src/app/modules/projects/screens/project-overview-screen/project-overview-screen.component.html +++ b/apps/red-ui/src/app/modules/projects/screens/project-overview-screen/project-overview-screen.component.html @@ -200,6 +200,7 @@ " [class.disabled]="fileStatus.isExcluded" [class.pointer]="permissionsService.canOpenFile(fileStatus)" + [class.last-opened]="isLastOpenedFile(fileStatus)" [routerLink]="fileLink(fileStatus)" class="table-item" > diff --git a/apps/red-ui/src/app/modules/projects/screens/project-overview-screen/project-overview-screen.component.scss b/apps/red-ui/src/app/modules/projects/screens/project-overview-screen/project-overview-screen.component.scss index c305c4f98..4f91ae3fe 100644 --- a/apps/red-ui/src/app/modules/projects/screens/project-overview-screen/project-overview-screen.component.scss +++ b/apps/red-ui/src/app/modules/projects/screens/project-overview-screen/project-overview-screen.component.scss @@ -122,3 +122,23 @@ cdk-virtual-scroll-viewport { .primary-attribute { padding-top: 6px; } + +.last-opened { + > .selection-column { + padding-left: 6px !important; + border-left: 4px solid $red-1; + } + + > div { + animation: red-fading-background 3s 1; + } +} + +@keyframes red-fading-background { + 0% { + background-color: rgba($red-1, 0.1); + } + 100% { + background-color: inherit; + } +} diff --git a/apps/red-ui/src/app/modules/projects/screens/project-overview-screen/project-overview-screen.component.ts b/apps/red-ui/src/app/modules/projects/screens/project-overview-screen/project-overview-screen.component.ts index 90aa23fe2..c82fdc20a 100644 --- a/apps/red-ui/src/app/modules/projects/screens/project-overview-screen/project-overview-screen.component.ts +++ b/apps/red-ui/src/app/modules/projects/screens/project-overview-screen/project-overview-screen.component.ts @@ -18,7 +18,7 @@ import { } from '@shared/components/filter/utils/filter-utils'; import { PermissionsService } from '@services/permissions.service'; import { UserService } from '@services/user.service'; -import { FileStatus } from '@redaction/red-ui-http'; +import { FileStatus, UserPreferenceControllerService } from '@redaction/red-ui-http'; import { Subscription, timer } from 'rxjs'; import { filter, tap } from 'rxjs/operators'; import { RedactionFilterSorter } from '@utils/sorters/redaction-filter-sorter'; @@ -57,6 +57,7 @@ export class ProjectOverviewScreenComponent private _routerEventsScrollPositionSub: Subscription; private _fileChangedSub: Subscription; private _lastScrollPosition: number; + private _lastOpenedFileId = ''; @ViewChild(CdkVirtualScrollViewport) private _scrollBar: CdkVirtualScrollViewport; @@ -75,6 +76,7 @@ export class ProjectOverviewScreenComponent private readonly _translateService: TranslateService, private readonly _fileDropOverlayService: FileDropOverlayService, private readonly _appStateService: AppStateService, + private readonly _userPreferenceControllerService: UserPreferenceControllerService, protected readonly _injector: Injector ) { super(_injector); @@ -85,6 +87,10 @@ export class ProjectOverviewScreenComponent return this._appStateService.activeProject; } + isLastOpenedFile(fileStatus: FileStatusWrapper): boolean { + return this._lastOpenedFileId === fileStatus.fileId; + } + protected get _filterComponents(): FilterComponent[] { return [ this._statusFilterComponent, @@ -112,6 +118,12 @@ export class ProjectOverviewScreenComponent } ngOnInit(): void { + this._userPreferenceControllerService.getAllUserAttributes().subscribe((attributes) => { + if (attributes === null || attributes === undefined) return; + const key = 'Project-Recent-' + this.activeProject.projectId; + this._lastOpenedFileId = attributes[key][0]; + }); + this._fileDropOverlayService.initFileDropHandling(); this.calculateData(); diff --git a/apps/red-ui/src/app/modules/shared/components/buttons/file-download-btn/file-download-btn.component.ts b/apps/red-ui/src/app/modules/shared/components/buttons/file-download-btn/file-download-btn.component.ts index 79d298f7d..d932d650e 100644 --- a/apps/red-ui/src/app/modules/shared/components/buttons/file-download-btn/file-download-btn.component.ts +++ b/apps/red-ui/src/app/modules/shared/components/buttons/file-download-btn/file-download-btn.component.ts @@ -1,10 +1,11 @@ -import { Component, Input } from '@angular/core'; +import { Component, Inject, Input } from '@angular/core'; import { PermissionsService } from '@services/permissions.service'; import { ProjectWrapper } from '@state/model/project.wrapper'; import { FileStatusWrapper } from '@models/file/file-status.wrapper'; import { FileDownloadService } from '@upload-download/services/file-download.service'; import { NotificationService } from '@services/notification.service'; import { TranslateService } from '@ngx-translate/core'; +import { BASE_HREF } from '../../../../../tokens'; export type MenuState = 'OPEN' | 'CLOSED'; @@ -21,6 +22,7 @@ export class FileDownloadBtnComponent { @Input() tooltipClass: string; constructor( + @Inject(BASE_HREF) private readonly _baseHref: string, private readonly _permissionsService: PermissionsService, private readonly _fileDownloadService: FileDownloadService, private readonly _translateService: TranslateService, @@ -47,7 +49,9 @@ export class FileDownloadBtnComponent { .downloadFiles(Array.isArray(this.file) ? this.file : [this.file], this.project) .subscribe(() => { this._notificationService.showToastNotification( - this._translateService.instant('download-status.queued') + this._translateService.instant('download-status.queued', { + baseUrl: this._baseHref + }) ); }); } diff --git a/apps/red-ui/src/assets/i18n/de.json b/apps/red-ui/src/assets/i18n/de.json index 5a3e94e6c..cc2704e91 100644 --- a/apps/red-ui/src/assets/i18n/de.json +++ b/apps/red-ui/src/assets/i18n/de.json @@ -275,7 +275,6 @@ "description": "Beschreibung" }, "header": "Dossierübersicht", - "no-project": "Angefordertes Dossier: {{projectId}} existiert nicht! Zurück zur Dossierliste.", "bulk": { "delete": "Dokumente löschen", "assign": "Prüfer zuweisen", diff --git a/apps/red-ui/src/assets/i18n/en.json b/apps/red-ui/src/assets/i18n/en.json index c0dc90b4e..a6442e658 100644 --- a/apps/red-ui/src/assets/i18n/en.json +++ b/apps/red-ui/src/assets/i18n/en.json @@ -22,7 +22,7 @@ } }, "download-status": { - "queued": "Your download has been queued, you can see all your requested downloads here: My Downloads .", + "queued": "Your download has been queued, you can see all your requested downloads here: My Downloads .", "error": { "generic": "Download failed" }, @@ -231,7 +231,8 @@ "report": { "action": "Download redaction report" }, - "assign": "Assign Reviewer", + "assign-reviewer": "Assign Reviewer", + "assign-approver": "Assign Approver", "assign-me": "Assign To Me", "table-header": { "title": "{{length}} documents", @@ -281,7 +282,6 @@ "description": "Description" }, "header": "Dossier Overview", - "no-project": "Requested dossier: {{projectId}} does not exist! Back to Dossier Listing. ", "bulk": { "delete": "Delete Documents", "assign": "Assign Reviewer", @@ -349,8 +349,8 @@ "download-original-file": "Download Original File", "exit-fullscreen": "Exit Full Screen (F)", "quick-nav": { - "jump-first": "Jump to first annotation", - "jump-last": "Jump to last annotation" + "jump-first": "Jump to first page", + "jump-last": "Jump to last page" } }, "annotation-actions": { @@ -376,6 +376,10 @@ "success": "Redaction suggestion approved!", "error": "Failed to approved redaction: {{error}}" }, + "request-remove": { + "success": "Requested to remove redaction!", + "error": "Failed to request removal of redaction: {{error}}" + }, "assign-reviewer": "Assign Reviewer", "assign-approver": "Assign Approver", "assign-me": "Assign To Me", @@ -431,35 +435,167 @@ "accept-recommendation": { "label": "Accept Recommendation" }, - "assign-reviewer-owner": { - "dialog": { - "single-user": "Reviewer", - "title": "Manage File Reviewer", - "save": "Save", - "cancel": "Cancel" - } + "suggest-remove-annotation": "Remove or Suggest to remove this entry", + "suggest-remove-annotations": "Remove or Suggest to remove selected entries", + "reject-suggestion": "Reject Suggestion", + "remove-annotation": { + "suggest-remove-from-dict": "Suggest to remove from dictionary", + "suggest-only-here": "Suggest to remove only here", + "remove-from-dict": "Remove from dictionary", + "only-here": "Remove only here", + "false-positive": "False Positive" }, - "assign-approver-owner": { - "dialog": { - "single-user": "Approver", - "title": "Manage File Approver", - "save": "Save", - "cancel": "Cancel" - } - }, - "assign-project-owner": { - "dialog": { - "single-user": "Owner", - "multi-user": "Review Team", - "title": "Manage Dossier Team", - "approvers": "Approvers", - "reviewers": "Reviewers", - "save": "Save Changes", - "cancel": "Cancel", - "search": "Search...", - "no-approvers": "No approvers yet.\nSelect from the list below.", - "no-reviewers": "No reviewers yet.\nSelect from the list below.", - "make-approver": "Make Approver" + "remove": "Remove", + "undo": "Undo", + "reject": "Reject", + "hide": "Hide", + "show": "Show" + }, + "initials-avatar": { + "unassigned": "Unassigned", + "you": "You" + }, + "assign-reviewer-owner": { + "dialog": { + "single-user": "Reviewer", + "title": "Manage File Reviewer", + "save": "Save", + "cancel": "Cancel" + } + }, + "assign-approver-owner": { + "dialog": { + "single-user": "Approver", + "title": "Manage File Approver", + "save": "Save", + "cancel": "Cancel" + } + }, + "assign-project-owner": { + "dialog": { + "single-user": "Owner", + "multi-user": "Review Team", + "title": "Manage Dossier Team", + "approvers": "Approvers", + "reviewers": "Reviewers", + "save": "Save Changes", + "cancel": "Cancel", + "search": "Search...", + "no-approvers": "No approvers yet.\nSelect from the list below.", + "no-reviewers": "No reviewers yet.\nSelect from the list below.", + "make-approver": "Make Approver" + } + }, + "project-member-guard": { + "access-denied": "You are not allowed to access that page." + }, + "comments": { + "comment": "{{count}} comment", + "comments": "{{count}} comments", + "add-comment": "Add a comment", + "hide-comments": "Hide", + "cancel": "Cancel" + }, + "UNPROCESSED": "Unprocessed", + "REPROCESS": "Processing", + "FULLREPROCESS": "Processing", + "PROCESSING": "Processing", + "OCR_PROCESSING": "OCR Processing", + "ERROR": "Re-processing required", + "UNASSIGNED": "Unassigned", + "UNDER_REVIEW": "Under Review", + "UNDER_APPROVAL": "Under Approval", + "APPROVED": "Approved", + "EXCLUDED": "Excluded", + "by": "by", + "efsa": "EFSA Approval", + "finished": "Finished", + "submitted": "Submitted", + "active": "Active", + "archived": "Archived", + "hint": "Hint", + "skipped": "Skipped", + "redaction": "Redaction", + "comment": "Comment", + "pending-analysis": "Pending Re-Analysis", + "suggestion": "Suggestion for redaction", + "dictionary": "Dictionary", + "type": "Type", + "content": "Reason", + "page": "Page", + "annotation": "Annotation", + "annotations": "Annotations", + "filter": { + "hint": "Hints only", + "redaction": "Redacted", + "suggestion": "Suggested Redaction", + "analysis": "Analysis required", + "none": "No Annotations", + "updated": "Updated", + "image": "Images" + }, + "filter-menu": { + "label": "Filter", + "filter-types": "Filter types", + "filter-options": "Filter options", + "with-comments": "Show only annotations with comments" + }, + "sorting": { + "recent": "Recent", + "oldest": "Oldest", + "alphabetically": "Alphabetically", + "number-of-pages": "Number of pages", + "number-of-analyses": "Number of analyses", + "custom": "Custom" + }, + "readonly-pill": "Read-only", + "group": { + "redactions": "Redaction Dictionaries", + "hints": "Hint Dictionaries" + }, + "annotation-type": { + "recommendation": "Recommendation", + "remove-only-here": "Pending removal ( only here )", + "add-dictionary": "Pending add to dictionary", + "remove-dictionary": "Pending remove from dictionary", + "suggestion-add-dictionary": "Suggested dictionary add", + "suggestion-force-redaction": "Suggestion force redaction", + "suggestion-remove-dictionary": "Suggested dictionary removal", + "suggestion-add": "Suggested redaction", + "suggestion-remove": "Suggested redaction removal", + "skipped": "Skipped", + "pending-analysis": "Pending Re-Analysis", + "hint": "Hint", + "redaction": "Redaction", + "manual-redaction": "Manual Redaction", + "declined-suggestion": "Declined Suggestion" + }, + "manual-annotation": { + "dialog": { + "header": { + "dictionary": "Add to dictionary", + "redaction": "Redaction", + "force": "Force Redaction", + "request-dictionary": "Request add to dictionary", + "request-redaction": "Request Redaction", + "false-positive": "Set false positive", + "request-false-positive": "Request false positive" + }, + "add-redaction": { + "success": "Redaction suggestion added!", + "failed": "Failed to add redaction: {{message}}" + }, + "actions": { + "save": "Save" + }, + "content": { + "text": "Selected text:", + "rectangle": "Custom Rectangle", + "dictionary": "Dictionary", + "reason": "Reason", + "reason-placeholder": "Select a reason ...", + "legalBasis": "Legal Basis", + "comment": "Comment" } }, "approve-request": { @@ -492,6 +628,7 @@ "title": "Remove Redaction", "question": "Following redactions will be removed only here:" }, + "image-type": "Image: {{typeLabel}}", "dictionary": "Dictionary", "value": "Value", "confirm": "Yes, proceed and remove!", diff --git a/package.json b/package.json index 58890f334..9a9e9d47a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "redaction", - "version": "2.38.0", + "version": "2.40.0", "private": true, "license": "MIT", "scripts": {