From 30d40fef037eed05ad4184f37dbba8876832cd7d Mon Sep 17 00:00:00 2001 From: Timo Bejan Date: Tue, 27 Oct 2020 16:16:02 +0200 Subject: [PATCH] blah --- .../initials-avatar.component.html | 6 +- .../initials-avatar.component.ts | 81 +- .../status-bar/status-bar.component.html | 8 +- .../status-bar/status-bar.component.scss | 124 +-- .../add-edit-project-dialog.component.html | 79 +- apps/red-ui/src/app/dialogs/dialog.service.ts | 392 ++++--- .../manual-redaction-dialog.component.html | 90 +- apps/red-ui/src/app/icons/icons.module.ts | 77 +- .../base-screen/base-screen.component.html | 169 +-- .../base-screen/base-screen.component.scss | 38 +- .../file-preview-screen.component.html | 445 ++++---- .../file-preview-screen.component.scss | 295 +++--- .../file-preview-screen.component.ts | 757 ++++++------- .../file/pdf-viewer/pdf-viewer.component.ts | 453 ++++---- .../project-listing-screen.component.html | 311 +++--- .../project-listing-screen.component.scss | 86 +- .../project-listing-screen.component.ts | 192 ++-- .../project-overview-screen.component.html | 512 +++++---- .../project-overview-screen.component.scss | 88 +- .../project-overview-screen.component.ts | 365 ++++--- .../red-ui/src/app/state/app-state.service.ts | 483 +++++---- apps/red-ui/src/assets/i18n/en.json | 994 +++++++++--------- .../src/assets/styles/red-checkbox.scss | 22 +- apps/red-ui/src/assets/styles/red-dialog.scss | 2 +- apps/red-ui/src/assets/styles/red-input.scss | 106 +- apps/red-ui/src/assets/styles/red-menu.scss | 44 +- apps/red-ui/src/assets/styles/red-mixins.scss | 12 +- .../src/assets/styles/red-page-layout.scss | 221 ++-- apps/red-ui/src/assets/styles/red-tables.scss | 169 ++- 29 files changed, 3561 insertions(+), 3060 deletions(-) diff --git a/apps/red-ui/src/app/common/initials-avatar/initials-avatar.component.html b/apps/red-ui/src/app/common/initials-avatar/initials-avatar.component.html index 0e4717459..d174eefb8 100644 --- a/apps/red-ui/src/app/common/initials-avatar/initials-avatar.component.html +++ b/apps/red-ui/src/app/common/initials-avatar/initials-avatar.component.html @@ -1,4 +1,6 @@
-
{{initials}}
-
{{username || ('initials-avatar.unassigned.label' | translate)}}
+
{{ initials }}
+
+ {{ username || ('initials-avatar.unassigned.label' | translate) }} +
diff --git a/apps/red-ui/src/app/common/initials-avatar/initials-avatar.component.ts b/apps/red-ui/src/app/common/initials-avatar/initials-avatar.component.ts index e748b81c3..f35d6db32 100644 --- a/apps/red-ui/src/app/common/initials-avatar/initials-avatar.component.ts +++ b/apps/red-ui/src/app/common/initials-avatar/initials-avatar.component.ts @@ -3,57 +3,56 @@ import { UserService } from '../../user/user.service'; import { User } from '@redaction/red-ui-http'; @Component({ - selector: 'redaction-initials-avatar', - templateUrl: './initials-avatar.component.html', - styleUrls: ['./initials-avatar.component.scss'] + selector: 'redaction-initials-avatar', + templateUrl: './initials-avatar.component.html', + styleUrls: ['./initials-avatar.component.scss'] }) export class InitialsAvatarComponent implements OnInit, OnChanges { - @Input() - public userId: string; + @Input() + public userId: string; - @Input() - public color = 'lightgray'; + @Input() + public color = 'lightgray'; - @Input() - public size: 'small' | 'large' = 'small'; + @Input() + public size: 'small' | 'large' = 'small'; - @Input() - public withName = false; + @Input() + public withName = false; - public _user: User; + public _user: User; - constructor(private readonly _userService: UserService) { - } + constructor(private readonly _userService: UserService) {} - ngOnInit(): void { - } + ngOnInit(): void {} - ngOnChanges(): void { - this._user = this._userService.getUserById(this.userId); - } - - public get username(): string { - return this._userService.getName(this._user); - } - - public get initials(): string { - if (!this._user) { - return '?' + ngOnChanges(): void { + this._user = this._userService.getUserById(this.userId); } - return this._userService.getName(this._user) - .split(' ') - .filter(value => value !== ' ') - .filter((value, idx) => idx < 2) - .map((str) => str[0]) - .join(''); - } - - public get colorClass() { - if (this.color.includes('-')) { - return this.color; + public get username(): string { + return this._userService.getName(this._user); + } + + public get initials(): string { + if (!this._user) { + return '?'; + } + + return this._userService + .getName(this._user) + .split(' ') + .filter((value) => value !== ' ') + .filter((value, idx) => idx < 2) + .map((str) => str[0]) + .join(''); + } + + public get colorClass() { + if (this.color.includes('-')) { + return this.color; + } + const textColor = !this._user || !this._userService.isManager(this._user) ? 'dark' : 'red'; + return `${this.color}-${textColor}`; } - const textColor = !this._user || !this._userService.isManager(this._user) ? 'dark' : 'red'; - return `${this.color}-${textColor}` - } } diff --git a/apps/red-ui/src/app/components/status-bar/status-bar.component.html b/apps/red-ui/src/app/components/status-bar/status-bar.component.html index 37013bbd0..0a134aaa4 100644 --- a/apps/red-ui/src/app/components/status-bar/status-bar.component.html +++ b/apps/red-ui/src/app/components/status-bar/status-bar.component.html @@ -1,6 +1,6 @@
-
-
-
{{ rect.label }}
-
+
+
+
{{ rect.label }}
+
diff --git a/apps/red-ui/src/app/components/status-bar/status-bar.component.scss b/apps/red-ui/src/app/components/status-bar/status-bar.component.scss index 66ec22018..e0e0a814b 100644 --- a/apps/red-ui/src/app/components/status-bar/status-bar.component.scss +++ b/apps/red-ui/src/app/components/status-bar/status-bar.component.scss @@ -1,78 +1,78 @@ @import '../../../assets/styles/red-variables'; .rectangle-container { - flex: 1; - display: flex; - width: 100%; - min-width: 12px; + flex: 1; + display: flex; + width: 100%; + min-width: 12px; + + &.small { + .rectangle { + width: 12px; + } + + .section-wrapper { + display: flex; + align-items: center; + gap: 10px; + } + } + + .section-wrapper:first-child { + .rectangle { + border-radius: 6px 0 0 6px; + } + } + + .section-wrapper:last-child { + .rectangle { + border-radius: 0 6px 6px 0; + } + + &:first-child { + .rectangle { + border-radius: 6px; + } + } + } - &.small { .rectangle { - width: 12px; - } + height: 4px; - .section-wrapper { - display: flex; - align-items: center; - gap: 10px; - } - } + &.UNASSIGNED { + background-color: $grey-5; + } - .section-wrapper:first-child { - .rectangle { - border-radius: 6px 0 0 6px; - } - } + &.UNDER_REVIEW { + background-color: $yellow-1; + } - .section-wrapper:last-child { - .rectangle { - border-radius: 0 6px 6px 0; - } + &.UNDER_APPROVAL { + background-color: $red-1; + } - &:first-child { - .rectangle { - border-radius: 6px; - } - } - } + &.APPROVED { + background-color: $blue-2; + } - .rectangle { - height: 4px; + &.SUBMITTED { + background-color: $blue-3; + } - &.UNASSIGNED { - background-color: $grey-5; - } + &.EFSA { + background-color: $blue-4; + } - &.UNDER_REVIEW { - background-color: $yellow-1; - } + &.FINISHED { + background-color: $green-2; + } - &.UNDER_APPROVAL { - background-color: $red-1; - } + &.ACTIVE { + background-color: $primary; + } - &.APPROVED { - background-color: $blue-2; + &.ARCHIVED { + background-color: rgba($red-1, 0.1); + } } - - &.SUBMITTED { - background-color: $blue-3; - } - - &.EFSA { - background-color: $blue-4; - } - - &.FINISHED { - background-color: $green-2; - } - - &.ACTIVE { - background-color: $primary; - } - - &.ARCHIVED { - background-color: rgba($red-1, 0.1); - } - } } diff --git a/apps/red-ui/src/app/dialogs/add-edit-project-dialog/add-edit-project-dialog.component.html b/apps/red-ui/src/app/dialogs/add-edit-project-dialog/add-edit-project-dialog.component.html index bf66f92d2..233478932 100644 --- a/apps/red-ui/src/app/dialogs/add-edit-project-dialog/add-edit-project-dialog.component.html +++ b/apps/red-ui/src/app/dialogs/add-edit-project-dialog/add-edit-project-dialog.component.html @@ -1,40 +1,51 @@
+
+
- -
+
+
+ + +
+
+ + +
-
+ + {{ + 'project-listing.add-edit-dialog.form.due-date.label' | translate + }} + + + + +
-
-
- - -
-
- - -
+
+ +
+ - - {{'project-listing.add-edit-dialog.form.due-date.label' | translate}} - - - - - - -
- -
- -
- - - - +
diff --git a/apps/red-ui/src/app/dialogs/dialog.service.ts b/apps/red-ui/src/app/dialogs/dialog.service.ts index be912d86c..135acebcc 100644 --- a/apps/red-ui/src/app/dialogs/dialog.service.ts +++ b/apps/red-ui/src/app/dialogs/dialog.service.ts @@ -2,11 +2,11 @@ import { Injectable } from '@angular/core'; import { FileDetailsDialogComponent } from './file-details-dialog/file-details-dialog.component'; import { MatDialog, MatDialogRef } from '@angular/material/dialog'; import { - FileStatus, - FileUploadControllerService, - ManualRedactionControllerService, - ManualRedactionEntry, - Project + FileStatus, + FileUploadControllerService, + ManualRedactionControllerService, + ManualRedactionEntry, + Project } from '@redaction/red-ui-http'; import { ConfirmationDialogComponent } from '../common/confirmation-dialog/confirmation-dialog.component'; import { NotificationService, NotificationType } from '../notification/notification.service'; @@ -19,180 +19,258 @@ import { ManualRedactionDialogComponent } from './manual-redaction-dialog/manual import { Annotations } from '@pdftron/webviewer'; const dialogConfig = { - width: '600px', - maxWidth: '90vw', - autoFocus: false + width: '600px', + maxWidth: '90vw', + autoFocus: false }; @Injectable({ - providedIn: 'root' + providedIn: 'root' }) export class DialogService { + constructor( + private readonly _dialog: MatDialog, + private readonly _translateService: TranslateService, + private readonly _appStateService: AppStateService, + private readonly _fileUploadControllerService: FileUploadControllerService, + private readonly _notificationService: NotificationService, + private readonly _manualRedactionControllerService: ManualRedactionControllerService + ) {} - constructor(private readonly _dialog: MatDialog, - private readonly _translateService: TranslateService, - private readonly _appStateService: AppStateService, - private readonly _fileUploadControllerService: FileUploadControllerService, - private readonly _notificationService: NotificationService, - private readonly _manualRedactionControllerService: ManualRedactionControllerService) { - - } - - public openFileDetailsDialog($event: MouseEvent, file: FileStatus): MatDialogRef { - $event.stopPropagation(); - return this._dialog.open(FileDetailsDialogComponent, { - ...dialogConfig, - data: file - }); - } - - public openDeleteFileDialog($event: MouseEvent, projectId: string, fileId: string, cb?: Function): MatDialogRef { - $event.stopPropagation(); - const ref = this._dialog.open(ConfirmationDialogComponent, dialogConfig); - - ref.afterClosed().subscribe(result => { - if (result) { - const file = this._appStateService.getFileById(projectId, fileId); - this._fileUploadControllerService.deleteFile(file.projectId, file.fileId).subscribe(async () => { - await this._appStateService.reloadActiveProjectFiles(); - if (cb) cb(); - }, () => { - this._notificationService.showToastNotification( - this._translateService.instant('delete-file-error.label', file), - null, - NotificationType.ERROR); + public openFileDetailsDialog( + $event: MouseEvent, + file: FileStatus + ): MatDialogRef { + $event.stopPropagation(); + return this._dialog.open(FileDetailsDialogComponent, { + ...dialogConfig, + data: file }); - } - }); + } - return ref; - } + public openDeleteFileDialog( + $event: MouseEvent, + projectId: string, + fileId: string, + cb?: Function + ): MatDialogRef { + $event.stopPropagation(); + const ref = this._dialog.open(ConfirmationDialogComponent, dialogConfig); - public openManualRedactionDialog($event: ManualRedactionEntry, cb?: Function): MatDialogRef { - const ref = this._dialog.open(ManualRedactionDialogComponent, { - ...dialogConfig, - autoFocus: true, - data: $event - }); - - ref.afterClosed().subscribe(result => { - if (cb) { - cb(result); - } - }); - - return ref; - } - - public acceptSuggestionAnnotation($event: MouseEvent, annotation: Annotations.Annotation, projectId: string, fileId: string): MatDialogRef { - $event.stopPropagation(); - - const parts = annotation.Id.split(':'); - const annotationId = parts[parts.length - 1]; - - const ref = this._dialog.open(ConfirmationDialogComponent, dialogConfig); - ref.afterClosed().subscribe(result => { - if (result) { - this._manualRedactionControllerService.approveRequest(projectId, fileId, annotationId).subscribe(() => { - this._notificationService.showToastNotification(this._translateService.instant('manual-redaction.confirm-annotation.success.label'), null, NotificationType.SUCCESS); - }, (err) => { - this._notificationService.showToastNotification(this._translateService.instant('manual-redaction.confirm-annotation.failed.label', err), null, NotificationType.ERROR); + ref.afterClosed().subscribe((result) => { + if (result) { + const file = this._appStateService.getFileById(projectId, fileId); + this._fileUploadControllerService.deleteFile(file.projectId, file.fileId).subscribe( + async () => { + await this._appStateService.reloadActiveProjectFiles(); + if (cb) cb(); + }, + () => { + this._notificationService.showToastNotification( + this._translateService.instant('delete-file-error.label', file), + null, + NotificationType.ERROR + ); + } + ); + } }); - } - }); - return ref; - } + return ref; + } - public suggestRemoveAnnotation($event: MouseEvent, annotation: Annotations.Annotation, projectId: string, fileId: string): MatDialogRef { - $event.stopPropagation(); - - const parts = annotation.Id.split(':'); - const annotationId = parts[parts.length - 1]; - - const ref = this._dialog.open(ConfirmationDialogComponent, { - width: '400px', - maxWidth: '90vw' - }); - - ref.afterClosed().subscribe(result => { - if (result) { - this._manualRedactionControllerService.undo(projectId, fileId, annotationId).subscribe(ok => { - this._notificationService.showToastNotification(this._translateService.instant('manual-redaction.remove-annotation.success.label'), null, NotificationType.SUCCESS); - }, (err) => { - this._notificationService.showToastNotification(this._translateService.instant('manual-redaction.remove-annotation.failed.label', err), null, NotificationType.ERROR); + public openManualRedactionDialog( + $event: ManualRedactionEntry, + cb?: Function + ): MatDialogRef { + const ref = this._dialog.open(ManualRedactionDialogComponent, { + ...dialogConfig, + autoFocus: true, + data: $event }); - } - }); - return ref; - } + ref.afterClosed().subscribe((result) => { + if (cb) { + cb(result); + } + }); - public openEditProjectDialog($event: MouseEvent, project: Project): MatDialogRef { - $event.stopPropagation(); - return this._dialog.open(AddEditProjectDialogComponent, { - ...dialogConfig, - autoFocus: true, - data: project - }); - } + return ref; + } - public openDeleteProjectDialog($event: MouseEvent, project: Project, cb?: Function): MatDialogRef { - $event.stopPropagation(); - const ref = this._dialog.open(ConfirmationDialogComponent, dialogConfig); - ref.afterClosed().subscribe(async result => { - if (result) { - await this._appStateService.deleteProject(project); - if (cb) cb(); - } - }); - return ref; - } + public acceptSuggestionAnnotation( + $event: MouseEvent, + annotation: Annotations.Annotation, + projectId: string, + fileId: string + ): MatDialogRef { + $event.stopPropagation(); - public openAssignProjectMembersAndOwnerDialog($event: MouseEvent, project: Project, cb?: Function): MatDialogRef { - $event?.stopPropagation(); - const ref = this._dialog.open(AssignOwnerDialogComponent, { - ...dialogConfig, - data: { type: 'project', project: project } - }); - ref.afterClosed().subscribe(result => { - if (result && cb) cb(); - }); - return ref; - } + const parts = annotation.Id.split(':'); + const annotationId = parts[parts.length - 1]; - public openAssignFileOwnerDialog($event: MouseEvent, file: FileStatus, cb?: Function): MatDialogRef { - $event.stopPropagation(); - const ref = this._dialog.open(AssignOwnerDialogComponent, { - ...dialogConfig, - data: { type: 'file', file: file } - }); + const ref = this._dialog.open(ConfirmationDialogComponent, dialogConfig); + ref.afterClosed().subscribe((result) => { + if (result) { + this._manualRedactionControllerService + .approveRequest(projectId, fileId, annotationId) + .subscribe( + () => { + this._notificationService.showToastNotification( + this._translateService.instant( + 'manual-redaction.confirm-annotation.success.label' + ), + null, + NotificationType.SUCCESS + ); + }, + (err) => { + this._notificationService.showToastNotification( + this._translateService.instant( + 'manual-redaction.confirm-annotation.failed.label', + err + ), + null, + NotificationType.ERROR + ); + } + ); + } + }); - ref.afterClosed().subscribe(() => { - if (cb) cb(); - }); + return ref; + } - return ref; - } + public suggestRemoveAnnotation( + $event: MouseEvent, + annotation: Annotations.Annotation, + projectId: string, + fileId: string + ): MatDialogRef { + $event.stopPropagation(); - public openProjectDetailsDialog($event: MouseEvent, project: ProjectWrapper): MatDialogRef { - $event.stopPropagation(); - return this._dialog.open(ProjectDetailsDialogComponent, { - ...dialogConfig, - data: project - }); - } + const parts = annotation.Id.split(':'); + const annotationId = parts[parts.length - 1]; - public openAddProjectDialog(cb?: Function): MatDialogRef { - const ref = this._dialog.open(AddEditProjectDialogComponent, { - ...dialogConfig, - autoFocus: true - }); + const ref = this._dialog.open(ConfirmationDialogComponent, { + width: '400px', + maxWidth: '90vw' + }); - ref.afterClosed().subscribe(result => { - if (result && cb) cb(); - }); + ref.afterClosed().subscribe((result) => { + if (result) { + this._manualRedactionControllerService + .undo(projectId, fileId, annotationId) + .subscribe( + (ok) => { + this._notificationService.showToastNotification( + this._translateService.instant( + 'manual-redaction.remove-annotation.success.label' + ), + null, + NotificationType.SUCCESS + ); + }, + (err) => { + this._notificationService.showToastNotification( + this._translateService.instant( + 'manual-redaction.remove-annotation.failed.label', + err + ), + null, + NotificationType.ERROR + ); + } + ); + } + }); - return ref; - } + return ref; + } + + public openEditProjectDialog( + $event: MouseEvent, + project: Project + ): MatDialogRef { + $event.stopPropagation(); + return this._dialog.open(AddEditProjectDialogComponent, { + ...dialogConfig, + autoFocus: true, + data: project + }); + } + + public openDeleteProjectDialog( + $event: MouseEvent, + project: Project, + cb?: Function + ): MatDialogRef { + $event.stopPropagation(); + const ref = this._dialog.open(ConfirmationDialogComponent, dialogConfig); + ref.afterClosed().subscribe(async (result) => { + if (result) { + await this._appStateService.deleteProject(project); + if (cb) cb(); + } + }); + return ref; + } + + public openAssignProjectMembersAndOwnerDialog( + $event: MouseEvent, + project: Project, + cb?: Function + ): MatDialogRef { + $event?.stopPropagation(); + const ref = this._dialog.open(AssignOwnerDialogComponent, { + ...dialogConfig, + data: { type: 'project', project: project } + }); + ref.afterClosed().subscribe((result) => { + if (result && cb) cb(); + }); + return ref; + } + + public openAssignFileOwnerDialog( + $event: MouseEvent, + file: FileStatus, + cb?: Function + ): MatDialogRef { + $event.stopPropagation(); + const ref = this._dialog.open(AssignOwnerDialogComponent, { + ...dialogConfig, + data: { type: 'file', file: file } + }); + + ref.afterClosed().subscribe(() => { + if (cb) cb(); + }); + + return ref; + } + + public openProjectDetailsDialog( + $event: MouseEvent, + project: ProjectWrapper + ): MatDialogRef { + $event.stopPropagation(); + return this._dialog.open(ProjectDetailsDialogComponent, { + ...dialogConfig, + data: project + }); + } + + public openAddProjectDialog(cb?: Function): MatDialogRef { + const ref = this._dialog.open(AddEditProjectDialogComponent, { + ...dialogConfig, + autoFocus: true + }); + + ref.afterClosed().subscribe((result) => { + if (result && cb) cb(); + }); + + return ref; + } } diff --git a/apps/red-ui/src/app/dialogs/manual-redaction-dialog/manual-redaction-dialog.component.html b/apps/red-ui/src/app/dialogs/manual-redaction-dialog/manual-redaction-dialog.component.html index 6bee578f6..a30ad3022 100644 --- a/apps/red-ui/src/app/dialogs/manual-redaction-dialog/manual-redaction-dialog.component.html +++ b/apps/red-ui/src/app/dialogs/manual-redaction-dialog/manual-redaction-dialog.component.html @@ -1,53 +1,53 @@
-
-
+ +
-
+
+
+ +
+ {{ addRedactionRequest.value }} -
+
+ + +
+
+ + +
-
- -
- {{ addRedactionRequest.value }} +
+ + + + {{ dictionary.type }} + + +
-
- - -
- -
- - -
- -
- - - - {{dictionary.type}} - - -
- -
- {{'manual-redaction.dialog.content.dictionary.add.label' | translate}} -
- - -
- -
- -
- - - +
+ {{ + 'manual-redaction.dialog.content.dictionary.add.label' | translate + }} +
+
+
+ +
+ +
diff --git a/apps/red-ui/src/app/icons/icons.module.ts b/apps/red-ui/src/app/icons/icons.module.ts index b492ecc2a..cb5cd2894 100644 --- a/apps/red-ui/src/app/icons/icons.module.ts +++ b/apps/red-ui/src/app/icons/icons.module.ts @@ -1,36 +1,55 @@ -import {NgModule} from '@angular/core'; -import {CommonModule} from '@angular/common'; -import {MatIconModule, MatIconRegistry} from '@angular/material/icon'; -import {DomSanitizer} from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { MatIconModule, MatIconRegistry } from '@angular/material/icon'; +import { DomSanitizer } from '@angular/platform-browser'; @NgModule({ - imports: [CommonModule, MatIconModule], - declarations: [], - exports: [MatIconModule] + imports: [CommonModule, MatIconModule], + declarations: [], + exports: [MatIconModule] }) export class IconsModule { - constructor( - private iconRegistry: MatIconRegistry, - private sanitizer: DomSanitizer - ) { - const icons = [ - 'add', 'analyse', 'arrow-down', 'arrow-up', 'arrow-right', 'assign', 'calendar', - 'check', 'close', 'document', 'double-chevron-right', 'download', - 'edit', 'error', 'folder', 'info', 'lightning', 'logout', 'menu', 'pages', - 'plus', 'preview', 'refresh', 'report', 'secret', 'sort-asc', 'sort-desc', - 'status', 'trash', 'user', 'check-alt', - ]; + constructor(private iconRegistry: MatIconRegistry, private sanitizer: DomSanitizer) { + const icons = [ + 'add', + 'analyse', + 'arrow-down', + 'arrow-up', + 'arrow-right', + 'assign', + 'calendar', + 'check', + 'close', + 'document', + 'double-chevron-right', + 'download', + 'edit', + 'error', + 'folder', + 'info', + 'lightning', + 'logout', + 'menu', + 'pages', + 'plus', + 'preview', + 'refresh', + 'report', + 'secret', + 'sort-asc', + 'sort-desc', + 'status', + 'trash', + 'user', + 'check-alt' + ]; - for (const icon of icons) { - iconRegistry.addSvgIconInNamespace( - 'red', - icon, - sanitizer.bypassSecurityTrustResourceUrl( - `/assets/icons/general/${icon}.svg` - ) - ); + for (const icon of icons) { + iconRegistry.addSvgIconInNamespace( + 'red', + icon, + sanitizer.bypassSecurityTrustResourceUrl(`/assets/icons/general/${icon}.svg`) + ); + } } - } - - } diff --git a/apps/red-ui/src/app/screens/base-screen/base-screen.component.html b/apps/red-ui/src/app/screens/base-screen/base-screen.component.html index 3070fa98e..6ec67a9a7 100644 --- a/apps/red-ui/src/app/screens/base-screen/base-screen.component.html +++ b/apps/red-ui/src/app/screens/base-screen/base-screen.component.html @@ -1,69 +1,110 @@
-
- -
+
- +
diff --git a/apps/red-ui/src/app/screens/base-screen/base-screen.component.scss b/apps/red-ui/src/app/screens/base-screen/base-screen.component.scss index 647662bef..5c1e56a0c 100644 --- a/apps/red-ui/src/app/screens/base-screen/base-screen.component.scss +++ b/apps/red-ui/src/app/screens/base-screen/base-screen.component.scss @@ -1,25 +1,25 @@ -@import "../../../assets/styles/red-variables"; -@import "../../../assets/styles/red-mixins"; +@import '../../../assets/styles/red-variables'; +@import '../../../assets/styles/red-mixins'; .breadcrumbs-container { - display: flex; - gap: 8px; + display: flex; + gap: 8px; - .breadcrumb { - text-decoration: none; - color: $accent; - font-weight: 600; - width: fit-content; - white-space: nowrap; + .breadcrumb { + text-decoration: none; + color: $accent; + font-weight: 600; + width: fit-content; + white-space: nowrap; - &:last-child { - color: $primary; - @include line-clamp(1); + &:last-child { + color: $primary; + @include line-clamp(1); + } + + .mat-icon { + vertical-align: middle; + width: 6px; + } } - - .mat-icon { - vertical-align: middle; - width: 6px; - } - } } diff --git a/apps/red-ui/src/app/screens/file/file-preview-screen/file-preview-screen.component.html b/apps/red-ui/src/app/screens/file/file-preview-screen/file-preview-screen.component.html index e792e566b..8171aec98 100644 --- a/apps/red-ui/src/app/screens/file/file-preview-screen/file-preview-screen.component.html +++ b/apps/red-ui/src/app/screens/file/file-preview-screen/file-preview-screen.component.html @@ -1,191 +1,276 @@
- - -
-
- -
- -
-
-
- -
- -
-
-
-
-
-
-
-
-
-
- - - - -
- - - {{"file-preview.filter-menu." + key + ".label" | translate }} - -
-
-
- - - {{"file-preview.filter-menu." + key + "." + subkey + ".label" | translate }} - -
-
-
- -
-
-
- -
-
-
- {{pageNumber}} -
+
diff --git a/apps/red-ui/src/app/screens/file/file-preview-screen/file-preview-screen.component.scss b/apps/red-ui/src/app/screens/file/file-preview-screen/file-preview-screen.component.scss index 07cda4c1f..9b532d927 100644 --- a/apps/red-ui/src/app/screens/file/file-preview-screen/file-preview-screen.component.scss +++ b/apps/red-ui/src/app/screens/file/file-preview-screen/file-preview-screen.component.scss @@ -1,172 +1,179 @@ -@import "../../../../assets/styles/red-variables"; -@import "../../../../assets/styles/red-mixins"; +@import '../../../../assets/styles/red-variables'; +@import '../../../../assets/styles/red-mixins'; redaction-pdf-viewer { - position: absolute; - width: calc(100vw - #{$right-container-width}); - height: calc(100vh - 110px); - top: 110px; - z-index: 2; + position: absolute; + width: calc(100vw - #{$right-container-width}); + height: calc(100vh - 110px); + top: 110px; + z-index: 2; } .actions-container { - display: flex; - justify-content: flex-end; - align-items: center; - gap: 10px; + display: flex; + justify-content: flex-end; + align-items: center; + gap: 10px; } .filename { - display: flex; - justify-content: center; + display: flex; + justify-content: center; } .right-fixed-container { - padding: 0; - width: $right-container-width; - box-sizing: border-box; - - .right-title { - height: 70px; - display: flex; - border-bottom: 1px solid $separator; - align-items: center; - justify-content: space-between; - padding: 0 24px; - - > div { - position: relative; - - .dot { - background: $primary; - height: 10px; - width: 10px; - border-radius: 50%; - position: absolute; - top: 0; - left: 0; - } - } - - .close-icon { - height: 14px; - width: 14px; - cursor: pointer; - } - } - - .right-content { - height: calc(100vh - 110px - 72px); + padding: 0; + width: $right-container-width; box-sizing: border-box; - display: flex; - .pages, .annotations { - overflow-y: scroll; - @include no-scroll-bar(); - outline: none; - - &.activePanel { - background-color: #FAFAFA; - } - } - - .pages { - border-right: 1px solid $separator; - display: flex; - flex-direction: column; - align-items: center; - gap: 10px; - padding: 16px; - min-width: 28px; - } - - .annotations { - width: 100%; - - .page-separator { - border-bottom: 1px solid $separator; - height: 40px; - box-sizing: border-box; - padding: 8px 10px; + .right-title { + height: 70px; display: flex; - align-items: flex-end; - background-color: $grey-6; - } - - .annotation { border-bottom: 1px solid $separator; - padding: 10px; - font-size: 12px; - cursor: pointer; - position: relative; - display: flex; - gap: 10px; - border-left: 2px solid transparent; + align-items: center; + justify-content: space-between; + padding: 0 24px; - redaction-annotation-icon { - margin-top: 6px; - } + > div { + position: relative; - &:hover { - background-color: #F9FAFB; - - .annotation-actions { - background: linear-gradient(to right, transparent 0%, #F9FAFB, #F9FAFB, #F9FAFB); - display: flex; - align-items: center; - justify-content: flex-end; - width: 120px; - padding-right: 16px; - - .confirm{ - color: $green-2; + .dot { + background: $primary; + height: 10px; + width: 10px; + border-radius: 50%; + position: absolute; + top: 0; + left: 0; } - } } + .close-icon { + height: 14px; + width: 14px; + cursor: pointer; + } + } + + .right-content { + height: calc(100vh - 110px - 72px); + box-sizing: border-box; + display: flex; + + .pages, + .annotations { + overflow-y: scroll; + @include no-scroll-bar(); + outline: none; + + &.activePanel { + background-color: #fafafa; + } + } + + .pages { + border-right: 1px solid $separator; + display: flex; + flex-direction: column; + align-items: center; + gap: 10px; + padding: 16px; + min-width: 28px; + } + + .annotations { + width: 100%; + + .page-separator { + border-bottom: 1px solid $separator; + height: 40px; + box-sizing: border-box; + padding: 8px 10px; + display: flex; + align-items: flex-end; + background-color: $grey-6; + } + + .annotation { + border-bottom: 1px solid $separator; + padding: 10px; + font-size: 12px; + cursor: pointer; + position: relative; + display: flex; + gap: 10px; + border-left: 2px solid transparent; + + redaction-annotation-icon { + margin-top: 6px; + } + + &:hover { + background-color: #f9fafb; + + .annotation-actions { + background: linear-gradient( + to right, + transparent 0%, + #f9fafb, + #f9fafb, + #f9fafb + ); + display: flex; + align-items: center; + justify-content: flex-end; + width: 120px; + padding-right: 16px; + + .confirm { + color: $green-2; + } + } + } + + &.active { + border-left: 2px solid $primary; + } + + .annotation-actions { + position: absolute; + right: 0; + top: 0; + bottom: 0; + display: none; + width: 40px; + } + } + } + } + + .page-number { + border: 1px solid $separator; + padding: 5px 10px; + display: flex; + justify-content: center; + align-items: center; + height: 28px; + min-height: 28px; + min-width: 14px; + opacity: 0.7; + font-size: 11px; + line-height: 14px; + &.active { - border-left: 2px solid $primary; + border: 1px solid $primary; } - - .annotation-actions { - position: absolute; - right: 0; - top: 0; - bottom: 0; - display: none; - width: 40px; - } - } } - } - - .page-number { - border: 1px solid $separator; - padding: 5px 10px; - display: flex; - justify-content: center; - align-items: center; - height: 28px; - min-height: 28px; - min-width: 14px; - opacity: 0.7; - font-size: 11px; - line-height: 14px; - - &.active { - border: 1px solid $primary; - } - } } .filter-menu-header { - display: flex; - justify-content: space-between; - padding: 7px 15px 15px; - width: 350px; - - .actions { display: flex; - gap: 8px; - } + justify-content: space-between; + padding: 7px 15px 15px; + width: 350px; + + .actions { + display: flex; + gap: 8px; + } } diff --git a/apps/red-ui/src/app/screens/file/file-preview-screen/file-preview-screen.component.ts b/apps/red-ui/src/app/screens/file/file-preview-screen/file-preview-screen.component.ts index 651ef114e..032c18f01 100644 --- a/apps/red-ui/src/app/screens/file/file-preview-screen/file-preview-screen.component.ts +++ b/apps/red-ui/src/app/screens/file/file-preview-screen/file-preview-screen.component.ts @@ -1,4 +1,12 @@ -import { ChangeDetectorRef, Component, ElementRef, HostListener, NgZone, OnInit, ViewChild } from '@angular/core'; +import { + ChangeDetectorRef, + Component, + ElementRef, + HostListener, + NgZone, + OnInit, + ViewChild +} from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { ManualRedactionEntry, ReanalysisControllerService } from '@redaction/red-ui-http'; import { AppStateService } from '../../../state/app-state.service'; @@ -17,393 +25,440 @@ import { DialogService } from '../../../dialogs/dialog.service'; import { MatDialogRef, MatDialogState } from '@angular/material/dialog'; @Component({ - selector: 'redaction-file-preview-screen', - templateUrl: './file-preview-screen.component.html', - styleUrls: ['./file-preview-screen.component.scss'] + selector: 'redaction-file-preview-screen', + templateUrl: './file-preview-screen.component.html', + styleUrls: ['./file-preview-screen.component.scss'] }) export class FilePreviewScreenComponent implements OnInit { + private projectId: string; + private _activeViewer: 'ANNOTATED' | 'REDACTED' = 'ANNOTATED'; + private instance: WebViewerInstance; + private _dialogRef: MatDialogRef; - private projectId: string; - private _activeViewer: 'ANNOTATED' | 'REDACTED' = 'ANNOTATED'; - private instance: WebViewerInstance; - private _dialogRef: MatDialogRef; + @ViewChild(PdfViewerComponent) private _viewerComponent: PdfViewerComponent; + @ViewChild('annotations') private _annotationsElement: ElementRef; + @ViewChild('quickNavigation') private _quickNavigationElement: ElementRef; - @ViewChild(PdfViewerComponent) private _viewerComponent: PdfViewerComponent; - @ViewChild('annotations') private _annotationsElement: ElementRef; - @ViewChild('quickNavigation') private _quickNavigationElement: ElementRef; + public annotatedFileData: Blob; + public redactedFileData: Blob; + public fileId: string; + public annotations: Annotations.Annotation[] = []; + public displayedAnnotations: { [key: number]: { annotations: Annotations.Annotation[] } } = {}; + public selectedAnnotation: Annotations.Annotation; + public filters: AnnotationFilters; + public expandedFilters: AnnotationFilters = { hint: false }; + public pagesPanelActive = true; - public annotatedFileData: Blob; - public redactedFileData: Blob; - public fileId: string; - public annotations: Annotations.Annotation[] = []; - public displayedAnnotations: { [key: number]: { annotations: Annotations.Annotation[] } } = {}; - public selectedAnnotation: Annotations.Annotation; - public filters: AnnotationFilters; - public expandedFilters: AnnotationFilters = { hint: false }; - public pagesPanelActive = true; - - constructor( - public readonly appStateService: AppStateService, - private readonly _changeDetectorRef: ChangeDetectorRef, - private readonly _activatedRoute: ActivatedRoute, - private readonly _dialogService: DialogService, - private readonly _router: Router, - private readonly _userService: UserService, - private readonly _fileDownloadService: FileDownloadService, - private readonly _reanalysisControllerService: ReanalysisControllerService, - private readonly _filtersService: FiltersService, - private ngZone: NgZone) { - this._activatedRoute.params.subscribe(params => { - this.projectId = params.projectId; - this.fileId = params.fileId; - this.appStateService.activateFile(this.projectId, this.fileId); - }); - this.filters = _filtersService.filters; - } - - public get user() { - return this._userService.user; - } - - public filterKeys(key?: string) { - if (key) { - return Object.keys(this.filters[key]); + constructor( + public readonly appStateService: AppStateService, + private readonly _changeDetectorRef: ChangeDetectorRef, + private readonly _activatedRoute: ActivatedRoute, + private readonly _dialogService: DialogService, + private readonly _router: Router, + private readonly _userService: UserService, + private readonly _fileDownloadService: FileDownloadService, + private readonly _reanalysisControllerService: ReanalysisControllerService, + private readonly _filtersService: FiltersService, + private ngZone: NgZone + ) { + this._activatedRoute.params.subscribe((params) => { + this.projectId = params.projectId; + this.fileId = params.fileId; + this.appStateService.activateFile(this.projectId, this.fileId); + }); + this.filters = _filtersService.filters; } - return Object.keys(this.filters); - } + public get user() { + return this._userService.user; + } - public get redactedView() { - return this._activeViewer === 'REDACTED'; - } + public filterKeys(key?: string) { + if (key) { + return Object.keys(this.filters[key]); + } - public set redactedView(value: boolean) { - this._activeViewer = value ? 'REDACTED' : 'ANNOTATED'; - } + return Object.keys(this.filters); + } - public ngOnInit(): void { - // PDFTRON cache fix - localStorage.clear(); - this._reloadFiles(); - this.appStateService.fileStatusChanged.subscribe((fileStatus) => { - if (fileStatus.fileId === this.fileId) { - console.log(fileStatus); + public get redactedView() { + return this._activeViewer === 'REDACTED'; + } + + public set redactedView(value: boolean) { + this._activeViewer = value ? 'REDACTED' : 'ANNOTATED'; + } + + public ngOnInit(): void { + // PDFTRON cache fix + localStorage.clear(); this._reloadFiles(); - } - }); - } - - private _reloadFiles() { - this._fileDownloadService.loadFile('ANNOTATED', this.fileId, (data) => { - this.annotatedFileData = data; - }).subscribe(() => { - }); - this._fileDownloadService.loadFile('REDACTED', this.fileId, (data) => { - this.redactedFileData = data; - }).subscribe(() => { - }); - } - - public openFileDetailsDialog($event: MouseEvent) { - this._dialogRef = this._dialogService.openFileDetailsDialog($event, this.appStateService.activeFile); - } - - public reanalyseFile($event: MouseEvent) { - $event.stopPropagation(); - this._reanalysisControllerService.reanalyzeFile(this.appStateService.activeProject.project.projectId, this.fileId).subscribe(async () => { - await this.appStateService.reloadActiveProjectFiles(); - }); - } - - public openDeleteFileDialog($event: MouseEvent) { - this._dialogRef = this._dialogService.openDeleteFileDialog($event, this.projectId, this.fileId, () => { - this._router.navigate([`/ui/projects/${this.projectId}`]); - }); - } - - public openAssignFileOwnerDialog($event: MouseEvent) { - const file = this.appStateService.getFileById(this.projectId, this.fileId); - this._dialogRef = this._dialogService.openAssignFileOwnerDialog($event, file); - } - - public get activeViewer() { - return this.instance; - } - - public applyFilters() { - this.displayedAnnotations = AnnotationUtils.parseAnnotations(this.annotations, this.filters); - } - - public get displayedPages(): number[] { - return Object.keys(this.displayedAnnotations).map(key => Number(key)); - } - - public handleAnnotationSelected(annotation: Annotations.Annotation) { - this.selectedAnnotation = annotation; - this.scrollToSelectedAnnotation(); - this._changeDetectorRef.detectChanges(); - } - - public selectAnnotation(annotation: Annotations.Annotation) { - this._viewerComponent.selectAnnotation(annotation); - } - - @debounce() - private scrollToSelectedAnnotation() { - if (!this.selectedAnnotation) { - return; + this.appStateService.fileStatusChanged.subscribe((fileStatus) => { + if (fileStatus.fileId === this.fileId) { + console.log(fileStatus); + this._reloadFiles(); + } + }); } - const elements: any[] = this._annotationsElement.nativeElement.querySelectorAll(`div[annotation-id="${this.selectedAnnotation.Id}"].active`); - this._scrollToFirstElement(elements); - } - public selectPage(pageNumber: number) { - this._viewerComponent.navigateToPage(pageNumber); - } - - public openManualRedactionDialog($event: ManualRedactionEntry) { - this.ngZone.run(() => { - this._dialogRef = this._dialogService.openManualRedactionDialog($event, () => { - }); - }); - - } - - get activeViewerPage() { - return this.instance.docViewer.getCurrentPage(); - } - - @debounce() - private _scrollViews() { - this._scrollQuickNavigation(); - this._scrollAnnotations(); - } - - private _scrollQuickNavigation() { - const elements: any[] = this._quickNavigationElement.nativeElement.querySelectorAll(`#quick-nav-page-${this.activeViewerPage}`); - this._scrollToFirstElement(elements); - } - - private _scrollAnnotations() { - if (this.selectedAnnotation?.getPageNumber() === this.activeViewerPage) { - return; + private _reloadFiles() { + this._fileDownloadService + .loadFile('ANNOTATED', this.fileId, (data) => { + this.annotatedFileData = data; + }) + .subscribe(() => {}); + this._fileDownloadService + .loadFile('REDACTED', this.fileId, (data) => { + this.redactedFileData = data; + }) + .subscribe(() => {}); } - const elements: any[] = this._annotationsElement.nativeElement.querySelectorAll(`div[anotation-page-header="${this.activeViewerPage}"]`); - this._scrollToFirstElement(elements); - } - private _scrollToFirstElement(elements: HTMLElement[]) { - if (elements.length > 0) { - scrollIntoView(elements[0], { - behavior: 'smooth', - scrollMode: 'if-needed', - block: 'start', - inline: 'start' - }); + public openFileDetailsDialog($event: MouseEvent) { + this._dialogRef = this._dialogService.openFileDetailsDialog( + $event, + this.appStateService.activeFile + ); } - } - public getType(annotation: Annotations.Annotation): string { - return AnnotationUtils.getType(annotation); - } + public reanalyseFile($event: MouseEvent) { + $event.stopPropagation(); + this._reanalysisControllerService + .reanalyzeFile(this.appStateService.activeProject.project.projectId, this.fileId) + .subscribe(async () => { + await this.appStateService.reloadActiveProjectFiles(); + }); + } - public getDictionary(annotation: Annotations.Annotation): string { - return AnnotationUtils.getDictionary(annotation); - } + public openDeleteFileDialog($event: MouseEvent) { + this._dialogRef = this._dialogService.openDeleteFileDialog( + $event, + this.projectId, + this.fileId, + () => { + this._router.navigate([`/ui/projects/${this.projectId}`]); + } + ); + } - public acceptSuggestionAnnotation($event: MouseEvent, annotation: Annotations.Annotation) { - this.ngZone.run(() => { - this._dialogRef = this._dialogService.acceptSuggestionAnnotation($event, annotation, this.projectId, this.fileId); - }); - } + public openAssignFileOwnerDialog($event: MouseEvent) { + const file = this.appStateService.getFileById(this.projectId, this.fileId); + this._dialogRef = this._dialogService.openAssignFileOwnerDialog($event, file); + } - public suggestRemoveAnnotation($event: MouseEvent, annotation: Annotations.Annotation) { - this.ngZone.run(() => { - this._dialogRef = this._dialogService.suggestRemoveAnnotation($event, annotation, this.projectId, this.fileId); - }); - } + public get activeViewer() { + return this.instance; + } - public downloadFile(type: FileType | string) { - this._fileDownloadService.loadFile(type, this.fileId).subscribe(data => { - saveAs(data, this.appStateService.activeFile.filename); - }); - } + public applyFilters() { + this.displayedAnnotations = AnnotationUtils.parseAnnotations( + this.annotations, + this.filters + ); + } - public setAllFilters(filter: AnnotationFilters, value: boolean, rootKey?: string) { - if (rootKey) { - this.filters[rootKey] = value; - } else { + public get displayedPages(): number[] { + return Object.keys(this.displayedAnnotations).map((key) => Number(key)); + } - for (const key of Object.keys(filter)) { - if (AnnotationUtils.hasSubsections(filter[key])) { - this.setAllFilters(filter[key], value); - } else { - filter[key] = value; + public handleAnnotationSelected(annotation: Annotations.Annotation) { + this.selectedAnnotation = annotation; + this.scrollToSelectedAnnotation(); + this._changeDetectorRef.detectChanges(); + } + + public selectAnnotation(annotation: Annotations.Annotation) { + this._viewerComponent.selectAnnotation(annotation); + } + + @debounce() + private scrollToSelectedAnnotation() { + if (!this.selectedAnnotation) { + return; } - } + const elements: any[] = this._annotationsElement.nativeElement.querySelectorAll( + `div[annotation-id="${this.selectedAnnotation.Id}"].active` + ); + this._scrollToFirstElement(elements); } - this.applyFilters(); - } - - public isChecked(key: string): boolean { - return AnnotationUtils.isChecked(this.filters[key]); - } - - public isIndeterminate(key: string): boolean { - return AnnotationUtils.isIndeterminate(this.filters[key]); - } - - public get hasActiveFilters(): boolean { - return AnnotationUtils.hasActiveFilters(this.filters); - } - - public hasSubsections(filter: AnnotationFilters | boolean) { - return AnnotationUtils.hasSubsections(filter); - } - - public setExpanded(key: string, value: boolean, $event: MouseEvent) { - $event.stopPropagation(); - this.expandedFilters[key] = value; - } - - public isManuallyAddedAnnotation(annotation: Annotations.Annotation) { - return annotation.Id.indexOf('request:') >= 0; - } - - @HostListener('window:keyup', ['$event']) - handleKeyEvent($event: KeyboardEvent) { - const keyArray = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown']; - - if (!keyArray.includes($event.key) || this._dialogRef?.getState() === MatDialogState.OPEN) { - return; + public selectPage(pageNumber: number) { + this._viewerComponent.navigateToPage(pageNumber); } - if ($event.key === 'ArrowLeft' || $event.key === 'ArrowRight') { - this.pagesPanelActive = !this.pagesPanelActive; - this._changeDetectorRef.detectChanges(); - return; + public openManualRedactionDialog($event: ManualRedactionEntry) { + this.ngZone.run(() => { + this._dialogRef = this._dialogService.openManualRedactionDialog($event, () => {}); + }); } - if (!this.pagesPanelActive) { - this._navigateAnnotations($event); - } else { - this._navigatePages($event); + get activeViewerPage() { + return this.instance.docViewer.getCurrentPage(); } - } - private _navigateAnnotations($event: KeyboardEvent) { - if (!this.selectedAnnotation || this.activeViewerPage !== this.selectedAnnotation.getPageNumber()) { - const pageIdx = this.displayedPages.indexOf(this.activeViewerPage); - if (pageIdx !== -1) { // Displayed page has annotations - this.selectAnnotation(this.displayedAnnotations[this.activeViewerPage].annotations[0]); - } else { // Displayed page doesn't have annotations - if ($event.key === 'ArrowDown') { - const nextPage = this._nextPageWithAnnotations(); - this.selectAnnotation(this.displayedAnnotations[nextPage].annotations[0]); - } else { - const prevPage = this._prevPageWithAnnotations(); - const prevPageAnnotations = this.displayedAnnotations[prevPage].annotations; - this.selectAnnotation(prevPageAnnotations[prevPageAnnotations.length - 1]); - } - } - - } else { - const page = this.selectedAnnotation.getPageNumber(); - const pageIdx = this.displayedPages.indexOf(page); - const annotationsOnPage = this.displayedAnnotations[page].annotations; - const idx = annotationsOnPage.indexOf(this.selectedAnnotation); - - if ($event.key === 'ArrowDown') { - if (idx + 1 !== annotationsOnPage.length) { // If not last item in page - this.selectAnnotation(annotationsOnPage[idx + 1]); - } else if (pageIdx + 1 < this.displayedPages.length) { // If not last page - const nextPageAnnotations = this.displayedAnnotations[this.displayedPages[pageIdx + 1]].annotations; - this.selectAnnotation(nextPageAnnotations[0]); - } - } else { - if (idx !== 0) { // If not first item in page - this.selectAnnotation(annotationsOnPage[idx - 1]); - } else if (pageIdx) { // If not first page - const prevPageAnnotations = this.displayedAnnotations[this.displayedPages[pageIdx - 1]].annotations; - this.selectAnnotation(prevPageAnnotations[prevPageAnnotations.length - 1]); - } - } - } - } - - private _navigatePages($event: KeyboardEvent) { - const pageIdx = this.displayedPages.indexOf(this.activeViewerPage); - - if ($event.key === 'ArrowDown') { - if (pageIdx !== -1) { // If active page has annotations - if (pageIdx !== this.displayedPages.length - 1) { - this.selectPage(this.displayedPages[pageIdx + 1]); - } - } else { // If active page doesn't have annotations - const nextPage = this._nextPageWithAnnotations(); - if (nextPage) { - this.selectPage(nextPage); - } - } - } else { - if (pageIdx !== -1) { // If active page has annotations - if (pageIdx !== 0) { - this.selectPage(this.displayedPages[pageIdx - 1]); - } - } else { // If active page doesn't have annotations - const prevPage = this._prevPageWithAnnotations(); - if (prevPage) { - this.selectPage(prevPage); - } - } - } - } - - private _nextPageWithAnnotations() { - let idx = 0; - for (const page of this.displayedPages) { - if (page > this.activeViewerPage) { - break; - } - ++idx; - } - return idx < this.displayedPages.length ? this.displayedPages[idx] : null; - } - - private _prevPageWithAnnotations() { - let idx = this.displayedPages.length - 1; - for (const page of this.displayedPages.reverse()) { - if (page < this.activeViewerPage) { - this.selectPage(this.displayedPages[idx]); + @debounce() + private _scrollViews() { + this._scrollQuickNavigation(); this._scrollAnnotations(); - break; - } - --idx; } - return idx >= 0 ? this.displayedPages[idx] : null; - } - viewerPageChanged($event: number) { - this._scrollViews(); - this._changeDetectorRef.detectChanges(); - } + private _scrollQuickNavigation() { + const elements: any[] = this._quickNavigationElement.nativeElement.querySelectorAll( + `#quick-nav-page-${this.activeViewerPage}` + ); + this._scrollToFirstElement(elements); + } - viewerReady($event: WebViewerInstance) { - this.instance = $event; - } + private _scrollAnnotations() { + if (this.selectedAnnotation?.getPageNumber() === this.activeViewerPage) { + return; + } + const elements: any[] = this._annotationsElement.nativeElement.querySelectorAll( + `div[anotation-page-header="${this.activeViewerPage}"]` + ); + this._scrollToFirstElement(elements); + } - handleAnnotationsAdded(annotations: Annotations.Annotation[]) { + private _scrollToFirstElement(elements: HTMLElement[]) { + if (elements.length > 0) { + scrollIntoView(elements[0], { + behavior: 'smooth', + scrollMode: 'if-needed', + block: 'start', + inline: 'start' + }); + } + } - // handle comments - annotations.forEach(a => { - a['comments'] = a['Mi'] ? a['Mi'].map(m => { - return { value: m.eC }; - }) : []; - }); + public getType(annotation: Annotations.Annotation): string { + return AnnotationUtils.getType(annotation); + } - AnnotationUtils.addAnnotations(this.annotations, annotations); - this.applyFilters(); - this._changeDetectorRef.detectChanges(); - } + public getDictionary(annotation: Annotations.Annotation): string { + return AnnotationUtils.getDictionary(annotation); + } + + public acceptSuggestionAnnotation($event: MouseEvent, annotation: Annotations.Annotation) { + this.ngZone.run(() => { + this._dialogRef = this._dialogService.acceptSuggestionAnnotation( + $event, + annotation, + this.projectId, + this.fileId + ); + }); + } + + public suggestRemoveAnnotation($event: MouseEvent, annotation: Annotations.Annotation) { + this.ngZone.run(() => { + this._dialogRef = this._dialogService.suggestRemoveAnnotation( + $event, + annotation, + this.projectId, + this.fileId + ); + }); + } + + public downloadFile(type: FileType | string) { + this._fileDownloadService.loadFile(type, this.fileId).subscribe((data) => { + saveAs(data, this.appStateService.activeFile.filename); + }); + } + + public setAllFilters(filter: AnnotationFilters, value: boolean, rootKey?: string) { + if (rootKey) { + this.filters[rootKey] = value; + } else { + for (const key of Object.keys(filter)) { + if (AnnotationUtils.hasSubsections(filter[key])) { + this.setAllFilters(filter[key], value); + } else { + filter[key] = value; + } + } + } + + this.applyFilters(); + } + + public isChecked(key: string): boolean { + return AnnotationUtils.isChecked(this.filters[key]); + } + + public isIndeterminate(key: string): boolean { + return AnnotationUtils.isIndeterminate(this.filters[key]); + } + + public get hasActiveFilters(): boolean { + return AnnotationUtils.hasActiveFilters(this.filters); + } + + public hasSubsections(filter: AnnotationFilters | boolean) { + return AnnotationUtils.hasSubsections(filter); + } + + public setExpanded(key: string, value: boolean, $event: MouseEvent) { + $event.stopPropagation(); + this.expandedFilters[key] = value; + } + + public isManuallyAddedAnnotation(annotation: Annotations.Annotation) { + return annotation.Id.indexOf('request:') >= 0; + } + + @HostListener('window:keyup', ['$event']) + handleKeyEvent($event: KeyboardEvent) { + const keyArray = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown']; + + if (!keyArray.includes($event.key) || this._dialogRef?.getState() === MatDialogState.OPEN) { + return; + } + + if ($event.key === 'ArrowLeft' || $event.key === 'ArrowRight') { + this.pagesPanelActive = !this.pagesPanelActive; + this._changeDetectorRef.detectChanges(); + return; + } + + if (!this.pagesPanelActive) { + this._navigateAnnotations($event); + } else { + this._navigatePages($event); + } + } + + private _navigateAnnotations($event: KeyboardEvent) { + if ( + !this.selectedAnnotation || + this.activeViewerPage !== this.selectedAnnotation.getPageNumber() + ) { + const pageIdx = this.displayedPages.indexOf(this.activeViewerPage); + if (pageIdx !== -1) { + // Displayed page has annotations + this.selectAnnotation( + this.displayedAnnotations[this.activeViewerPage].annotations[0] + ); + } else { + // Displayed page doesn't have annotations + if ($event.key === 'ArrowDown') { + const nextPage = this._nextPageWithAnnotations(); + this.selectAnnotation(this.displayedAnnotations[nextPage].annotations[0]); + } else { + const prevPage = this._prevPageWithAnnotations(); + const prevPageAnnotations = this.displayedAnnotations[prevPage].annotations; + this.selectAnnotation(prevPageAnnotations[prevPageAnnotations.length - 1]); + } + } + } else { + const page = this.selectedAnnotation.getPageNumber(); + const pageIdx = this.displayedPages.indexOf(page); + const annotationsOnPage = this.displayedAnnotations[page].annotations; + const idx = annotationsOnPage.indexOf(this.selectedAnnotation); + + if ($event.key === 'ArrowDown') { + if (idx + 1 !== annotationsOnPage.length) { + // If not last item in page + this.selectAnnotation(annotationsOnPage[idx + 1]); + } else if (pageIdx + 1 < this.displayedPages.length) { + // If not last page + const nextPageAnnotations = this.displayedAnnotations[ + this.displayedPages[pageIdx + 1] + ].annotations; + this.selectAnnotation(nextPageAnnotations[0]); + } + } else { + if (idx !== 0) { + // If not first item in page + this.selectAnnotation(annotationsOnPage[idx - 1]); + } else if (pageIdx) { + // If not first page + const prevPageAnnotations = this.displayedAnnotations[ + this.displayedPages[pageIdx - 1] + ].annotations; + this.selectAnnotation(prevPageAnnotations[prevPageAnnotations.length - 1]); + } + } + } + } + + private _navigatePages($event: KeyboardEvent) { + const pageIdx = this.displayedPages.indexOf(this.activeViewerPage); + + if ($event.key === 'ArrowDown') { + if (pageIdx !== -1) { + // If active page has annotations + if (pageIdx !== this.displayedPages.length - 1) { + this.selectPage(this.displayedPages[pageIdx + 1]); + } + } else { + // If active page doesn't have annotations + const nextPage = this._nextPageWithAnnotations(); + if (nextPage) { + this.selectPage(nextPage); + } + } + } else { + if (pageIdx !== -1) { + // If active page has annotations + if (pageIdx !== 0) { + this.selectPage(this.displayedPages[pageIdx - 1]); + } + } else { + // If active page doesn't have annotations + const prevPage = this._prevPageWithAnnotations(); + if (prevPage) { + this.selectPage(prevPage); + } + } + } + } + + private _nextPageWithAnnotations() { + let idx = 0; + for (const page of this.displayedPages) { + if (page > this.activeViewerPage) { + break; + } + ++idx; + } + return idx < this.displayedPages.length ? this.displayedPages[idx] : null; + } + + private _prevPageWithAnnotations() { + let idx = this.displayedPages.length - 1; + for (const page of this.displayedPages.reverse()) { + if (page < this.activeViewerPage) { + this.selectPage(this.displayedPages[idx]); + this._scrollAnnotations(); + break; + } + --idx; + } + return idx >= 0 ? this.displayedPages[idx] : null; + } + + viewerPageChanged($event: number) { + this._scrollViews(); + this._changeDetectorRef.detectChanges(); + } + + viewerReady($event: WebViewerInstance) { + this.instance = $event; + } + + handleAnnotationsAdded(annotations: Annotations.Annotation[]) { + // handle comments + annotations.forEach((a) => { + a['comments'] = a['Mi'] + ? a['Mi'].map((m) => { + return { value: m.eC }; + }) + : []; + }); + + AnnotationUtils.addAnnotations(this.annotations, annotations); + this.applyFilters(); + this._changeDetectorRef.detectChanges(); + } } diff --git a/apps/red-ui/src/app/screens/file/pdf-viewer/pdf-viewer.component.ts b/apps/red-ui/src/app/screens/file/pdf-viewer/pdf-viewer.component.ts index 981d94634..b98ca230c 100644 --- a/apps/red-ui/src/app/screens/file/pdf-viewer/pdf-viewer.component.ts +++ b/apps/red-ui/src/app/screens/file/pdf-viewer/pdf-viewer.component.ts @@ -1,252 +1,249 @@ import { - AfterViewInit, - Component, - ElementRef, - EventEmitter, - Input, - OnChanges, - OnInit, - Output, - SimpleChanges, - ViewChild + AfterViewInit, + Component, + ElementRef, + EventEmitter, + Input, + OnChanges, + OnInit, + Output, + SimpleChanges, + ViewChild } from '@angular/core'; -import {AppConfigKey, AppConfigService} from '../../../app-config/app-config.service'; -import {FileStatus, ManualRedactionEntry, Rectangle} from '@redaction/red-ui-http'; -import WebViewer, {Annotations, WebViewerInstance} from '@pdftron/webviewer'; -import {TranslateService} from '@ngx-translate/core'; -import {FileDownloadService} from '../service/file-download.service'; +import { AppConfigKey, AppConfigService } from '../../../app-config/app-config.service'; +import { FileStatus, ManualRedactionEntry, Rectangle } from '@redaction/red-ui-http'; +import WebViewer, { Annotations, WebViewerInstance } from '@pdftron/webviewer'; +import { TranslateService } from '@ngx-translate/core'; +import { FileDownloadService } from '../service/file-download.service'; export interface ViewerState { - - displayMode?: any; - layoutMode?: any; - pageNumber?: any; - scrollTop?: any; - scrollLeft?: any; - zoom?: any; - leftPanelState?: any; - + displayMode?: any; + layoutMode?: any; + pageNumber?: any; + scrollTop?: any; + scrollLeft?: any; + zoom?: any; + leftPanelState?: any; } @Component({ - selector: 'redaction-pdf-viewer', - templateUrl: './pdf-viewer.component.html', - styleUrls: ['./pdf-viewer.component.scss'] + selector: 'redaction-pdf-viewer', + templateUrl: './pdf-viewer.component.html', + styleUrls: ['./pdf-viewer.component.scss'] }) export class PdfViewerComponent implements OnInit, AfterViewInit, OnChanges { + private _viewerState: ViewerState = null; // no initial state - private _viewerState: ViewerState = null; // no initial state + @Input() fileData: Blob; + @Input() fileStatus: FileStatus; - @Input() fileData: Blob; - @Input() fileStatus: FileStatus; + @Output() fileReady = new EventEmitter(); + @Output() annotationsAdded = new EventEmitter(); + @Output() annotationSelected = new EventEmitter(); + @Output() manualAnnotationRequested = new EventEmitter(); + @Output() pageChanged = new EventEmitter(); + @Output() keyUp = new EventEmitter(); + @Output() viewerReady = new EventEmitter(); - @Output() fileReady = new EventEmitter(); - @Output() annotationsAdded = new EventEmitter(); - @Output() annotationSelected = new EventEmitter(); - @Output() manualAnnotationRequested = new EventEmitter(); - @Output() pageChanged = new EventEmitter(); - @Output() keyUp = new EventEmitter(); + @Input() flag = false; - @Output() viewerReady = new EventEmitter(); + @ViewChild('viewer', { static: true }) viewer: ElementRef; + instance: WebViewerInstance; - @Input() flag = false; + constructor( + private readonly _translateService: TranslateService, + private readonly _fileDownloadService: FileDownloadService, + private readonly _appConfigService: AppConfigService + ) {} - @ViewChild('viewer', {static: true}) viewer: ElementRef; - instance: WebViewerInstance; - - constructor(private readonly _translateService: TranslateService, - private readonly _fileDownloadService: FileDownloadService, - private readonly _appConfigService: AppConfigService) { - } - - ngOnInit() { - this._restoreViewerState = this._restoreViewerState.bind(this); - } - - ngOnChanges(changes: SimpleChanges): void { - if (changes.fileData && !changes.fileData.firstChange) { - this._changeDocument(); + ngOnInit() { + this._restoreViewerState = this._restoreViewerState.bind(this); } - } - ngAfterViewInit(): void { - this._loadViewer(this.fileData); - } - - private _loadViewer(pdfBlob: any) { - const license = this._appConfigService.getConfig(AppConfigKey.PDFTRON_LICENSE); - WebViewer({ - licenseKey: license, - isReadOnly: true, - path: '/assets/wv-resources' - }, this.viewer.nativeElement).then(instance => { - this.instance = instance; - this._disableElements(); - this._configureTextPopup(); - this._configureHeader(); - instance.annotManager.on('annotationChanged', (annotations, action) => { - if (action === 'add') { - this.annotationsAdded.emit(annotations); + ngOnChanges(changes: SimpleChanges): void { + if (changes.fileData && !changes.fileData.firstChange) { + this._changeDocument(); } - }); + } - instance.annotManager.on('annotationSelected', ((annotationList, action) => { - if (action === 'deselected') { - this.annotationSelected.emit(null); + ngAfterViewInit(): void { + this._loadViewer(this.fileData); + } + + private _loadViewer(pdfBlob: any) { + const license = this._appConfigService.getConfig(AppConfigKey.PDFTRON_LICENSE); + WebViewer( + { + licenseKey: license, + isReadOnly: true, + path: '/assets/wv-resources' + }, + this.viewer.nativeElement + ).then((instance) => { + this.instance = instance; + this._disableElements(); + this._configureTextPopup(); + this._configureHeader(); + instance.annotManager.on('annotationChanged', (annotations, action) => { + if (action === 'add') { + this.annotationsAdded.emit(annotations); + } + }); + + instance.annotManager.on('annotationSelected', (annotationList, action) => { + if (action === 'deselected') { + this.annotationSelected.emit(null); + } else { + this.annotationSelected.emit(annotationList[0]); + } + }); + + instance.docViewer.on('pageComplete', (p) => { + this.pageChanged.emit(p); + }); + + instance.docViewer.on('documentLoaded', this._restoreViewerState); + + instance.docViewer.on('keyDown', ($event) => { + if ($event.key.startsWith('Arrow')) { + $event.preventDefault(); + } + }); + + instance.docViewer.on('keyUp', ($event) => { + if ($event.key.startsWith('Arrow')) { + this.keyUp.emit($event); + } + }); + + instance.loadDocument(pdfBlob, { + filename: this.fileStatus ? this.fileStatus.filename : 'document.pdf' + }); + + this.viewerReady.emit(instance); + }); + } + + private _disableElements() { + this.instance.disableElements([ + 'textHighlightToolButton', + 'textUnderlineToolButton', + 'textSquigglyToolButton', + 'textStrikeoutToolButton', + 'linkButton', + 'toggleNotesButton', + 'notesPanel', + 'thumbnailControl', + 'documentControl', + 'ribbons', + 'rotateClockwiseButton', + 'rotateCounterClockwiseButton' + ]); + } + + private _configureTextPopup() { + this.instance.textPopup.add({ + type: 'actionButton', + img: '/assets/icons/general/add-redaction.svg', + title: this._translateService.instant( + 'pdf-viewer.text-popup.actions.suggestion-redaction.label' + ), + onClick: () => { + const selectedQuads = this.instance.docViewer.getSelectedTextQuads(); + const text = this.instance.docViewer.getSelectedText(); + const entry: ManualRedactionEntry = { positions: [] }; + for (const key of Object.keys(selectedQuads)) { + for (const quad of selectedQuads[key]) { + entry.positions.push(this.toPosition(parseInt(key, 10), quad)); + } + } + entry.value = text; + this.manualAnnotationRequested.emit(entry); + } + }); + } + + private toPosition(page: number, selectedQuad: any): Rectangle { + const pageHeight = this.instance.docViewer.getPageHeight(page); + const height = selectedQuad.y2 - selectedQuad.y4; + return { + page: page, + topLeft: { + x: selectedQuad.x4, + y: pageHeight - (selectedQuad.y4 + height) + }, + height: height, + width: selectedQuad.x3 - selectedQuad.x4 + }; + } + + private _configureHeader() { + this.instance.setToolbarGroup('toolbarGroup-View'); + } + + public selectAnnotation(annotation: Annotations.Annotation) { + this.instance.annotManager.deselectAllAnnotations(); + this.instance.annotManager.selectAnnotation(annotation); + this.navigateToPage(annotation.getPageNumber()); + } + + public navigateToPage(pageNumber: number) { + const activePage = this.instance.docViewer.getCurrentPage(); + if (activePage !== pageNumber) { + this.instance.docViewer.displayPageLocation(pageNumber, 0, 0); + } + } + + private _changeDocument() { + // sync layout and display mode + + const instance = this.instance; + const docViewer = this.instance.docViewer; + + const lastScrolledViewerScrollElement = docViewer.getScrollViewElement(); + + const viewerState: ViewerState = { + displayMode: docViewer.getDisplayModeManager().getDisplayMode().mode, + layoutMode: instance.getLayoutMode(), + pageNumber: instance.docViewer.getCurrentPage(), + scrollLeft: lastScrolledViewerScrollElement.scrollLeft, + scrollTop: lastScrolledViewerScrollElement.scrollTop, + zoom: docViewer.getZoom(), + leftPanelState: instance.isElementOpen('leftPanel') + }; + + this.instance.loadDocument(this.fileData, { + filename: this.fileStatus ? this.fileStatus.filename : 'document.pdf' + }); + + this._viewerState = viewerState; + } + + private _restoreViewerState() { + this._restoreState(this._viewerState, this.instance); + } + + private _restoreState(viewerState: ViewerState, instance: WebViewerInstance) { + if (this._viewerState) { + instance.docViewer.setCurrentPage(viewerState.pageNumber); + instance.setLayoutMode(viewerState.layoutMode); + const instanceDisplayMode = instance.docViewer.getDisplayModeManager().getDisplayMode(); + instanceDisplayMode.mode = viewerState.displayMode; + instance.docViewer.getDisplayModeManager().setDisplayMode(instanceDisplayMode); + // Synchronize zoom - needs to be done before scrolling + instance.docViewer.zoomTo(viewerState.zoom); + const viewerScrollElement = instance.docViewer.getScrollViewElement(); + viewerScrollElement.scrollTo(viewerState.scrollLeft, viewerState.scrollTop); + + if (viewerState.leftPanelState) { + instance.openElements(['leftPanel']); + } else { + instance.closeElements(['leftPanel']); + } } else { - this.annotationSelected.emit(annotationList[0]); + // viewer init + this.instance.setFitMode('FitPage'); } - })); - - instance.docViewer.on('pageComplete', (p) => { - this.pageChanged.emit(p); - }); - - instance.docViewer.on('documentLoaded', this._restoreViewerState); - - instance.docViewer.on('keyDown', ($event) => { - if ($event.key.startsWith('Arrow')) { - $event.preventDefault(); - } - }) - - instance.docViewer.on('keyUp', ($event) => { - if ($event.key.startsWith('Arrow')) { - this.keyUp.emit($event); - } - }) - - instance.loadDocument(pdfBlob, {filename: this.fileStatus ? this.fileStatus.filename : 'document.pdf'}); - - this.viewerReady.emit(instance); - }); - } - - - private _disableElements() { - this.instance.disableElements([ - 'textHighlightToolButton', - 'textUnderlineToolButton', - 'textSquigglyToolButton', - 'textStrikeoutToolButton', - 'linkButton', - 'toggleNotesButton', - 'notesPanel', - 'thumbnailControl', - 'documentControl', - 'ribbons', - 'rotateClockwiseButton', - 'rotateCounterClockwiseButton' - ]); - } - - private _configureTextPopup() { - this.instance.textPopup.add({ - type: 'actionButton', - img: '/assets/icons/general/add-redaction.svg', - title: this._translateService.instant('pdf-viewer.text-popup.actions.suggestion-redaction.label'), - onClick: () => { - const selectedQuads = this.instance.docViewer.getSelectedTextQuads(); - const text = this.instance.docViewer.getSelectedText(); - const entry: ManualRedactionEntry = {positions: []}; - for (const key of Object.keys(selectedQuads)) { - for (const quad of selectedQuads[key]) { - entry.positions.push(this.toPosition(parseInt(key, 10), quad)); - } - } - entry.value = text; - this.manualAnnotationRequested.emit(entry); - } - }); - } - - private toPosition(page: number, selectedQuad: any): Rectangle { - - const pageHeight = this.instance.docViewer.getPageHeight(page); - const height = selectedQuad.y2 - selectedQuad.y4; - return { - page: page, - topLeft: { - x: selectedQuad.x4, - y: pageHeight - (selectedQuad.y4 + height) - }, - height: height, - width: selectedQuad.x3 - selectedQuad.x4 - }; - } - - private _configureHeader() { - this.instance.setToolbarGroup('toolbarGroup-View'); - } - - - public selectAnnotation(annotation: Annotations.Annotation) { - this.instance.annotManager.deselectAllAnnotations(); - this.instance.annotManager.selectAnnotation(annotation); - this.navigateToPage(annotation.getPageNumber()); - } - - public navigateToPage(pageNumber: number) { - const activePage = this.instance.docViewer.getCurrentPage(); - if (activePage !== pageNumber) { - this.instance.docViewer.displayPageLocation(pageNumber, 0, 0); } - } - - - private _changeDocument() { - // sync layout and display mode - - - const instance = this.instance; - const docViewer = this.instance.docViewer; - - const lastScrolledViewerScrollElement = docViewer.getScrollViewElement(); - - const viewerState: ViewerState = { - displayMode: docViewer.getDisplayModeManager().getDisplayMode().mode, - layoutMode: instance.getLayoutMode(), - pageNumber: instance.docViewer.getCurrentPage(), - scrollLeft: lastScrolledViewerScrollElement.scrollLeft, - scrollTop: lastScrolledViewerScrollElement.scrollTop, - zoom: docViewer.getZoom(), - leftPanelState: instance.isElementOpen('leftPanel') - } - - this.instance.loadDocument(this.fileData, {filename: this.fileStatus ? this.fileStatus.filename : 'document.pdf'}); - - this._viewerState = viewerState; - - } - - private _restoreViewerState() { - this._restoreState(this._viewerState, this.instance); - } - - private _restoreState(viewerState: ViewerState, instance: WebViewerInstance) { - if (this._viewerState) { - instance.docViewer.setCurrentPage(viewerState.pageNumber); - instance.setLayoutMode(viewerState.layoutMode); - const instanceDisplayMode = instance.docViewer.getDisplayModeManager().getDisplayMode(); - instanceDisplayMode.mode = viewerState.displayMode; - instance.docViewer.getDisplayModeManager().setDisplayMode(instanceDisplayMode); - // Synchronize zoom - needs to be done before scrolling - instance.docViewer.zoomTo(viewerState.zoom); - const viewerScrollElement = instance.docViewer.getScrollViewElement(); - viewerScrollElement.scrollTo(viewerState.scrollLeft, viewerState.scrollTop); - - if (viewerState.leftPanelState) { - instance.openElements(['leftPanel']); - } else { - instance.closeElements(['leftPanel']); - } - } else { - // viewer init - this.instance.setFitMode('FitPage'); - } - } - - } - diff --git a/apps/red-ui/src/app/screens/project-listing-screen/project-listing-screen.component.html b/apps/red-ui/src/app/screens/project-listing-screen/project-listing-screen.component.html index 91ddf98ae..895fd5ba6 100644 --- a/apps/red-ui/src/app/screens/project-listing-screen/project-listing-screen.component.html +++ b/apps/red-ui/src/app/screens/project-listing-screen/project-listing-screen.component.html @@ -1,151 +1,188 @@ -
-
-
- - {{'project-listing.table-header.title.label'| translate:{ length: appStateService.allProjects?.length || 0 } }} - -
-
- - - - {{option.label | translate }} - - - -
+
+
+ + {{ + 'project-listing.table-header.title.label' + | translate: { length: appStateService.allProjects?.length || 0 } + }} + +
+
+ + + + {{ option.label | translate }} + + + +
+
+ +
+
+ +
+
+ +
+
+ +
+ +
+ +
+
+
+ {{ pw.project.projectName }} +
+
+
+ + {{ documentCount(pw) }} +
+
+ + {{ userCount(pw) }} +
+
+ + {{ pw.project.date | date: 'mediumDate' }} +
+
+ + {{ pw.project.dueDate | date: 'mediumDate' }} +
+
+
+
+ +
+
+ + +
+ + + +
+
+
+
-
-
- -
-
- -
-
- -
- -
- -
+
-
- {{pw.project.projectName}} -
-
-
- - {{documentCount(pw)}}
-
- - {{userCount(pw)}}
-
- - {{pw.project.date | date:'mediumDate'}} + + +
+
+ +
+
{{ totalPages }}
+
+
+
+ +
+ +
+
{{ totalPeople }}
+
+
+
-
- - {{pw.project.dueDate | date:'mediumDate'}} -
-
- +
-
- - -
- - - -
-
-
-
- -
- -
-
- - -
-
- -
-
{{totalPages}}
-
-
-
- -
- -
-
{{totalPeople}}
-
-
-
-
-
-
- -
-
- diff --git a/apps/red-ui/src/app/screens/project-listing-screen/project-listing-screen.component.scss b/apps/red-ui/src/app/screens/project-listing-screen/project-listing-screen.component.scss index e5fb4b227..ca0443bde 100644 --- a/apps/red-ui/src/app/screens/project-listing-screen/project-listing-screen.component.scss +++ b/apps/red-ui/src/app/screens/project-listing-screen/project-listing-screen.component.scss @@ -1,58 +1,58 @@ -@import "../../../assets/styles/red-mixins"; +@import '../../../assets/styles/red-mixins'; .add-project-btn { - mat-icon { - width: 14px; - margin-right: 10px; - color: $white; - } + mat-icon { + width: 14px; + margin-right: 10px; + color: $white; + } } .left-container { - width: calc(100vw - #{$right-container-width} - 130px); + width: calc(100vw - #{$right-container-width} - 130px); - .grid-container { - grid-template-columns: 2fr 1fr auto; - } + .grid-container { + grid-template-columns: 2fr 1fr auto; + } - .stats-subtitle { - margin-top: 6px; - } + .stats-subtitle { + margin-top: 6px; + } - .status-container { - width: 160px; - } + .status-container { + width: 160px; + } } .right-fixed-container { - display: flex; - width: 470px; - padding-top: 50px; - - > div { - flex: 1; display: flex; - flex-direction: column; - align-items: center; - } + width: 470px; + padding-top: 50px; - .project-stats-container { - width: fit-content; - - .project-stats-item { - display: flex; - width: fit-content; - gap: 5px; - margin-top: 25px; - - &:first-of-type { - margin-top: 50px; - } - - mat-icon { - height: 16px; - margin-top: 2px; - } + > div { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + } + + .project-stats-container { + width: fit-content; + + .project-stats-item { + display: flex; + width: fit-content; + gap: 5px; + margin-top: 25px; + + &:first-of-type { + margin-top: 50px; + } + + mat-icon { + height: 16px; + margin-top: 2px; + } + } } - } } diff --git a/apps/red-ui/src/app/screens/project-listing-screen/project-listing-screen.component.ts b/apps/red-ui/src/app/screens/project-listing-screen/project-listing-screen.component.ts index 29d82bae9..ba3ed65bd 100644 --- a/apps/red-ui/src/app/screens/project-listing-screen/project-listing-screen.component.ts +++ b/apps/red-ui/src/app/screens/project-listing-screen/project-listing-screen.component.ts @@ -1,112 +1,120 @@ -import {Component, OnInit} from '@angular/core'; -import {Project} from '@redaction/red-ui-http'; -import {AppStateService, ProjectWrapper} from '../../state/app-state.service'; -import {UserService} from '../../user/user.service'; -import {DoughnutChartConfig} from '../../components/simple-doughnut-chart/simple-doughnut-chart.component'; -import {SortingOption} from '../../utils/types'; -import {groupBy} from '../../utils/functions'; -import {DialogService} from '../../dialogs/dialog.service'; +import { Component, OnInit } from '@angular/core'; +import { Project } from '@redaction/red-ui-http'; +import { AppStateService, ProjectWrapper } from '../../state/app-state.service'; +import { UserService } from '../../user/user.service'; +import { DoughnutChartConfig } from '../../components/simple-doughnut-chart/simple-doughnut-chart.component'; +import { SortingOption } from '../../utils/types'; +import { groupBy } from '../../utils/functions'; +import { DialogService } from '../../dialogs/dialog.service'; @Component({ - selector: 'redaction-project-listing-screen', - templateUrl: './project-listing-screen.component.html', - styleUrls: ['./project-listing-screen.component.scss'] + selector: 'redaction-project-listing-screen', + templateUrl: './project-listing-screen.component.html', + styleUrls: ['./project-listing-screen.component.scss'] }) export class ProjectListingScreenComponent implements OnInit { - public projectsChartData: DoughnutChartConfig [] = []; - public documentsChartData: DoughnutChartConfig [] = []; - public sortingOptions: SortingOption[] = [ - {label: 'project-listing.sorting.recent.label', order: 'desc', column: 'projectDate'}, - {label: 'project-listing.sorting.alphabetically.label', order: 'asc', column: 'project.projectName'} - ]; - public sortingOption: SortingOption = this.sortingOptions[0]; - - constructor( - public readonly appStateService: AppStateService, - private readonly _userService: UserService, - private readonly _dialogService: DialogService - ) { - } - - ngOnInit(): void { - this.appStateService.reset(); - this._calculateData(); - this.appStateService.fileStatusChanged.subscribe(() => { - this._calculateData(); - }) - } - - private _calculateData() { - this.projectsChartData = [ - {value: this.activeProjects, color: 'ACTIVE', label: 'active'}, - {value: this.inactiveProjects, color: 'DELETED', label: 'archived'} + public projectsChartData: DoughnutChartConfig[] = []; + public documentsChartData: DoughnutChartConfig[] = []; + public sortingOptions: SortingOption[] = [ + { label: 'project-listing.sorting.recent.label', order: 'desc', column: 'projectDate' }, + { + label: 'project-listing.sorting.alphabetically.label', + order: 'asc', + column: 'project.projectName' + } ]; - const groups = groupBy(this.appStateService.aggregatedFiles, 'status'); - this.documentsChartData = []; - for (const key of Object.keys(groups)) { - this.documentsChartData.push({value: groups[key].length, color: key, label: key}); + public sortingOption: SortingOption = this.sortingOptions[0]; + + constructor( + public readonly appStateService: AppStateService, + private readonly _userService: UserService, + private readonly _dialogService: DialogService + ) {} + + ngOnInit(): void { + this.appStateService.reset(); + this._calculateData(); + this.appStateService.fileStatusChanged.subscribe(() => { + this._calculateData(); + }); } - } - public get user() { - return this._userService.user; - } + private _calculateData() { + this.projectsChartData = [ + { value: this.activeProjects, color: 'ACTIVE', label: 'active' }, + { value: this.inactiveProjects, color: 'DELETED', label: 'archived' } + ]; + const groups = groupBy(this.appStateService.aggregatedFiles, 'status'); + this.documentsChartData = []; + for (const key of Object.keys(groups)) { + this.documentsChartData.push({ value: groups[key].length, color: key, label: key }); + } + } - public get totalPages() { - return this.appStateService.totalAnalysedPages; - } + public get user() { + return this._userService.user; + } - public get totalPeople() { - return this.appStateService.totalPeople; - } + public get totalPages() { + return this.appStateService.totalAnalysedPages; + } - public get activeProjects() { - return this.appStateService.allProjects.reduce((i, p) => i + (p.project.status === Project.StatusEnum.ACTIVE ? 1 : 0), 0); - } + public get totalPeople() { + return this.appStateService.totalPeople; + } - public get inactiveProjects() { - return this.appStateService.allProjects.length - this.activeProjects; - } + public get activeProjects() { + return this.appStateService.allProjects.reduce( + (i, p) => i + (p.project.status === Project.StatusEnum.ACTIVE ? 1 : 0), + 0 + ); + } - public documentCount(project: ProjectWrapper) { - return project.files.length; - } + public get inactiveProjects() { + return this.appStateService.allProjects.length - this.activeProjects; + } - public userCount(project: ProjectWrapper) { - return 1; - } + public documentCount(project: ProjectWrapper) { + return project.files.length; + } - public openAddProjectDialog(): void { - this._dialogService.openAddProjectDialog(() => { - this._calculateData(); - }); - } + public userCount(project: ProjectWrapper) { + return 1; + } - public openDeleteProjectDialog($event: MouseEvent, project: Project) { - this._dialogService.openDeleteProjectDialog($event, project, () => { - this._calculateData(); - }); - } + public openAddProjectDialog(): void { + this._dialogService.openAddProjectDialog(() => { + this._calculateData(); + }); + } - public openProjectDetailsDialog($event: MouseEvent, project: ProjectWrapper) { - this._dialogService.openProjectDetailsDialog($event, project); - } + public openDeleteProjectDialog($event: MouseEvent, project: Project) { + this._dialogService.openDeleteProjectDialog($event, project, () => { + this._calculateData(); + }); + } - public openAssignProjectOwnerDialog($event: MouseEvent, project: Project) { - this._dialogService.openAssignProjectMembersAndOwnerDialog($event, project); - } + public openProjectDetailsDialog($event: MouseEvent, project: ProjectWrapper) { + this._dialogService.openProjectDetailsDialog($event, project); + } - public getProjectStatusConfig(pw: ProjectWrapper) { - const obj = pw.files.reduce((acc, file) => { - const status = file.status; - if (!acc[status]) { - acc[status] = 1; - } else { - acc[status]++; - } - return acc; - }, {}) + public openAssignProjectOwnerDialog($event: MouseEvent, project: Project) { + this._dialogService.openAssignProjectMembersAndOwnerDialog($event, project); + } - return Object.keys(obj).sort().map(status => ({length: obj[status], color: status})); - } + public getProjectStatusConfig(pw: ProjectWrapper) { + const obj = pw.files.reduce((acc, file) => { + const status = file.status; + if (!acc[status]) { + acc[status] = 1; + } else { + acc[status]++; + } + return acc; + }, {}); + + return Object.keys(obj) + .sort() + .map((status) => ({ length: obj[status], color: status })); + } } diff --git a/apps/red-ui/src/app/screens/project-overview-screen/project-overview-screen.component.html b/apps/red-ui/src/app/screens/project-overview-screen/project-overview-screen.component.html index 0a9356f9b..faf3e6c48 100644 --- a/apps/red-ui/src/app/screens/project-overview-screen/project-overview-screen.component.html +++ b/apps/red-ui/src/app/screens/project-overview-screen/project-overview-screen.component.html @@ -1,231 +1,317 @@ -
+
-
-
-
-
- - {{'project-overview.table-header.title.label'| translate:{ length: appStateService.activeProject?.files.length || 0 } }} - -
-
-
- - - - {{option.label | translate }} - - - -
+
+
+
+
+ + {{ + 'project-overview.table-header.title.label' + | translate + : { length: appStateService.activeProject?.files.length || 0 } + }} + +
+
+
+ + + + {{ option.label | translate }} + + + +
+
+ +
+ +
+ +
+ +
+ +
+ +
+ + +
+
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+
+
+
+ +
+ {{ fileStatus.filename }} +
+ +
+ {{ fileStatus.added | date: 'd MMM. yyyy, hh:mm a' }} +
+ +
+ + +
+ +
+ +
+ +
+ + +
+ + + + +
+
+
+
-
- -
- -
- -
- -
- -
- - -
-
- -
- -
- -
- -
- -
- -
- -
- -
-
-
- -
- {{ fileStatus.filename }} -
- -
- {{ fileStatus.added | date:'d MMM. yyyy, hh:mm a' }} -
- -
- - -
- -
- -
- -
- - -
- - - - -
-
-
-
- -
-
- - - -
- -
-
- - {{ appStateService.activeProject.files.length }} -
-
- - {{ appStateService.activeProject.project.memberIds.length }} -
-
- - {{ appStateService.activeProject.project.date | date:'d MMM. yyyy' }} -
-
- - {{appStateService.activeProject.project.dueDate | date:'mediumDate'}} -
-
- -
- {{ appStateService.activeProject.project.projectName }} -
- -
- -
- -
- {{ appStateService.activeProject.project.description }} -
- -
-
-
-
- -
-
-
+{{overflowCount}}
-
-
-
+
+
+
+ + {{ appStateService.activeProject.files.length }} +
+
+ + {{ appStateService.activeProject.project.memberIds.length }} +
+
+ + {{ appStateService.activeProject.project.date | date: 'd MMM. yyyy' }} +
+
+ + {{ appStateService.activeProject.project.dueDate | date: 'mediumDate' }} +
-
-
+
+ {{ appStateService.activeProject.project.projectName }} +
-
- -
+
+ +
-
-
- - {{ 'project-overview.legend.contains-hints.label' | translate }} -
-
- - {{ 'project-overview.legend.contains-redactions.label' | translate }} -
-
- - {{ 'project-overview.legend.contains-suggestions.label' | translate }} -
+
+ {{ appStateService.activeProject.project.description }} +
+ +
+
+
+
+ +
+
+
+{{ overflowCount }}
+
+
+
+
+
+
+
+ +
+ +
+ +
+
+ + {{ 'project-overview.legend.contains-hints.label' | translate }} +
+
+ + {{ 'project-overview.legend.contains-redactions.label' | translate }} +
+
+ + {{ 'project-overview.legend.contains-suggestions.label' | translate }} +
+
-
diff --git a/apps/red-ui/src/app/screens/project-overview-screen/project-overview-screen.component.scss b/apps/red-ui/src/app/screens/project-overview-screen/project-overview-screen.component.scss index c0b41e96d..91a082c05 100644 --- a/apps/red-ui/src/app/screens/project-overview-screen/project-overview-screen.component.scss +++ b/apps/red-ui/src/app/screens/project-overview-screen/project-overview-screen.component.scss @@ -1,76 +1,76 @@ -@import "../../../assets/styles/red-variables"; +@import '../../../assets/styles/red-variables'; .file-upload-input { - display: none; + display: none; } .select-all-container { - display: flex; - gap: 16px; - align-items: center; + display: flex; + gap: 16px; + align-items: center; - .select-oval { - margin-left: 0; - } + .select-oval { + margin-left: 0; + } } .pr-0 { - padding-right: 0 !important; + padding-right: 0 !important; } .select-oval { - width: 20px; - height: 20px; - border-radius: 50%; - border: 1px solid $grey-5; - background-color: $white; - cursor: pointer; + width: 20px; + height: 20px; + border-radius: 50%; + border: 1px solid $grey-5; + background-color: $white; + cursor: pointer; - &.active { - background-color: $primary; - } + &.active { + background-color: $primary; + } - &.placeholder { - visibility: hidden; - } + &.placeholder { + visibility: hidden; + } } .grid-container { - grid-template-columns: auto 3fr 2fr 1fr 2fr auto; + grid-template-columns: auto 3fr 2fr 1fr 2fr auto; - .table-item { - .table-item-title { - line-height: 80px; - } + .table-item { + .table-item-title { + line-height: 80px; + } - .needs-work { - display: flex; - flex-direction: row; - align-items: center; - justify-content: flex-start; - gap: 4px; + .needs-work { + display: flex; + flex-direction: row; + align-items: center; + justify-content: flex-start; + gap: 4px; + } } - } } .project-details-container { - .members-container { - gap: 5px; - } + .members-container { + gap: 5px; + } } .legend { - display: flex; - flex-direction: column; - gap: 8px; - - > div { display: flex; + flex-direction: column; gap: 8px; - align-items: center; - } + + > div { + display: flex; + gap: 8px; + align-items: center; + } } .mt-32 { - margin-top: 32px; + margin-top: 32px; } diff --git a/apps/red-ui/src/app/screens/project-overview-screen/project-overview-screen.component.ts b/apps/red-ui/src/app/screens/project-overview-screen/project-overview-screen.component.ts index 5cfd2cdbe..9cb91988b 100644 --- a/apps/red-ui/src/app/screens/project-overview-screen/project-overview-screen.component.ts +++ b/apps/red-ui/src/app/screens/project-overview-screen/project-overview-screen.component.ts @@ -1,6 +1,10 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; -import { FileStatus, ReanalysisControllerService, StatusControllerService } from '@redaction/red-ui-http'; +import { + FileStatus, + ReanalysisControllerService, + StatusControllerService +} from '@redaction/red-ui-http'; import { NotificationService } from '../../notification/notification.service'; import { AppStateService } from '../../state/app-state.service'; import { FileDropOverlayService } from '../../upload/file-drop/service/file-drop-overlay.service'; @@ -13,177 +17,210 @@ import { DoughnutChartConfig } from '../../components/simple-doughnut-chart/simp import { groupBy } from '../../utils/functions'; import { DialogService } from '../../dialogs/dialog.service'; - @Component({ - selector: 'redaction-project-overview-screen', - templateUrl: './project-overview-screen.component.html', - styleUrls: ['./project-overview-screen.component.scss'] + selector: 'redaction-project-overview-screen', + templateUrl: './project-overview-screen.component.html', + styleUrls: ['./project-overview-screen.component.scss'] }) export class ProjectOverviewScreenComponent implements OnInit, OnDestroy { - private _selectedFileIds: string[] = []; + private _selectedFileIds: string[] = []; - public sortingOptions: SortingOption[] = [ - { label: 'project-overview.sorting.recent.label', order: 'desc', column: 'lastUpdated' }, - { label: 'project-overview.sorting.oldest.label', order: 'asc', column: 'lastUpdated' }, - { label: 'project-overview.sorting.alphabetically.label', order: 'asc', column: 'filename' }, - { label: 'project-overview.sorting.number-of-pages.label', order: 'asc', column: 'numberOfPages' }, - { label: 'project-overview.sorting.number-of-analyses.label', order: 'desc', column: 'numberOfAnalyses' } - ]; - public sortingOption: SortingOption = this.sortingOptions[0]; - public documentsChartData: DoughnutChartConfig[] = []; + public sortingOptions: SortingOption[] = [ + { label: 'project-overview.sorting.recent.label', order: 'desc', column: 'lastUpdated' }, + { label: 'project-overview.sorting.oldest.label', order: 'asc', column: 'lastUpdated' }, + { + label: 'project-overview.sorting.alphabetically.label', + order: 'asc', + column: 'filename' + }, + { + label: 'project-overview.sorting.number-of-pages.label', + order: 'asc', + column: 'numberOfPages' + }, + { + label: 'project-overview.sorting.number-of-analyses.label', + order: 'desc', + column: 'numberOfAnalyses' + } + ]; + public sortingOption: SortingOption = this.sortingOptions[0]; + public documentsChartData: DoughnutChartConfig[] = []; - constructor(public readonly appStateService: AppStateService, - private readonly _activatedRoute: ActivatedRoute, - private readonly _statusControllerService: StatusControllerService, - private readonly _notificationService: NotificationService, - private readonly _dialogService: DialogService, - private readonly _fileUploadService: FileUploadService, - private readonly _uploadStatusOverlayService: UploadStatusOverlayService, - private readonly _reanalysisControllerService: ReanalysisControllerService, - private readonly _router: Router, - private readonly _userService: UserService, - private readonly _fileDropOverlayService: FileDropOverlayService) { - this._activatedRoute.params.subscribe(params => { - this.appStateService.activateProject(params.projectId); - }); + constructor( + public readonly appStateService: AppStateService, + private readonly _activatedRoute: ActivatedRoute, + private readonly _statusControllerService: StatusControllerService, + private readonly _notificationService: NotificationService, + private readonly _dialogService: DialogService, + private readonly _fileUploadService: FileUploadService, + private readonly _uploadStatusOverlayService: UploadStatusOverlayService, + private readonly _reanalysisControllerService: ReanalysisControllerService, + private readonly _router: Router, + private readonly _userService: UserService, + private readonly _fileDropOverlayService: FileDropOverlayService + ) { + this._activatedRoute.params.subscribe((params) => { + this.appStateService.activateProject(params.projectId); + }); - this.appStateService.fileStatusChanged.subscribe(() => { - this._calculateChartConfig(); - }) - } - - ngOnInit(): void { - this._fileDropOverlayService.initFileDropHandling(); - this._calculateChartConfig(); - } - - ngOnDestroy(): void { - this._fileDropOverlayService.cleanupFileDropHandling(); - } - - public get activeProject() { - return this.appStateService.activeProject.project; - } - - public get user() { - return this._userService.user; - } - - public get displayMembers() { - return this.activeProject.memberIds.slice(0, 6); - } - - public get overflowCount() { - return this.activeProject.memberIds.length > 6 ? this.activeProject.memberIds.length - 6 : 0; - } - - private _getFileStatus() { - this.appStateService.reloadActiveProjectFiles().then(() => { - this._calculateChartConfig(); - }); - } - - private _calculateChartConfig() { - const groups = groupBy(this.appStateService.activeProject.files, 'status'); - this.documentsChartData = []; - for (const key of Object.keys(groups)) { - this.documentsChartData.push({ value: groups[key].length, color: key, label: key }); - } - } - - public toggleFileSelected($event: MouseEvent, file: FileStatus) { - $event.stopPropagation(); - const idx = this._selectedFileIds.indexOf(file.fileId); - if (idx === -1) { - this._selectedFileIds.push(file.fileId); - } else { - this._selectedFileIds.splice(idx, 1); - } - } - - public toggleSelectAll() { - if (this.areAllFilesSelected()) { - this._selectedFileIds = []; - } else { - this._selectedFileIds = this.appStateService.activeProject.files.map(file => file.fileId); - } - } - - public areAllFilesSelected() { - return this.appStateService.activeProject.files.length !== 0 && - this._selectedFileIds.length === this.appStateService.activeProject.files.length; - } - - public isFileSelected(file: FileStatus) { - return this._selectedFileIds.indexOf(file.fileId) !== -1; - } - - public openDeleteFileDialog($event: MouseEvent, fileStatus: FileStatus) { - this._dialogService.openDeleteFileDialog($event, fileStatus.projectId, fileStatus.fileId, () => { - this._calculateChartConfig(); - }); - } - - public openDetailsDialog($event: MouseEvent) { - this._dialogService.openProjectDetailsDialog($event, this.appStateService.activeProject); - } - - public openEditProjectDialog($event: MouseEvent) { - this._dialogService.openEditProjectDialog($event, this.appStateService.activeProject.project); - } - - public openDeleteProjectDialog($event: MouseEvent) { - this._dialogService.openDeleteProjectDialog($event, this.appStateService.activeProject.project, () => { - this._router.navigate(['/ui/projects']); - }); - } - - public openAssignProjectMembersDialog() { - this._dialogService.openAssignProjectMembersAndOwnerDialog(null, this.activeProject, () => { - this._getFileStatus(); - }); - } - - public openAssignFileOwnerDialog($event: MouseEvent, file: FileStatus) { - this._dialogService.openAssignFileOwnerDialog($event, file, () => { - this._getFileStatus(); - }); - } - - public reanalyseFile($event: MouseEvent, fileStatus: FileStatus) { - $event.stopPropagation(); - this._reanalysisControllerService.reanalyzeFile(this.appStateService.activeProject.project.projectId, fileStatus.fileId).subscribe(() => { - this._getFileStatus(); - }); - } - - public fileId(index, item) { - return item.fileId; - } - - public uploadFiles(files: FileList | File[]) { - const uploadFiles: FileUploadModel[] = []; - for (let i = 0; i < files.length; i++) { - const file = files[i]; - uploadFiles.push({ - file: file, - progress: 0, - completed: false, - error: null - }); + this.appStateService.fileStatusChanged.subscribe(() => { + this._calculateChartConfig(); + }); } - this._fileUploadService.uploadFiles(uploadFiles); - this._uploadStatusOverlayService.openStatusOverlay(); - } + ngOnInit(): void { + this._fileDropOverlayService.initFileDropHandling(); + this._calculateChartConfig(); + } - public canOpenFile(fileStatus: FileStatus): boolean { - // TODO check correct condition for this - return fileStatus === 'PROCESSING' || fileStatus === 'REVIEWED' || true; - } + ngOnDestroy(): void { + this._fileDropOverlayService.cleanupFileDropHandling(); + } - public toggleSortByAddedOn() { - const sortedByRecent: boolean = (this.sortingOption === this.sortingOptions[0]); - this.sortingOption = sortedByRecent ? this.sortingOptions[1] : this.sortingOptions[0]; - } + public get activeProject() { + return this.appStateService.activeProject.project; + } + + public get user() { + return this._userService.user; + } + + public get displayMembers() { + return this.activeProject.memberIds.slice(0, 6); + } + + public get overflowCount() { + return this.activeProject.memberIds.length > 6 + ? this.activeProject.memberIds.length - 6 + : 0; + } + + private _getFileStatus() { + this.appStateService.reloadActiveProjectFiles().then(() => { + this._calculateChartConfig(); + }); + } + + private _calculateChartConfig() { + const groups = groupBy(this.appStateService.activeProject.files, 'status'); + this.documentsChartData = []; + for (const key of Object.keys(groups)) { + this.documentsChartData.push({ value: groups[key].length, color: key, label: key }); + } + } + + public toggleFileSelected($event: MouseEvent, file: FileStatus) { + $event.stopPropagation(); + const idx = this._selectedFileIds.indexOf(file.fileId); + if (idx === -1) { + this._selectedFileIds.push(file.fileId); + } else { + this._selectedFileIds.splice(idx, 1); + } + } + + public toggleSelectAll() { + if (this.areAllFilesSelected()) { + this._selectedFileIds = []; + } else { + this._selectedFileIds = this.appStateService.activeProject.files.map( + (file) => file.fileId + ); + } + } + + public areAllFilesSelected() { + return ( + this.appStateService.activeProject.files.length !== 0 && + this._selectedFileIds.length === this.appStateService.activeProject.files.length + ); + } + + public isFileSelected(file: FileStatus) { + return this._selectedFileIds.indexOf(file.fileId) !== -1; + } + + public openDeleteFileDialog($event: MouseEvent, fileStatus: FileStatus) { + this._dialogService.openDeleteFileDialog( + $event, + fileStatus.projectId, + fileStatus.fileId, + () => { + this._calculateChartConfig(); + } + ); + } + + public openDetailsDialog($event: MouseEvent) { + this._dialogService.openProjectDetailsDialog($event, this.appStateService.activeProject); + } + + public openEditProjectDialog($event: MouseEvent) { + this._dialogService.openEditProjectDialog( + $event, + this.appStateService.activeProject.project + ); + } + + public openDeleteProjectDialog($event: MouseEvent) { + this._dialogService.openDeleteProjectDialog( + $event, + this.appStateService.activeProject.project, + () => { + this._router.navigate(['/ui/projects']); + } + ); + } + + public openAssignProjectMembersDialog() { + this._dialogService.openAssignProjectMembersAndOwnerDialog(null, this.activeProject, () => { + this._getFileStatus(); + }); + } + + public openAssignFileOwnerDialog($event: MouseEvent, file: FileStatus) { + this._dialogService.openAssignFileOwnerDialog($event, file, () => { + this._getFileStatus(); + }); + } + + public reanalyseFile($event: MouseEvent, fileStatus: FileStatus) { + $event.stopPropagation(); + this._reanalysisControllerService + .reanalyzeFile(this.appStateService.activeProject.project.projectId, fileStatus.fileId) + .subscribe(() => { + this._getFileStatus(); + }); + } + + public fileId(index, item) { + return item.fileId; + } + + public uploadFiles(files: FileList | File[]) { + const uploadFiles: FileUploadModel[] = []; + for (let i = 0; i < files.length; i++) { + const file = files[i]; + uploadFiles.push({ + file: file, + progress: 0, + completed: false, + error: null + }); + } + + this._fileUploadService.uploadFiles(uploadFiles); + this._uploadStatusOverlayService.openStatusOverlay(); + } + + public canOpenFile(fileStatus: FileStatus): boolean { + // TODO check correct condition for this + return fileStatus === 'PROCESSING' || fileStatus === 'REVIEWED' || true; + } + + public toggleSortByAddedOn() { + const sortedByRecent: boolean = this.sortingOption === this.sortingOptions[0]; + this.sortingOption = sortedByRecent ? this.sortingOptions[1] : this.sortingOptions[0]; + } } diff --git a/apps/red-ui/src/app/state/app-state.service.ts b/apps/red-ui/src/app/state/app-state.service.ts index 5a7c7b17b..546a183e0 100644 --- a/apps/red-ui/src/app/state/app-state.service.ts +++ b/apps/red-ui/src/app/state/app-state.service.ts @@ -1,261 +1,298 @@ -import {EventEmitter, Injectable} from "@angular/core"; +import { EventEmitter, Injectable } from '@angular/core'; import { - FileStatus, - Project, - ProjectControllerService, - ReanalysisControllerService, - StatusControllerService -} from "@redaction/red-ui-http"; -import {NotificationService, NotificationType} from "../notification/notification.service"; -import {TranslateService} from "@ngx-translate/core"; -import {Router} from "@angular/router"; -import {UserService} from "../user/user.service"; -import {interval} from "rxjs"; -import {tap} from "rxjs/operators"; - + FileStatus, + Project, + ProjectControllerService, + ReanalysisControllerService, + StatusControllerService +} from '@redaction/red-ui-http'; +import { NotificationService, NotificationType } from '../notification/notification.service'; +import { TranslateService } from '@ngx-translate/core'; +import { Router } from '@angular/router'; +import { UserService } from '../user/user.service'; +import { interval } from 'rxjs'; +import { tap } from 'rxjs/operators'; export interface AppState { - - projects: ProjectWrapper[]; - activeProject: ProjectWrapper; - activeFile: FileStatus; - totalAnalysedPages?: number; - totalDocuments?: number; - totalPeople?: number; + projects: ProjectWrapper[]; + activeProject: ProjectWrapper; + activeFile: FileStatus; + totalAnalysedPages?: number; + totalDocuments?: number; + totalPeople?: number; } export class ProjectWrapper { - constructor(public project: Project, public files: FileStatus[]) { + constructor(public project: Project, public files: FileStatus[]) {} - } - - get projectDate() { - return this.project.date; - } + get projectDate() { + return this.project.date; + } } @Injectable({ - providedIn: 'root' + providedIn: 'root' }) export class AppStateService { + private _appState: AppState; - private _appState: AppState; + public fileStatusChanged = new EventEmitter(); - public fileStatusChanged = new EventEmitter(); + constructor( + private readonly _router: Router, + private readonly _userService: UserService, + private readonly _projectControllerService: ProjectControllerService, + private readonly _notificationService: NotificationService, + private readonly _reanalysisControllerService: ReanalysisControllerService, + private readonly _translateService: TranslateService, + private readonly _statusControllerService: StatusControllerService + ) { + this._appState = { + projects: [], + activeProject: null, + activeFile: null + }; - constructor( - private readonly _router: Router, - private readonly _userService: UserService, - private readonly _projectControllerService: ProjectControllerService, - private readonly _notificationService: NotificationService, - private readonly _reanalysisControllerService: ReanalysisControllerService, - private readonly _translateService: TranslateService, - private readonly _statusControllerService: StatusControllerService) { - this._appState = { - projects: [], - activeProject: null, - activeFile: null + interval(5000) + .pipe( + tap(() => { + this.reloadActiveProjectFiles(); + }) + ) + .subscribe(); + + interval(30000) + .pipe( + tap(() => { + this.loadAllProjects(); + }) + ) + .subscribe(); } - interval(5000).pipe(tap(() => { - this.reloadActiveProjectFiles(); - })).subscribe(); - - interval(30000).pipe(tap(() => { - this.loadAllProjects(); - })).subscribe(); - } - - - get isActiveProjectOwner() { - return this._appState.activeProject?.project?.ownerId === this._userService.userId - } - - get isActiveProjectMember() { - return this._appState.activeProject?.project?.memberIds?.indexOf(this._userService.userId) >= 0; - } - - get isActiveFileDocumentReviewer() { - return this._appState.activeFile?.currentReviewer === this._userService.userId; - } - - - get aggregatedFiles(): FileStatus[] { - const result: FileStatus[] = []; - this._appState.projects.forEach(p => { - result.push(...p.files); - }) - return result; - } - - get activeProjectId(): string { - return this._appState.activeProject?.project?.projectId; - } - - get activeProject(): ProjectWrapper { - return this._appState.activeProject; - } - - get allProjects(): ProjectWrapper[] { - return this._appState.projects; - } - - get activeFile(): FileStatus { - return this._appState.activeFile; - } - - get totalAnalysedPages() { - return this._appState.totalAnalysedPages; - } - - get totalPeople() { - return this._appState.totalPeople; - } - - get totalDocuments() { - return this._appState.totalDocuments; - } - - public getProjectById(id: string) { - return this.allProjects.find(project => project.project.projectId === id); - } - - public getFileById(projectId: string, fileId: string) { - return this.getProjectById(projectId).files.find(file => file.fileId === fileId); - } - - async loadAllProjects() { - const projects = await this._projectControllerService.getProjects().toPromise(); - if (projects) { - - const mappedProjects = projects.map(p => { - return new ProjectWrapper(p, this._getExistingFiles(p)); - }); - - for (const project of mappedProjects) { - await this.getFiles(project); - } - - this._appState.projects = mappedProjects; - this._computeStats(); + get isActiveProjectOwner() { + return this._appState.activeProject?.project?.ownerId === this._userService.userId; } - } - private _getExistingFiles(project: Project) { - const found = this._appState.projects.find(p => p.project.projectId === project.projectId); - return found ? found.files : []; - } + get isActiveProjectMember() { + return ( + this._appState.activeProject?.project?.memberIds?.indexOf(this._userService.userId) >= 0 + ); + } - async getFiles(project?: ProjectWrapper) { - const files = await this._statusControllerService.getProjectStatus(project.project.projectId).toPromise(); - const oldFiles = [...project.files]; + get isActiveFileDocumentReviewer() { + return this._appState.activeFile?.currentReviewer === this._userService.userId; + } - for (const file of files) { - let found = false; - for (const oldFile of oldFiles) { - if (oldFile.fileId === file.fileId) { - // emit when analysis count changed - if (oldFile.numberOfAnalyses !== file.numberOfAnalyses) { - this.fileStatusChanged.emit(oldFile); - } - found = true; - break; + get aggregatedFiles(): FileStatus[] { + const result: FileStatus[] = []; + this._appState.projects.forEach((p) => { + result.push(...p.files); + }); + return result; + } + + get activeProjectId(): string { + return this._appState.activeProject?.project?.projectId; + } + + get activeProject(): ProjectWrapper { + return this._appState.activeProject; + } + + get allProjects(): ProjectWrapper[] { + return this._appState.projects; + } + + get activeFile(): FileStatus { + return this._appState.activeFile; + } + + get totalAnalysedPages() { + return this._appState.totalAnalysedPages; + } + + get totalPeople() { + return this._appState.totalPeople; + } + + get totalDocuments() { + return this._appState.totalDocuments; + } + + public getProjectById(id: string) { + return this.allProjects.find((project) => project.project.projectId === id); + } + + public getFileById(projectId: string, fileId: string) { + return this.getProjectById(projectId).files.find((file) => file.fileId === fileId); + } + + async loadAllProjects() { + const projects = await this._projectControllerService.getProjects().toPromise(); + if (projects) { + const mappedProjects = projects.map((p) => { + return new ProjectWrapper(p, this._getExistingFiles(p)); + }); + + for (const project of mappedProjects) { + await this.getFiles(project); + } + + this._appState.projects = mappedProjects; + this._computeStats(); } - } - // emit for new file - if (!found) { - this.fileStatusChanged.emit(file); - } } - project.files = files; - - this._computeStats(); - return files; - } - - - activateProject(projectId: string) { - this._appState.activeFile = null; - this._appState.activeProject = this._appState.projects.find(p => p.project.projectId === projectId); - if (!this._appState.activeProject) { - this._router.navigate(['/ui/projects']); + private _getExistingFiles(project: Project) { + const found = this._appState.projects.find( + (p) => p.project.projectId === project.projectId + ); + return found ? found.files : []; } - return this._appState.activeProject; - } - activateFile(projectId: string, fileId: string) { - this._appState.activeFile = null; - this._appState.activeProject = this._appState.projects.find(p => p.project.projectId === projectId); - this._appState.activeFile = this._appState.activeProject.files.find(f => f.fileId === fileId); - } + async getFiles(project?: ProjectWrapper) { + const files = await this._statusControllerService + .getProjectStatus(project.project.projectId) + .toPromise(); + const oldFiles = [...project.files]; - reset() { - this._appState.activeFile = null; - this._appState.activeProject = null; - } + for (const file of files) { + let found = false; + for (const oldFile of oldFiles) { + if (oldFile.fileId === file.fileId) { + // emit when analysis count changed + if (oldFile.numberOfAnalyses !== file.numberOfAnalyses) { + this.fileStatusChanged.emit(oldFile); + } + found = true; + break; + } + } + // emit for new file + if (!found) { + this.fileStatusChanged.emit(file); + } + } - deleteProject(project: Project) { - return this._projectControllerService.deleteProject(project.projectId).toPromise().then(() => { - const index = this._appState.projects.findIndex(p => p.project.projectId === project.projectId); - this._appState.projects.splice(index, 1); - this._appState.projects = [...this._appState.projects]; - }, () => { - this._notificationService.showToastNotification(this._translateService.instant('projects.delete.delete-failed.label', project), null, NotificationType.ERROR) - }); - } + project.files = files; - async addOrUpdateProject(project: Project) { - try { - const updatedProject = await this._projectControllerService.createProjectOrUpdateProject(project).toPromise(); - const foundProject = this._appState.projects.find(p => p.project.projectId === updatedProject.projectId); - if (foundProject) { - Object.assign(foundProject.project, updatedProject); - } else { - this._appState.projects.push(new ProjectWrapper(updatedProject, [])); - } - this._appState.projects = [...this._appState.projects]; - } catch (error) { - this._notificationService.showToastNotification(this._translateService.instant('projects.add-edit-dialog.errors.save'), null, NotificationType.ERROR); + this._computeStats(); + return files; } - } - private _computeStats() { - let totalAnalysedPages = 0; - let totalDocuments = 0; - const totalPeople = new Set(); - this._appState.projects.forEach(p => { - totalDocuments += p.files.length; - if (p.project.memberIds) { - p.project.memberIds.forEach(m => totalPeople.add(m)); - } - p.files.forEach(f => { - totalAnalysedPages += f.numberOfPages; - }) - }) - - this._appState.totalPeople = totalPeople.size; - this._appState.totalAnalysedPages = totalAnalysedPages; - this._appState.totalDocuments = totalDocuments; - - } - - async reloadActiveProjectFiles() { - if (this._appState.activeProject) { - await this.getFiles(this._appState.activeProject); + activateProject(projectId: string) { + this._appState.activeFile = null; + this._appState.activeProject = this._appState.projects.find( + (p) => p.project.projectId === projectId + ); + if (!this._appState.activeProject) { + this._router.navigate(['/ui/projects']); + } + return this._appState.activeProject; } - } - async loadAllProjectsIfNecessary() { - if (!this._appState.projects.length) { - await this.loadAllProjects(); + activateFile(projectId: string, fileId: string) { + this._appState.activeFile = null; + this._appState.activeProject = this._appState.projects.find( + (p) => p.project.projectId === projectId + ); + this._appState.activeFile = this._appState.activeProject.files.find( + (f) => f.fileId === fileId + ); } - } - async reanalyseActiveFile() { - await this._reanalysisControllerService.reanalyzeFile(this._appState.activeProject.project.projectId, this._appState.activeFile.fileId).toPromise(); - await this.reloadActiveProjectFiles(); - } + reset() { + this._appState.activeFile = null; + this._appState.activeProject = null; + } + deleteProject(project: Project) { + return this._projectControllerService + .deleteProject(project.projectId) + .toPromise() + .then( + () => { + const index = this._appState.projects.findIndex( + (p) => p.project.projectId === project.projectId + ); + this._appState.projects.splice(index, 1); + this._appState.projects = [...this._appState.projects]; + }, + () => { + this._notificationService.showToastNotification( + this._translateService.instant( + 'projects.delete.delete-failed.label', + project + ), + null, + NotificationType.ERROR + ); + } + ); + } + async addOrUpdateProject(project: Project) { + try { + const updatedProject = await this._projectControllerService + .createProjectOrUpdateProject(project) + .toPromise(); + const foundProject = this._appState.projects.find( + (p) => p.project.projectId === updatedProject.projectId + ); + if (foundProject) { + Object.assign(foundProject.project, updatedProject); + } else { + this._appState.projects.push(new ProjectWrapper(updatedProject, [])); + } + this._appState.projects = [...this._appState.projects]; + } catch (error) { + this._notificationService.showToastNotification( + this._translateService.instant('projects.add-edit-dialog.errors.save'), + null, + NotificationType.ERROR + ); + } + } + + private _computeStats() { + let totalAnalysedPages = 0; + let totalDocuments = 0; + const totalPeople = new Set(); + this._appState.projects.forEach((p) => { + totalDocuments += p.files.length; + if (p.project.memberIds) { + p.project.memberIds.forEach((m) => totalPeople.add(m)); + } + p.files.forEach((f) => { + totalAnalysedPages += f.numberOfPages; + }); + }); + + this._appState.totalPeople = totalPeople.size; + this._appState.totalAnalysedPages = totalAnalysedPages; + this._appState.totalDocuments = totalDocuments; + } + + async reloadActiveProjectFiles() { + if (this._appState.activeProject) { + await this.getFiles(this._appState.activeProject); + } + } + + async loadAllProjectsIfNecessary() { + if (!this._appState.projects.length) { + await this.loadAllProjects(); + } + } + + async reanalyseActiveFile() { + await this._reanalysisControllerService + .reanalyzeFile( + this._appState.activeProject.project.projectId, + this._appState.activeFile.fileId + ) + .toPromise(); + await this.reloadActiveProjectFiles(); + } } diff --git a/apps/red-ui/src/assets/i18n/en.json b/apps/red-ui/src/assets/i18n/en.json index 39b152da1..e71cf0365 100644 --- a/apps/red-ui/src/assets/i18n/en.json +++ b/apps/red-ui/src/assets/i18n/en.json @@ -1,534 +1,534 @@ { - "auth-error": { - "heading": { - "label": "Your user doesn't have the required RED-* roles to access this application. Please contact your admin for access!" - }, - "heading-with-name-and-link": { - "label": "Your user doesn't have the required RED-* roles to access this application. Please contact {{adminName}} for access!" - }, - "heading-with-name": { - "label": "Your user doesn't have the required RED-* roles to access this application. Please contact {{adminName}} for access!" - }, - "heading-with-link": { - "label": "Your user doesn't have the required RED-* roles to access this application. Please contact your admin for access!" - }, - "logout": { - "label": "Logout" - } - }, - "manual-redaction": { - "confirm-annotation": { - "success": { - "label": "Annotation Confirmed!" - }, - "failed": { - "label": "Error confirming Annotation removal!" - } - }, - "remove-annotation": { - "success": { - "label": "Annotation Suggested for removal!" - }, - "failed": { - "label": "Error requesting Annotation removal!" - } - }, - "dialog": { - "header": { - "label": "Add Manual Redaction" - }, - "add-redaction": { - "success": { - "label": "Redaction suggestion added!" + "auth-error": { + "heading": { + "label": "Your user doesn't have the required RED-* roles to access this application. Please contact your admin for access!" }, - "failed": { - "label": "Failed to add manual redaction: {{message}}" - } - }, - "actions": { - "save": { - "label": "Save Manual Redaction" - } - }, - "content": { - "text": { - "label": "Selected text:" + "heading-with-name-and-link": { + "label": "Your user doesn't have the required RED-* roles to access this application. Please contact {{adminName}} for access!" }, - "dictionary": { - "add": { - "label": "Add to dictionary" - }, - "label": "Type" + "heading-with-name": { + "label": "Your user doesn't have the required RED-* roles to access this application. Please contact {{adminName}} for access!" }, - "reason": { - "label": "Reason" + "heading-with-link": { + "label": "Your user doesn't have the required RED-* roles to access this application. Please contact your admin for access!" }, - "comment": { - "label": "Comment" - } - } - } - }, - "app-name": { - "label": "Redacto" - }, - "upload-status": { - "dialog": { - "title": { - "label": "File Upload" - }, - "actions": { - "re-upload": { - "label": "Retry Upload" - }, - "cancel": { - "label": "Cancel Upload" - } - } - } - }, - "pdf-viewer": { - "text-popup": { - "actions": { - "suggestion-redaction": { - "label": "Suggest Redaction" - } - } - } - }, - "common": { - "dialog": { - "close": { - "label": "Close Dialog" - } - }, - "confirmation-dialog": { - "title": { - "label": "Confirm Action" - }, - "description": { - "label": "This action requires confirmation, do you wish to proceed?" - }, - "confirm": { - "label": "Yes" - }, - "deny": { - "label": "No" - } - } - }, - "top-bar": { - "navigation-items": { - "projects": { - "label": "Projects" - }, - "my-account": { - "label": "My Account", - "children": { - "language": { - "label": "Language", - "english": { - "label": "English" - }, - "german": { - "label": "German" - } - }, - "logout": { + "logout": { "label": "Logout" - } } - } - } - }, - "filters": { - "filter-by": { - "label": "Filter by:" }, - "status": { - "label": "Status" - }, - "people": { - "label": "People" - }, - "due-date": { - "label": "Due Date" - }, - "created-on": { - "label": "Created On" - }, - "project": { - "label": "Project" - }, - "document": { - "label": "Document" - } - }, - "project-listing": { - "table-header": { - "title": { - "label": "{{length}} active projects" - }, - "bulk-select": { - "label": "Bulk select" - }, - "recent": { - "label": "Recent" - } - }, - "table-col-names": { - "name": { - "label": "Name" - }, - "owner": { - "label": "Owner" - }, - "status": { - "label": "Status" - } - }, - "stats": { - "analyzed-pages": { - "label": "Analyzed pages" - }, - "total-people": { - "label": "Total people" - }, - "charts": { - "projects": { - "label": "Projects" + "manual-redaction": { + "confirm-annotation": { + "success": { + "label": "Annotation Confirmed!" + }, + "failed": { + "label": "Error confirming Annotation removal!" + } }, - "total-documents": { - "label": "Total Documents" + "remove-annotation": { + "success": { + "label": "Annotation Suggested for removal!" + }, + "failed": { + "label": "Error requesting Annotation removal!" + } + }, + "dialog": { + "header": { + "label": "Add Manual Redaction" + }, + "add-redaction": { + "success": { + "label": "Redaction suggestion added!" + }, + "failed": { + "label": "Failed to add manual redaction: {{message}}" + } + }, + "actions": { + "save": { + "label": "Save Manual Redaction" + } + }, + "content": { + "text": { + "label": "Selected text:" + }, + "dictionary": { + "add": { + "label": "Add to dictionary" + }, + "label": "Type" + }, + "reason": { + "label": "Reason" + }, + "comment": { + "label": "Comment" + } + } } - } }, - "add-edit-dialog": { - "header-new": { - "label": "New Project" - }, - "header-edit": { - "label": "Edit Project" - }, - "form": { - "description": { - "label": "Description" + "app-name": { + "label": "Redacto" + }, + "upload-status": { + "dialog": { + "title": { + "label": "File Upload" + }, + "actions": { + "re-upload": { + "label": "Retry Upload" + }, + "cancel": { + "label": "Cancel Upload" + } + } + } + }, + "pdf-viewer": { + "text-popup": { + "actions": { + "suggestion-redaction": { + "label": "Suggest Redaction" + } + } + } + }, + "common": { + "dialog": { + "close": { + "label": "Close Dialog" + } }, - "name": { - "label": "Name" + "confirmation-dialog": { + "title": { + "label": "Confirm Action" + }, + "description": { + "label": "This action requires confirmation, do you wish to proceed?" + }, + "confirm": { + "label": "Yes" + }, + "deny": { + "label": "No" + } + } + }, + "top-bar": { + "navigation-items": { + "projects": { + "label": "Projects" + }, + "my-account": { + "label": "My Account", + "children": { + "language": { + "label": "Language", + "english": { + "label": "English" + }, + "german": { + "label": "German" + } + }, + "logout": { + "label": "Logout" + } + } + } + } + }, + "filters": { + "filter-by": { + "label": "Filter by:" + }, + "status": { + "label": "Status" + }, + "people": { + "label": "People" }, "due-date": { - "label": "Due Date" - } - }, - "actions": { - "save": { - "label": "Save Project" - } - } - }, - "header": { - "label": "Projects" - }, - "edit": { - "action": { - "label": "Edit Project" - } - }, - "delete": { - "action": { - "label": "Delete Project" - }, - "delete-failed": { - "label": "Failed to delete project: {{projectName}}" - } - }, - "add-new": { - "label": "New Project" - }, - "no-projects": { - "label": "You currently have no projects. You can start your work by creating a new one!" - }, - "sorting": { - "recent": { - "label": "Recent" - }, - "alphabetically": { - "label": "Alphabetically" - } - } - }, - "file-details": { - "dialog": { - "title": { - "label": "File Details" - }, - "actions": { - "download-redaction-report": { - "label": "Download Redaction Report" - } - } - } - }, - "project-details": { - "dialog": { - "title": { - "label": "Project Details" - }, - "info": { - "file-count": { - "label": "Number of files: {{fileCount}}" - } - }, - "actions": { - "download-redaction-report": { - "label": "Download Redaction Report" + "label": "Due Date" }, - "reanalyse-project": { - "label": "Reanalyse Project" + "created-on": { + "label": "Created On" + }, + "project": { + "label": "Project" + }, + "document": { + "label": "Document" } - } - } - }, - "project-overview": { - "table-header": { - "title": { - "label": "{{length}} documents" - }, - "bulk-select": { - "label": "Bulk select" - }, - "recent": { - "label": "Recent" - } }, - "table-col-names": { - "name": { - "label": "Name" - }, - "added-on": { - "label": "Added on" - }, - "needs-work": { - "label": "Needs work" - }, - "assigned-to": { - "label": "Assigned to" - }, - "status": { - "label": "Status" - } - }, - "sorting": { - "recent": { - "label": "Recent" - }, - "oldest": { - "label": "Oldest" - }, - "alphabetically": { - "label": "Alphabetically" - }, - "number-of-pages": { - "label": "Number of pages" - }, - "number-of-analyses": { - "label": "Number of analyses" - } - }, - "upload-error": { - "label": "Failed to upload file: {{name}}" - }, - "delete-file-error": { - "label": "Failed to delete file: {{filename}}" - }, - "reanalyse": { - "action": { - "label": "Reanalyse File" - } - }, - "delete": { - "action": { - "label": "Delete File" - } - }, - "file-listing": { - "file-entry": { - "status": { - "label": "Status: {{status}}" + "project-listing": { + "table-header": { + "title": { + "label": "{{length}} active projects" + }, + "bulk-select": { + "label": "Bulk select" + }, + "recent": { + "label": "Recent" + } }, - "number-of-pages": { - "label": "Number of pages: {{numberOfPages}}" + "table-col-names": { + "name": { + "label": "Name" + }, + "owner": { + "label": "Owner" + }, + "status": { + "label": "Status" + } }, - "number-of-analyses": { - "label": "Analysis count: {{numberOfAnalyses}}" + "stats": { + "analyzed-pages": { + "label": "Analyzed pages" + }, + "total-people": { + "label": "Total people" + }, + "charts": { + "projects": { + "label": "Projects" + }, + "total-documents": { + "label": "Total Documents" + } + } }, - "added": { - "label": "Date added: {{added}}" + "add-edit-dialog": { + "header-new": { + "label": "New Project" + }, + "header-edit": { + "label": "Edit Project" + }, + "form": { + "description": { + "label": "Description" + }, + "name": { + "label": "Name" + }, + "due-date": { + "label": "Due Date" + } + }, + "actions": { + "save": { + "label": "Save Project" + } + } }, - "last-updated": { - "label": "Last updated: {{lastUpdated}}" + "header": { + "label": "Projects" + }, + "edit": { + "action": { + "label": "Edit Project" + } + }, + "delete": { + "action": { + "label": "Delete Project" + }, + "delete-failed": { + "label": "Failed to delete project: {{projectName}}" + } + }, + "add-new": { + "label": "New Project" + }, + "no-projects": { + "label": "You currently have no projects. You can start your work by creating a new one!" + }, + "sorting": { + "recent": { + "label": "Recent" + }, + "alphabetically": { + "label": "Alphabetically" + } + } + }, + "file-details": { + "dialog": { + "title": { + "label": "File Details" + }, + "actions": { + "download-redaction-report": { + "label": "Download Redaction Report" + } + } } - } }, "project-details": { - "project-team": { - "label": "Project team" - }, - "charts": { - "total-documents": { - "label": "Total Documents" + "dialog": { + "title": { + "label": "Project Details" + }, + "info": { + "file-count": { + "label": "Number of files: {{fileCount}}" + } + }, + "actions": { + "download-redaction-report": { + "label": "Download Redaction Report" + }, + "reanalyse-project": { + "label": "Reanalyse Project" + } + } } - } }, - "header": { - "label": "Project Overview" - }, - "upload-document": { - "label": "Upload Document" - }, - "no-project": { - "label": "Requested project: {{projectId}} does not exist! Back to Project Listing. " - }, - "legend": { - "contains-hints": { - "label": "Contains hints " - }, - "contains-redactions": { - "label": "Contains redactions " - }, - "contains-suggestions": { - "label": "Contains suggestions for redaction " - } - } - }, - "file-preview": { - "view-toggle": { - "label": "Redacted View" - }, - "filter-menu": { - "label": "Filter", - "all": { - "label": "All" - }, - "none": { - "label": "None" - }, - "filter-types": { - "label": "Filter types" - }, - "hint": { - "label": "Hint annotation", - "hint_only": { - "label": "Hint only" + "project-overview": { + "table-header": { + "title": { + "label": "{{length}} documents" + }, + "bulk-select": { + "label": "Bulk select" + }, + "recent": { + "label": "Recent" + } }, - "vertebrate": { - "label": "Vertebrate" + "table-col-names": { + "name": { + "label": "Name" + }, + "added-on": { + "label": "Added on" + }, + "needs-work": { + "label": "Needs work" + }, + "assigned-to": { + "label": "Assigned to" + }, + "status": { + "label": "Status" + } }, - "names": { - "label": "Names" + "sorting": { + "recent": { + "label": "Recent" + }, + "oldest": { + "label": "Oldest" + }, + "alphabetically": { + "label": "Alphabetically" + }, + "number-of-pages": { + "label": "Number of pages" + }, + "number-of-analyses": { + "label": "Number of analyses" + } + }, + "upload-error": { + "label": "Failed to upload file: {{name}}" + }, + "delete-file-error": { + "label": "Failed to delete file: {{filename}}" + }, + "reanalyse": { + "action": { + "label": "Reanalyse File" + } + }, + "delete": { + "action": { + "label": "Delete File" + } + }, + "file-listing": { + "file-entry": { + "status": { + "label": "Status: {{status}}" + }, + "number-of-pages": { + "label": "Number of pages: {{numberOfPages}}" + }, + "number-of-analyses": { + "label": "Analysis count: {{numberOfAnalyses}}" + }, + "added": { + "label": "Date added: {{added}}" + }, + "last-updated": { + "label": "Last updated: {{lastUpdated}}" + } + } + }, + "project-details": { + "project-team": { + "label": "Project team" + }, + "charts": { + "total-documents": { + "label": "Total Documents" + } + } + }, + "header": { + "label": "Project Overview" + }, + "upload-document": { + "label": "Upload Document" + }, + "no-project": { + "label": "Requested project: {{projectId}} does not exist! Back to Project Listing. " + }, + "legend": { + "contains-hints": { + "label": "Contains hints " + }, + "contains-redactions": { + "label": "Contains redactions " + }, + "contains-suggestions": { + "label": "Contains suggestions for redaction " + } } - }, - "redaction": { - "label": "Redaction" - }, - "comment": { - "label": "Comment annotation" - }, - "suggestion": { - "label": "Suggested redaction" - }, - "ignore": { - "label": "Ignored redaction" - } }, - "tabs": { - "quick-navigation": { - "label": "Quick Navigation" - }, - "annotations": { - "label": "Annotations" - }, - "info": { - "label": "Info", - "assign-reviewer": { - "label": "Assign reviewer" + "file-preview": { + "view-toggle": { + "label": "Redacted View" }, - "added-on": { - "label": "Added on" + "filter-menu": { + "label": "Filter", + "all": { + "label": "All" + }, + "none": { + "label": "None" + }, + "filter-types": { + "label": "Filter types" + }, + "hint": { + "label": "Hint annotation", + "hint_only": { + "label": "Hint only" + }, + "vertebrate": { + "label": "Vertebrate" + }, + "names": { + "label": "Names" + } + }, + "redaction": { + "label": "Redaction" + }, + "comment": { + "label": "Comment annotation" + }, + "suggestion": { + "label": "Suggested redaction" + }, + "ignore": { + "label": "Ignored redaction" + } }, - "added-by": { - "label": "Added by" + "tabs": { + "quick-navigation": { + "label": "Quick Navigation" + }, + "annotations": { + "label": "Annotations" + }, + "info": { + "label": "Info", + "assign-reviewer": { + "label": "Assign reviewer" + }, + "added-on": { + "label": "Added on" + }, + "added-by": { + "label": "Added by" + }, + "last-modified-on": { + "label": "Last modified on" + } + } }, - "last-modified-on": { - "label": "Last modified on" + "download": { + "label": "Download", + "dropdown": { + "original": { + "label": "Original" + }, + "annotated": { + "label": "Annotated" + }, + "redacted": { + "label": "Redacted" + } + } } - } }, - "download": { - "label": "Download", - "dropdown": { - "original": { - "label": "Original" - }, - "annotated": { - "label": "Annotated" - }, - "redacted": { - "label": "Redacted" + "initials-avatar": { + "unassigned": { + "label": "Unassigned" } - } - } - }, - "initials-avatar": { - "unassigned": { - "label": "Unassigned" - } - }, - "assign-file-owner": { - "dialog": { - "single-user": { - "label": "Reviewer" - }, - "title": { - "label": "Manage File Reviewer" - }, - "save": { - "label": "Save" - } - } - }, - "assign-project-owner": { - "dialog": { - "single-user": { - "label": "Owner" - }, - "multi-user": { - "label": "Members" - }, - "title": { - "label": "Manage Project Owner and Members" - }, - "save": { - "label": "Save" - } - } - }, - "unassigned": "Unassigned", - "under-review": "Under review", - "under-approval": "Under approval", - "efsa": "EFSA approval", - "finished": "Finished", - "approved": "Approved", - "submitted": "Submitted", - "active": "Active", - "archived": "Archived", - "hint": "Hint", - "ignore": "Ignore", - "redaction": "Redaction", - "comment": "Comment", - "suggestion": "Suggestion for redaction", - "dictionary": "Dictionary", - "content": "Content", - "page": "Page" + }, + "assign-file-owner": { + "dialog": { + "single-user": { + "label": "Reviewer" + }, + "title": { + "label": "Manage File Reviewer" + }, + "save": { + "label": "Save" + } + } + }, + "assign-project-owner": { + "dialog": { + "single-user": { + "label": "Owner" + }, + "multi-user": { + "label": "Members" + }, + "title": { + "label": "Manage Project Owner and Members" + }, + "save": { + "label": "Save" + } + } + }, + "unassigned": "Unassigned", + "under-review": "Under review", + "under-approval": "Under approval", + "efsa": "EFSA approval", + "finished": "Finished", + "approved": "Approved", + "submitted": "Submitted", + "active": "Active", + "archived": "Archived", + "hint": "Hint", + "ignore": "Ignore", + "redaction": "Redaction", + "comment": "Comment", + "suggestion": "Suggestion for redaction", + "dictionary": "Dictionary", + "content": "Content", + "page": "Page" } diff --git a/apps/red-ui/src/assets/styles/red-checkbox.scss b/apps/red-ui/src/assets/styles/red-checkbox.scss index 5ab0c4c63..21457c3ee 100644 --- a/apps/red-ui/src/assets/styles/red-checkbox.scss +++ b/apps/red-ui/src/assets/styles/red-checkbox.scss @@ -13,17 +13,17 @@ } .mat-checkbox-layout { - .mat-checkbox-inner-container { - margin-left: 0; - } + .mat-checkbox-inner-container { + margin-left: 0; + } - .mat-checkbox-label { - font-family: 'Inter', sans-serif; - font-size: 13px; - color: $accent; + .mat-checkbox-label { + font-family: 'Inter', sans-serif; + font-size: 13px; + color: $accent; - display: flex; - align-items: center; - gap: 4px; - } + display: flex; + align-items: center; + gap: 4px; + } } diff --git a/apps/red-ui/src/assets/styles/red-dialog.scss b/apps/red-ui/src/assets/styles/red-dialog.scss index d9609a2f8..6c3c8115a 100644 --- a/apps/red-ui/src/assets/styles/red-dialog.scss +++ b/apps/red-ui/src/assets/styles/red-dialog.scss @@ -1,5 +1,5 @@ .mat-dialog-container { - border-radius: 8px; + border-radius: 8px; } .dialog { diff --git a/apps/red-ui/src/assets/styles/red-input.scss b/apps/red-ui/src/assets/styles/red-input.scss index e60713169..283afa080 100644 --- a/apps/red-ui/src/assets/styles/red-input.scss +++ b/apps/red-ui/src/assets/styles/red-input.scss @@ -1,63 +1,65 @@ -@import "red-variables"; -@import "red-mixins"; +@import 'red-variables'; +@import 'red-mixins'; .red-input-group { - display: flex; - flex-direction: column; - margin-top: 13px; + display: flex; + flex-direction: column; + margin-top: 13px; - &:first-child { - margin-top: 0; - } - - input, textarea, mat-select { - box-sizing: border-box; - width: 322px; - padding-left: 11px; - padding-right: 11px; - border: 1px solid $grey-5; - font-family: Inter, sans-serif; - font-size: 11px; - line-height: 14px; - background-color: #FFFFFF; - border-radius: 8px; - outline: none; - margin-top: 3px; - min-height: 34px; - - &:focus { - border-color: $grey-1; - } - } - - mat-select { - width: 220px; - - .mat-select-trigger { - height: 32px; + &:first-child { + margin-top: 0; } - .mat-select-value { - vertical-align: middle; + input, + textarea, + mat-select { + box-sizing: border-box; + width: 322px; + padding-left: 11px; + padding-right: 11px; + border: 1px solid $grey-5; + font-family: Inter, sans-serif; + font-size: 11px; + line-height: 14px; + background-color: #ffffff; + border-radius: 8px; + outline: none; + margin-top: 3px; + min-height: 34px; + + &:focus { + border-color: $grey-1; + } } - } - textarea { - resize: none; - padding-top: 7px; - padding-bottom: 7px; - @include no-scroll-bar(); - } + mat-select { + width: 220px; - label { - opacity: 0.7; - font-size: 11px; - letter-spacing: 0; - line-height: 14px; - margin-bottom: 2px; + .mat-select-trigger { + height: 32px; + } - &.mat-checkbox-layout { - opacity: 1; + .mat-select-value { + vertical-align: middle; + } + } + + textarea { + resize: none; + padding-top: 7px; + padding-bottom: 7px; + @include no-scroll-bar(); + } + + label { + opacity: 0.7; + font-size: 11px; + letter-spacing: 0; + line-height: 14px; + margin-bottom: 2px; + + &.mat-checkbox-layout { + opacity: 1; + } } - } } diff --git a/apps/red-ui/src/assets/styles/red-menu.scss b/apps/red-ui/src/assets/styles/red-menu.scss index 14d760557..bed241428 100644 --- a/apps/red-ui/src/assets/styles/red-menu.scss +++ b/apps/red-ui/src/assets/styles/red-menu.scss @@ -1,29 +1,29 @@ -@import "red-variables"; +@import 'red-variables'; .mat-menu-panel { - border-radius: 8px !important; - box-shadow: 0 2px 6px 0 rgba(40, 50, 65, 0.3); - max-width: none !important; + border-radius: 8px !important; + box-shadow: 0 2px 6px 0 rgba(40, 50, 65, 0.3); + max-width: none !important; - .mat-menu-item { - font-family: 'Inter', sans-serif; - font-size: 13px; - color: $accent; + .mat-menu-item { + font-family: 'Inter', sans-serif; + font-size: 13px; + color: $accent; - .arrow-wrapper { - width: 16px; - margin-right: 8px; - text-align: center; + .arrow-wrapper { + width: 16px; + margin-right: 8px; + text-align: center; - mat-icon { - width: 9px; - height: 9px; - margin: 0; - } + mat-icon { + width: 9px; + height: 9px; + margin: 0; + } + } + + &.padding-left { + padding-left: 64px; + } } - - &.padding-left { - padding-left: 64px; - } - } } diff --git a/apps/red-ui/src/assets/styles/red-mixins.scss b/apps/red-ui/src/assets/styles/red-mixins.scss index 4e6d5bc70..8930792e3 100644 --- a/apps/red-ui/src/assets/styles/red-mixins.scss +++ b/apps/red-ui/src/assets/styles/red-mixins.scss @@ -10,10 +10,10 @@ } @mixin no-scroll-bar { - scrollbar-width: none; /* Firefox */ - -ms-overflow-style: none; /* IE 10+ */ - &::-webkit-scrollbar { - width: 0; - background: transparent; /* Chrome/Safari/Webkit */ - } + scrollbar-width: none; /* Firefox */ + -ms-overflow-style: none; /* IE 10+ */ + &::-webkit-scrollbar { + width: 0; + background: transparent; /* Chrome/Safari/Webkit */ + } } diff --git a/apps/red-ui/src/assets/styles/red-page-layout.scss b/apps/red-ui/src/assets/styles/red-page-layout.scss index 5137d1761..becfe3218 100644 --- a/apps/red-ui/src/assets/styles/red-page-layout.scss +++ b/apps/red-ui/src/assets/styles/red-page-layout.scss @@ -1,181 +1,182 @@ -@import "red-variables"; +@import 'red-variables'; -html, body { - margin: 0; - padding: 0; - height: 100vh; - font-family: Inter, sans-serif; - color: $accent; - font-size: 13px; - line-height: 16px; +html, +body { + margin: 0; + padding: 0; + height: 100vh; + font-family: Inter, sans-serif; + color: $accent; + font-size: 13px; + line-height: 16px; } .page-header { - display: flex; - justify-content: space-between; - align-items: center; - padding: 0 24px; - height: 50px; - box-shadow: 0 2px 4px 0 $grey-4; - position: fixed; - top: 61px; - width: 100vw; - box-sizing: border-box; - background-color: $white; - z-index: 3; + display: flex; + justify-content: space-between; + align-items: center; + padding: 0 24px; + height: 50px; + box-shadow: 0 2px 4px 0 $grey-4; + position: fixed; + top: 61px; + width: 100vw; + box-sizing: border-box; + background-color: $white; + z-index: 3; } .red-content-inner { - margin-top: 50px; + margin-top: 50px; } .right-fixed-container { - border-left: 1px solid $grey-4; - height: 100%; - width: $right-container-inside-width; - padding: $right-container-padding; - position: fixed; - right: 0; + border-left: 1px solid $grey-4; + height: 100%; + width: $right-container-inside-width; + padding: $right-container-padding; + position: fixed; + right: 0; - .actions-row { - display: flex; - height: 40px; + .actions-row { + display: flex; + height: 40px; - > div { - padding: 10px; + > div { + padding: 10px; - mat-icon { - cursor: pointer; - width: 14px; + mat-icon { + cursor: pointer; + width: 14px; - &:hover { - color: $primary; + &:hover { + color: $primary; + } + } } - } } - } } .filters { - div { - margin-right: 6px; - } + div { + margin-right: 6px; + } - button { - flex-direction: row-reverse; - } + button { + flex-direction: row-reverse; + } } .flex-row { - display: flex; - align-items: center; + display: flex; + align-items: center; } .flex { - display: flex !important; + display: flex !important; } .flex-end { - display: flex; - justify-content: flex-end; + display: flex; + justify-content: flex-end; } .flex-1 { - flex: 1; + flex: 1; } .flex-2 { - flex: 2; + flex: 2; } .mt-5 { - margin-top: 5px; + margin-top: 5px; } .mt-20 { - margin-top: 20px; + margin-top: 20px; } .left-container { - height: calc(100vh - 61px - 50px); - width: calc(100vw - #{$right-container-width}); + height: calc(100vh - 61px - 50px); + width: calc(100vw - #{$right-container-width}); } .break-20 { - height: 20px; - background: transparent; - display: block; + height: 20px; + background: transparent; + display: block; } .detail-row { - opacity: 1; - font-family: Inconsolata, monospace, monospace; - font-size: 14px; - letter-spacing: 0; - line-height: 14px; - padding: 4px; + opacity: 1; + font-family: Inconsolata, monospace, monospace; + font-size: 14px; + letter-spacing: 0; + line-height: 14px; + padding: 4px; } .red-top-bar { - height: 61px; - width: 100%; - max-height: 61px; - display: flex; - flex-direction: column; - - .top-bar-row { - height: 60px; + height: 61px; + width: 100%; + max-height: 61px; display: flex; - justify-content: space-between; - padding: 0 24px; + flex-direction: column; - .center { - display: flex; - align-items: center; - justify-content: center; + .top-bar-row { + height: 60px; + display: flex; + justify-content: space-between; + padding: 0 24px; + + .center { + display: flex; + align-items: center; + justify-content: center; + } + + .app-name { + margin-left: 16px; + height: 20px; + font-size: 16px; + font-weight: 600; + letter-spacing: 0; + line-height: 20px; + } + + .menu { + display: flex; + align-items: center; + overflow: hidden; + + &.right { + justify-content: flex-end; + } + } } - .app-name { - margin-left: 16px; - height: 20px; - font-size: 16px; - font-weight: 600; - letter-spacing: 0; - line-height: 20px; + .divider { + height: 1px; + opacity: 0.15; + background-color: $grey-1; } - - .menu { - display: flex; - align-items: center; - overflow: hidden; - - &.right { - justify-content: flex-end; - } - } - } - - .divider { - height: 1px; - opacity: 0.15; - background-color: $grey-1; - } } .red-content { - width: 100vw; - height: calc(100vh - 61px); - overflow: auto; + width: 100vw; + height: calc(100vh - 61px); + overflow: auto; } .hidden { - display: none !important; + display: none !important; } .not-visible { - visibility: hidden; + visibility: hidden; } .pointer { - cursor: pointer; + cursor: pointer; } diff --git a/apps/red-ui/src/assets/styles/red-tables.scss b/apps/red-ui/src/assets/styles/red-tables.scss index 908a86093..f4c44891d 100644 --- a/apps/red-ui/src/assets/styles/red-tables.scss +++ b/apps/red-ui/src/assets/styles/red-tables.scss @@ -1,98 +1,97 @@ -@import "red-variables"; -@import "red-mixins"; +@import 'red-variables'; +@import 'red-mixins'; .table-header { - background-color: rgba(226, 228, 233, 0.9); - height: 50px; - padding: 0 16px; - display: flex; - justify-content: space-between; - align-items: center; - - .actions { + background-color: rgba(226, 228, 233, 0.9); + height: 50px; + padding: 0 16px; display: flex; + justify-content: space-between; align-items: center; - gap: 25px; - } + + .actions { + display: flex; + align-items: center; + gap: 25px; + } } .grid-container { - display: grid; - position: relative; + display: grid; + position: relative; - .no-data { - grid-column: 1/-1; - } - - .table-col-name { - font-weight: 600; - display: flex; - gap: 8px; - padding: 8px 16px; - border-bottom: 1px solid rgba(226, 228, 233, 0.9); - align-items: center; - text-transform: uppercase; - - .sort-arrows-container { - mat-icon { - display: block; - width: 6px; - height: 11px; - } - } - } - - .table-item { - display: contents; - - > div { - display: flex; - flex-direction: column; - justify-content: center; - height: 100%; - width: 100%; - position: relative; - box-sizing: border-box; + .no-data { + grid-column: 1/-1; } - .table-item-title { - font-weight: 600; - @include line-clamp(1); - } - - > div { - height: 80px; - border-bottom: 1px solid rgba(226, 228, 233, 0.9); - padding: 0 16px; - } - - - .action-buttons { - position: absolute; - display: none; - right: 0; - top: 0; - height: 100%; - width: fit-content; - flex-direction: row; - align-items: center; - padding-left: 100px; - padding-right: 8px; - background: linear-gradient(to right, rgba(244, 245, 247, 0) 0%, #F4F5F7 35%); - - mat-icon { - width: 14px; - } - } - - &:hover { - > div { - background-color: #F9FAFB; - } - - .action-buttons { + .table-col-name { + font-weight: 600; display: flex; - } + gap: 8px; + padding: 8px 16px; + border-bottom: 1px solid rgba(226, 228, 233, 0.9); + align-items: center; + text-transform: uppercase; + + .sort-arrows-container { + mat-icon { + display: block; + width: 6px; + height: 11px; + } + } + } + + .table-item { + display: contents; + + > div { + display: flex; + flex-direction: column; + justify-content: center; + height: 100%; + width: 100%; + position: relative; + box-sizing: border-box; + } + + .table-item-title { + font-weight: 600; + @include line-clamp(1); + } + + > div { + height: 80px; + border-bottom: 1px solid rgba(226, 228, 233, 0.9); + padding: 0 16px; + } + + .action-buttons { + position: absolute; + display: none; + right: 0; + top: 0; + height: 100%; + width: fit-content; + flex-direction: row; + align-items: center; + padding-left: 100px; + padding-right: 8px; + background: linear-gradient(to right, rgba(244, 245, 247, 0) 0%, #f4f5f7 35%); + + mat-icon { + width: 14px; + } + } + + &:hover { + > div { + background-color: #f9fafb; + } + + .action-buttons { + display: flex; + } + } } - } }