Pull request #17: Ui updates

Merge in RED/ui from ui-updates to master

* commit '13802f4082163edec4de000980cab1a1758492a3':
  Select files in project overview
  Added legend
  Fixed assign members
  Moved all dialogs into dialog service
  Removed all dialogs from project listing
  Moved some dialogs into a service
  Moved dialogs in separate directory
  Display filename in top bar
  Center logo in menu bar
  Show annotation comment - not very nice
This commit is contained in:
Timo Bejan 2020-10-22 09:23:39 +02:00
commit b77b58c74a
41 changed files with 686 additions and 416 deletions

View File

@ -20,7 +20,7 @@ import { languageInitializer } from './i18n/language.initializer';
import { LanguageService } from './i18n/language.service';
import { MatIconModule } from '@angular/material/icon';
import { IconsModule } from './icons/icons.module';
import { AddEditProjectDialogComponent } from './screens/project-listing-screen/add-edit-project-dialog/add-edit-project-dialog.component';
import { AddEditProjectDialogComponent } from './dialogs/add-edit-project-dialog/add-edit-project-dialog.component';
import { MatDialogModule } from '@angular/material/dialog';
import { MatSnackBarModule } from '@angular/material/snack-bar';
import { MatTooltipModule } from '@angular/material/tooltip';
@ -33,11 +33,11 @@ import { NgpSortModule } from 'ngp-sort-pipe';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatSelectModule } from '@angular/material/select';
import { MatSidenavModule } from '@angular/material/sidenav';
import { FileDetailsDialogComponent } from './screens/file/file-preview-screen/file-details-dialog/file-details-dialog.component';
import { FileDetailsDialogComponent } from './dialogs/file-details-dialog/file-details-dialog.component';
import { ToastrModule } from 'ngx-toastr';
import { ServiceWorkerModule } from '@angular/service-worker';
import { environment } from '../environments/environment';
import { ProjectDetailsDialogComponent } from './screens/project-overview-screen/project-details-dialog/project-details-dialog.component';
import { ProjectDetailsDialogComponent } from './dialogs/project-details-dialog/project-details-dialog.component';
import { AuthModule } from './auth/auth.module';
import { FileUploadModule } from './upload/file-upload.module';
import { FullPageLoadingIndicatorComponent } from './utils/full-page-loading-indicator/full-page-loading-indicator.component';
@ -48,15 +48,16 @@ import { LogoComponent } from './logo/logo.component';
import { CompositeRouteGuard } from './utils/composite-route.guard';
import { AppStateGuard } from './state/app-state.guard';
import { SimpleDoughnutChartComponent } from './components/simple-doughnut-chart/simple-doughnut-chart.component';
import { ManualRedactionDialogComponent } from './screens/file/manual-redaction-dialog/manual-redaction-dialog.component';
import { ManualRedactionDialogComponent } from './dialogs/manual-redaction-dialog/manual-redaction-dialog.component';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
import { AnnotationIconComponent } from './components/annotation-icon/annotation-icon.component';
import { AuthGuard } from './auth/auth.guard';
import { AuthErrorComponent } from './screens/auth-error/auth-error.component';
import { RedRoleGuard } from './auth/red-role.guard';
import { AssignProjectMembersDialogComponent } from './dialogs/assign-project-members-dialog/assign-project-members-dialog.component';
import { MatListModule } from '@angular/material/list';
import { AssignOwnerDialogComponent } from './components/project-members-dialog/assign-owner-dialog.component';
import { AssignOwnerDialogComponent } from './dialogs/assign-owner-dialog/assign-owner-dialog.component';
export function HttpLoaderFactory(httpClient: HttpClient) {
return new TranslateHttpLoader(httpClient, '/assets/i18n/', '.json');
@ -74,6 +75,7 @@ export function HttpLoaderFactory(httpClient: HttpClient) {
PdfViewerComponent,
FileDetailsDialogComponent,
ProjectDetailsDialogComponent,
AssignProjectMembersDialogComponent,
AssignOwnerDialogComponent,
FullPageLoadingIndicatorComponent,
InitialsAvatarComponent,

View File

@ -1,5 +0,0 @@
@import "../../../assets/styles/red-variables";
.owner {
background-color: rgba($primary, 0.1);
}

View File

@ -1,91 +0,0 @@
import {Component, Inject} from '@angular/core';
import {FileStatus, Project, ProjectControllerService, StatusControllerService} from '@redaction/red-ui-http';
import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog';
import {AppStateService} from '../../state/app-state.service';
import {UserService} from '../../user/user.service';
import {NotificationService} from '../../notification/notification.service';
import {FormBuilder, FormGroup, Validators} from "@angular/forms";
class DialogData {
type: 'file' | 'project';
project?: Project;
file?: FileStatus;
}
@Component({
selector: 'redaction-project-details-dialog',
templateUrl: './assign-owner-dialog.component.html',
styleUrls: ['./assign-owner-dialog.component.scss']
})
export class AssignOwnerDialogComponent {
usersForm: FormGroup;
constructor(public readonly userService: UserService,
private readonly _projectControllerService: ProjectControllerService,
private readonly _notificationService: NotificationService,
private readonly _formBuilder: FormBuilder,
private readonly _statusControllerService: StatusControllerService,
private readonly _appStateService: AppStateService,
public dialogRef: MatDialogRef<AssignOwnerDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: DialogData) {
this._loadData();
}
private _loadData() {
if (this.data.type === 'project') {
const project = this.data.project;
this.usersForm = this._formBuilder.group({
singleUser: [project?.ownerId, Validators.required],
userList: [project?.memberIds]
});
}
if (this.data.type === 'file') {
const file = this.data.file;
this.usersForm = this._formBuilder.group({
singleUser: [file?.currentReviewer],
});
}
}
async saveUsers() {
if (this.data.type === 'project') {
const ownerId = this.usersForm.get('singleUser').value;
const memberIds = this.usersForm.get('userList').value;
const project = this.data.project;
await this._projectControllerService.addMembersToProject({memberIds: memberIds}, project.projectId).toPromise();
await this._projectControllerService.assignProjectOwner(project.projectId, ownerId).toPromise();
const updatedProject = await this._projectControllerService.getProject(project.projectId).toPromise();
const toRemoveMembers = updatedProject.memberIds.filter(m => memberIds.indexOf(m) < 0);
if (toRemoveMembers.length > 0) {
await this._projectControllerService.deleteMembersToProject({memberIds: toRemoveMembers}, project.projectId).toPromise();
}
project.ownerId = ownerId;
project.memberIds = [...new Set([ownerId, ...memberIds])];
this._notificationService.showToastNotification('Successfully assigned ' + this.userService.getNameForId(ownerId) + ' to project: ' + project.projectName);
//this._notificationService.showToastNotification('Failed: ' + error.error.message, null, NotificationType.ERROR);
}
if (this.data.type === 'file') {
const reviewerId = this.usersForm.get('singleUser').value;
await this._statusControllerService.assignProjectOwner1(this._appStateService.activeProjectId, this.data.file.fileId, reviewerId).toPromise();
this.data.file.currentReviewer = reviewerId;
this._notificationService.showToastNotification('Successfully assigned ' + this.userService.getNameForId(reviewerId) + ' to file: ' + this.data.file.filename);
}
this.dialogRef.close();
}
}

View File

@ -2,7 +2,7 @@ import {Component, Inject, OnInit} from '@angular/core';
import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog";
import {Project} from "@redaction/red-ui-http";
import {FormBuilder, FormGroup, Validators} from "@angular/forms";
import {AppStateService} from "../../../state/app-state.service";
import {AppStateService} from "../../state/app-state.service";
@Component({
selector: 'redaction-add-edit-project-dialog',
@ -32,7 +32,7 @@ export class AddEditProjectDialogComponent implements OnInit {
const project: Project = this._formToObject();
project.projectId = this.project?.projectId;
await this._appStateService.addOrUpdateProject(project);
this.dialogRef.close();
this.dialogRef.close(true);
}
private _formToObject(): Project {

View File

@ -0,0 +1,93 @@
import { Component, Inject } from '@angular/core';
import { FileStatus, Project, ProjectControllerService, StatusControllerService } from '@redaction/red-ui-http';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { AppStateService } from '../../state/app-state.service';
import { UserService } from '../../user/user.service';
import { NotificationService, NotificationType } from '../../notification/notification.service';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
class DialogData {
type: 'file' | 'project';
project?: Project;
file?: FileStatus;
}
@Component({
selector: 'redaction-project-details-dialog',
templateUrl: './assign-owner-dialog.component.html',
styleUrls: ['./assign-owner-dialog.component.scss']
})
export class AssignOwnerDialogComponent {
usersForm: FormGroup;
constructor(public readonly userService: UserService,
private readonly _projectControllerService: ProjectControllerService,
private readonly _notificationService: NotificationService,
private readonly _formBuilder: FormBuilder,
private readonly _statusControllerService: StatusControllerService,
private readonly _appStateService: AppStateService,
public dialogRef: MatDialogRef<AssignOwnerDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: DialogData) {
this._loadData();
}
private _loadData() {
if (this.data.type === 'project') {
const project = this.data.project;
this.usersForm = this._formBuilder.group({
singleUser: [project?.ownerId, Validators.required],
userList: [project?.memberIds]
});
}
if (this.data.type === 'file') {
const file = this.data.file;
this.usersForm = this._formBuilder.group({
singleUser: [file?.currentReviewer]
});
}
}
async saveUsers() {
try {
if (this.data.type === 'project') {
const ownerId = this.usersForm.get('singleUser').value;
const memberIds = this.usersForm.get('userList').value;
const project = this.data.project;
await this._projectControllerService.addMembersToProject({ memberIds: memberIds }, project.projectId).toPromise();
await this._projectControllerService.assignProjectOwner(project.projectId, ownerId).toPromise();
const updatedProject = await this._projectControllerService.getProject(project.projectId).toPromise();
const toRemoveMembers = updatedProject.memberIds.filter(m => memberIds.indexOf(m) < 0);
if (toRemoveMembers.length > 0) {
await this._projectControllerService.deleteMembersToProject({ memberIds: toRemoveMembers }, project.projectId).toPromise();
}
project.ownerId = ownerId;
project.memberIds = [...new Set([ownerId, ...memberIds])];
this._notificationService.showToastNotification('Successfully assigned ' + this.userService.getNameForId(ownerId) + ' to project: ' + project.projectName);
}
if (this.data.type === 'file') {
const reviewerId = this.usersForm.get('singleUser').value;
await this._statusControllerService.assignProjectOwner1(this._appStateService.activeProjectId, this.data.file.fileId, reviewerId).toPromise();
this.data.file.currentReviewer = reviewerId;
this._notificationService.showToastNotification('Successfully assigned ' + this.userService.getNameForId(reviewerId) + ' to file: ' + this.data.file.filename);
}
} catch (error) {
this._notificationService.showToastNotification('Failed: ' + error.error.message, null, NotificationType.ERROR);
}
this.dialogRef.close();
}
}

View File

@ -0,0 +1,22 @@
<section class="dialog">
<div [translate]="'project-members.dialog.title.label'"
class="dialog-header heading-l">
</div>
<div class="dialog-content">
<mat-selection-list class="list-50vh" color="primary"
[(ngModel)]="memberIds"
(selectionChange)="selectionChange($event)">
<ng-container *ngFor="let user of userService.allUsers">
<mat-list-option *ngIf="userService.isManager(user) || userService.isUser(user)" [value]="user.userId" checkboxPosition="before">
{{ userService.getNameForId(user.userId) }}
</mat-list-option>
</ng-container>
</mat-selection-list>
</div>
<button (click)="dialogRef.close()" class="dialog-close" mat-icon-button>
<mat-icon svgIcon="red:close"></mat-icon>
</button>
</section>

View File

@ -0,0 +1,55 @@
import { Component, Inject } from '@angular/core';
import { Project, ProjectControllerService } from '@redaction/red-ui-http';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { AppStateService } from '../../state/app-state.service';
import { UserService } from '../../user/user.service';
import { MatSelectionListChange } from '@angular/material/list';
import { NotificationService, NotificationType } from '../../notification/notification.service';
@Component({
selector: 'redaction-project-details-dialog',
templateUrl: './assign-project-members-dialog.component.html',
styleUrls: ['./assign-project-members-dialog.component.scss']
})
export class AssignProjectMembersDialogComponent {
public memberIds: string[];
constructor(private readonly _projectControllerService: ProjectControllerService,
private readonly _notificationService: NotificationService,
public readonly userService: UserService,
private readonly _appStateService: AppStateService,
public dialogRef: MatDialogRef<AssignProjectMembersDialogComponent>,
@Inject(MAT_DIALOG_DATA) public _project: Project) {
this._loadMembers();
}
private _loadMembers() {
this.memberIds = [...this._project.memberIds];
}
private _reloadProject() {
this._appStateService.addOrUpdateProject(this._project).then(() => {
this._loadMembers();
});
}
public selectionChange($event: MatSelectionListChange) {
const userId = $event.option.value;
const selected = $event.option.selected;
const userName = this.userService.getNameForId(userId);
if (selected) {
this._projectControllerService.addMembersToProject({ memberIds: [userId] }, this._project.projectId).subscribe(() => {
this._notificationService.showToastNotification('Successfully assigned ' + userName + ' to project: ' + this._project.projectName);
}, error => {
this._notificationService.showToastNotification('Failed: ' + error.error.message, null, NotificationType.ERROR);
}).add(() => this._reloadProject());
} else {
this._projectControllerService.deleteMembersToProject({ memberIds: [userId] }, this._project.projectId).subscribe(() => {
this._notificationService.showToastNotification('Successfully removed ' + userName + ' from project: ' + this._project.projectName);
}, error => {
this._notificationService.showToastNotification('Failed: ' + error.error.message, null, NotificationType.ERROR);
}).add(() => this._reloadProject());
}
}
}

View File

@ -0,0 +1,178 @@
import { Injectable } from '@angular/core';
import { FileDetailsDialogComponent } from './file-details-dialog/file-details-dialog.component';
import { MatDialog } from '@angular/material/dialog';
import {
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';
import { TranslateService } from '@ngx-translate/core';
import { AppStateService, ProjectWrapper } from '../state/app-state.service';
import { AddEditProjectDialogComponent } from './add-edit-project-dialog/add-edit-project-dialog.component';
import { AssignOwnerDialogComponent } from './assign-owner-dialog/assign-owner-dialog.component';
import { ProjectDetailsDialogComponent } from './project-details-dialog/project-details-dialog.component';
import { AssignProjectMembersDialogComponent } from './assign-project-members-dialog/assign-project-members-dialog.component';
import { ManualRedactionDialogComponent } from './manual-redaction-dialog/manual-redaction-dialog.component';
import { Annotations } from '@pdftron/webviewer';
const dialogConfig = {
width: '600px',
maxWidth: '90vw',
autoFocus: false
};
@Injectable({
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) {
}
public openFileDetailsDialog($event: MouseEvent, file: FileStatus) {
$event.stopPropagation();
this._dialog.open(FileDetailsDialogComponent, {
...dialogConfig,
data: file
});
}
public openDeleteFileDialog($event: MouseEvent, projectId: string, fileId: string, cb?: Function) {
$event.stopPropagation();
this._dialog.open(ConfirmationDialogComponent, dialogConfig).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 openManualRedactionDialog($event: ManualRedactionEntry) {
this._dialog.open(ManualRedactionDialogComponent, {
...dialogConfig,
autoFocus: true,
data: $event
});
}
public acceptSuggestionAnnotation($event: MouseEvent, annotation: Annotations.Annotation, projectId: string, fileId: string) {
$event.stopPropagation();
const parts = annotation.Id.split(':');
const annotationId = parts[parts.length - 1];
this._dialog.open(ConfirmationDialogComponent, dialogConfig)
.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);
});
}
});
}
public suggestRemoveAnnotation($event: MouseEvent, annotation: Annotations.Annotation, projectId: string, fileId: string) {
$event.stopPropagation();
const parts = annotation.Id.split(':');
const annotationId = parts[parts.length - 1];
this._dialog.open(ConfirmationDialogComponent, {
width: '400px',
maxWidth: '90vw'
}).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 openEditProjectDialog($event: MouseEvent, project: Project) {
$event.stopPropagation();
this._dialog.open(AddEditProjectDialogComponent, {
...dialogConfig,
autoFocus: true,
data: project
});
}
public openDeleteProjectDialog($event: MouseEvent, project: Project, cb?: Function) {
$event.stopPropagation();
this._dialog.open(ConfirmationDialogComponent, dialogConfig)
.afterClosed().subscribe(async result => {
if (result) {
await this._appStateService.deleteProject(project);
if (cb) cb();
}
});
}
public openAssignProjectOwnerDialog($event: MouseEvent, project: Project) {
$event.stopPropagation();
this._dialog.open(AssignOwnerDialogComponent, {
...dialogConfig,
data: { type: 'project', project: project }
});
}
public openAssignFileOwnerDialog($event: MouseEvent, file: FileStatus, cb?: Function) {
$event.stopPropagation();
this._dialog.open(AssignOwnerDialogComponent, {
...dialogConfig,
data: { type: 'file', file: file }
}).afterClosed().subscribe(() => {
if (cb) cb();
});
}
public openProjectDetailsDialog($event: MouseEvent, project: ProjectWrapper) {
$event.stopPropagation();
this._dialog.open(ProjectDetailsDialogComponent, {
...dialogConfig,
data: project
});
}
public openAddProjectDialog(cb?: Function): void {
this._dialog.open(AddEditProjectDialogComponent, {
...dialogConfig,
autoFocus: true
}).afterClosed().subscribe(result => {
if (result && cb) cb();
});
}
public openAssignProjectMembersDialog(project: Project, cb?: Function) {
this._dialog.open(AssignProjectMembersDialogComponent, {
...dialogConfig,
data: project
}).afterClosed().subscribe(result => {
if (result && cb) cb();
});
}
}

View File

@ -0,0 +1,2 @@
@import "../../../assets/styles/red-variables";

View File

@ -1,7 +1,7 @@
import {Component, Inject, OnInit} from '@angular/core';
import {FileStatus, FileUploadControllerService} from "@redaction/red-ui-http";
import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog";
import {download} from "../../../../utils/file-download-utils";
import {download} from "../../utils/file-download-utils";
@Component({
selector: 'redaction-file-details-dialog',

View File

@ -1,6 +1,6 @@
import {Component, Inject, OnInit} from '@angular/core';
import {FormBuilder, FormGroup, Validators} from "@angular/forms";
import {AppStateService} from "../../../state/app-state.service";
import {AppStateService} from "../../state/app-state.service";
import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog";
import {
AddRedactionRequest,
@ -8,11 +8,11 @@ import {
ManualRedactionControllerService,
TypeValue
} from "@redaction/red-ui-http";
import {NotificationService, NotificationType} from "../../../notification/notification.service";
import {NotificationService, NotificationType} from "../../notification/notification.service";
import {TranslateService} from "@ngx-translate/core";
import {map} from "rxjs/operators";
import {Observable} from "rxjs";
import {UserService} from "../../../user/user.service";
import {UserService} from "../../user/user.service";
@Component({

View File

@ -1,8 +1,8 @@
import {Component, Inject, OnInit} from '@angular/core';
import {FileUploadControllerService, ReanalysisControllerService} from "@redaction/red-ui-http";
import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog";
import {download} from "../../../utils/file-download-utils";
import {ProjectWrapper} from "../../../state/app-state.service";
import {download} from "../../utils/file-download-utils";
import {ProjectWrapper} from "../../state/app-state.service";
@Component({
selector: 'redaction-project-details-dialog',

View File

@ -1,6 +1,6 @@
<div class="red-top-bar">
<div class="top-bar-row">
<div class="menu left visible-lt-lg">
<div class="menu visible-lt-lg">
<button [matMenuTriggerFor]="menuNav" mat-flat-button>
<mat-icon svgIcon="red:menu"></mat-icon>
</button>
@ -16,29 +16,29 @@
mat-menu-item>{{appStateService.activeFile.filename}}</button>
</mat-menu>
</div>
<div class="menu left visible-lg">
<div class="menu flex-2 visible-lg">
<a class="breadcrumb" routerLink="/ui/projects"
translate="top-bar.navigation-items.projects.label"></a>
<div *ngIf="appStateService.activeProject" class="breadcrumb">
<mat-icon svgIcon="red:double-chevron-right"></mat-icon>
<mat-icon>arrow_right</mat-icon>
</div>
<a *ngIf="appStateService.activeProject" class="breadcrumb"
[routerLink]="'/ui/projects/'+appStateService.activeProjectId">
{{appStateService.activeProject.project.projectName}}
</a>
<div *ngIf="appStateService.activeFile" class="breadcrumb">
<mat-icon svgIcon="red:double-chevron-right"></mat-icon>
<mat-icon>arrow_right</mat-icon>
</div>
<a *ngIf="appStateService.activeFile" class="breadcrumb"
[routerLink]="'/ui/projects/'+appStateService.activeProjectId+'/file/'+appStateService.activeFile.fileId">
{{appStateService.activeFile.filename}}
</a>
</div>
<div class="center">
<div class="center flex-1">
<redaction-logo></redaction-logo>
<div class="app-name" translate="app-name.label"></div>
</div>
<div class="menu right">
<div class="menu right flex-2">
<button [matMenuTriggerFor]="menu" mat-button>
<redaction-initials-avatar color="red-white" size="small" [username]="user?.name" [withName]="true"></redaction-initials-avatar>
<mat-icon>arrow_drop_down</mat-icon>

View File

@ -1,2 +0,0 @@
@import "../../../../../assets/styles/red-variables";

View File

@ -1,9 +1,36 @@
<section [class.hidden]="!viewReady">
<div class="page-header">
<mat-slide-toggle color="primary"
[(ngModel)]="redactedView"
translate="file-preview.view-toggle.label"></mat-slide-toggle>
<div>
<div class="flex-1">
<mat-slide-toggle color="primary"
labelPosition="after"
[(ngModel)]="redactedView">
{{ "file-preview.view-toggle.label" | translate }}
</mat-slide-toggle>
</div>
<div class="flex-1 filename page-title">
{{ appStateService.activeFile.filename }}
</div>
<div class="flex-1 actions-container">
<div class="actions-row">
<button mat-icon-button (click)="openDeleteFileDialog($event)">
<mat-icon svgIcon="red:trash"></mat-icon>
</button>
<button mat-icon-button>
<mat-icon svgIcon="red:report"></mat-icon>
</button>
<button mat-icon-button (click)="openAssignFileOwnerDialog($event)">
<mat-icon svgIcon="red:assign"></mat-icon>
</button>
<button mat-icon-button (click)="reanalyseFile($event)">
<mat-icon svgIcon="red:analyse"></mat-icon>
</button>
<button mat-icon-button (click)="openFileDetailsDialog($event)">
<mat-icon svgIcon="red:info"></mat-icon>
</button>
</div>
<button color="primary" mat-flat-button
[matMenuTriggerFor]="downloadMenu">
<span translate="file-preview.download.label"></span>
@ -32,12 +59,12 @@
[fileStatus]="appStateService.activeFile"
(fileReady)="fileReady('ANNOTATED')"
(pageChanged)="viewerPageChanged($event)"
(manualAnnotationRequested)="handleManualAnnotationRequest($event)"
(manualAnnotationRequested)="openManualRedactionDialog($event)"
(annotationSelected)="handleAnnotationSelected($event)"
(annotationsAdded)="handleAnnotationsAdded($event)"></redaction-pdf-viewer>
<redaction-pdf-viewer [class.visible]="activeViewer === 'REDACTED'" [fileId]="fileId" fileType="REDACTED"
(pageChanged)="viewerPageChanged($event)"
(manualAnnotationRequested)="handleManualAnnotationRequest($event)"
(manualAnnotationRequested)="openManualRedactionDialog($event)"
(fileReady)="fileReady('REDACTED')"></redaction-pdf-viewer>
</div>
@ -124,6 +151,9 @@
<div><strong><span translate="dictionary"></span>: </strong>{{getDictionary(annotation)}}</div>
<div *ngIf="annotation.getContents()"><strong><span translate="content"></span>:
</strong>{{annotation.getContents()}}</div>
<div *ngIf="annotation.Mi[0]?.eC">
<strong><span translate="comment"></span>:</strong> {{ annotation.Mi[0].eC }}
</div>
</div>
<div class="page-number">
@ -144,9 +174,6 @@
</div>
</div>
</div>
<button (click)="showDetailsDialog($event)" aria-label="details" class="details-button" color="primary" mat-fab>
<mat-icon svgIcon="red:info"></mat-icon>
</button>
</section>
<redaction-full-page-loading-indicator [displayed]="!viewReady"></redaction-full-page-loading-indicator>

View File

@ -1,4 +1,5 @@
@import "../../../../assets/styles/red-variables.scss";
@import "../../../../assets/styles/red-variables";
@import "../../../../assets/styles/red-mixins";
redaction-pdf-viewer {
position: absolute;
@ -11,6 +12,18 @@ redaction-pdf-viewer {
}
}
.actions-container {
display: flex;
justify-content: flex-end;
align-items: center;
gap: 10px;
}
.filename {
display: flex;
justify-content: center;
}
.right-fixed-container {
padding: 0;
width: $right-container-width;
@ -92,6 +105,7 @@ redaction-pdf-viewer {
position: relative;
display: flex;
gap: 10px;
border-left: 2px solid transparent;
redaction-annotation-icon {
margin-top: 6px;

View File

@ -1,23 +1,11 @@
import { ChangeDetectorRef, Component, ElementRef, NgZone, OnInit, ViewChild } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import {
AddRedactionRequest,
FileUploadControllerService,
ManualRedactionControllerService,
ManualRedactionEntry,
ProjectControllerService,
StatusControllerService
} from '@redaction/red-ui-http';
import { TranslateService } from '@ngx-translate/core';
import {NotificationService, NotificationType} from '../../../notification/notification.service';
import { MatDialog } from '@angular/material/dialog';
import { ManualRedactionEntry, ReanalysisControllerService } from '@redaction/red-ui-http';
import { AppStateService } from '../../../state/app-state.service';
import { FileDetailsDialogComponent } from './file-details-dialog/file-details-dialog.component';
import { ViewerSyncService } from '../service/viewer-sync.service';
import { Annotations } from '@pdftron/webviewer';
import { PdfViewerComponent } from '../pdf-viewer/pdf-viewer.component';
import { AnnotationUtils } from '../../../utils/annotation-utils';
import { ManualRedactionDialogComponent } from '../manual-redaction-dialog/manual-redaction-dialog.component';
import { UserService } from '../../../user/user.service';
import { debounce } from '../../../utils/debounce';
import scrollIntoView from 'scroll-into-view-if-needed';
@ -26,7 +14,7 @@ import { FiltersService } from '../service/filters.service';
import { FileDownloadService } from '../service/file-download.service';
import { saveAs } from 'file-saver';
import { FileType } from '../model/file-type';
import { ConfirmationDialogComponent } from '../../../common/confirmation-dialog/confirmation-dialog.component';
import { DialogService } from '../../../dialogs/dialog.service';
@Component({
selector: 'redaction-file-preview-screen',
@ -54,17 +42,12 @@ export class FilePreviewScreenComponent implements OnInit {
public readonly appStateService: AppStateService,
private readonly _changeDetectorRef: ChangeDetectorRef,
private readonly _activatedRoute: ActivatedRoute,
private readonly _statusControllerService: StatusControllerService,
private readonly _translateService: TranslateService,
private readonly _notificationService: NotificationService,
private readonly _viewerSyncService: ViewerSyncService,
private readonly _dialog: MatDialog,
private readonly _dialogService: DialogService,
private readonly _router: Router,
private readonly _manualRedactionControllerService: ManualRedactionControllerService,
private readonly _userService: UserService,
private readonly _fileDownloadService: FileDownloadService,
private readonly _fileUploadControllerService: FileUploadControllerService,
private readonly _projectControllerService: ProjectControllerService,
private readonly _reanalysisControllerService: ReanalysisControllerService,
private readonly _filtersService: FiltersService,
private ngZone: NgZone) {
this._activatedRoute.params.subscribe(params => {
@ -102,15 +85,28 @@ export class FilePreviewScreenComponent implements OnInit {
this._viewerSyncService.activateViewer('ANNOTATED');
}
public showDetailsDialog($event: MouseEvent) {
public openFileDetailsDialog($event: MouseEvent) {
this._dialogService.openFileDetailsDialog($event, this.appStateService.activeFile);
}
public reanalyseFile($event: MouseEvent) {
$event.stopPropagation();
this._dialog.open(FileDetailsDialogComponent, {
width: '600px',
maxWidth: '90vw',
data: this.appStateService.activeFile
this._reanalysisControllerService.reanalyzeFile(this.appStateService.activeProject.project.projectId, this.fileId).subscribe(async () => {
await this.appStateService.reloadActiveProjectFiles();
});
}
public openDeleteFileDialog($event: MouseEvent) {
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._dialogService.openAssignFileOwnerDialog($event, file);
}
public fileReady(viewer: string) {
this._readyViewers.push(viewer);
this._changeDetectorRef.detectChanges();
@ -165,13 +161,9 @@ export class FilePreviewScreenComponent implements OnInit {
this._viewerComponent.navigateToPage(pageNumber);
}
handleManualAnnotationRequest($event: ManualRedactionEntry) {
public openManualRedactionDialog($event: ManualRedactionEntry) {
this.ngZone.run(() => {
this._dialog.open(ManualRedactionDialogComponent, {
width: '600px',
maxWidth: '90vw',
data: $event
});
this._dialogService.openManualRedactionDialog($event);
});
}
@ -213,59 +205,24 @@ export class FilePreviewScreenComponent implements OnInit {
}
}
getType(annotation: Annotations.Annotation): string {
public getType(annotation: Annotations.Annotation): string {
return AnnotationUtils.getType(annotation);
}
getDictionary(annotation: Annotations.Annotation): string {
public getDictionary(annotation: Annotations.Annotation): string {
return AnnotationUtils.getDictionary(annotation);
}
acceptSuggestionAnnotation($event: MouseEvent, annotation: Annotations.Annotation) {
$event.stopPropagation();
public acceptSuggestionAnnotation($event: MouseEvent, annotation: Annotations.Annotation) {
this.ngZone.run(() => {
const dialogRef = this._dialog.open(ConfirmationDialogComponent, {
width: '400px',
maxWidth: '90vw'
});
const parts = annotation.Id.split(':');
const annotationId = parts[parts.length - 1];
dialogRef.afterClosed().subscribe(result => {
if (result) {
this._manualRedactionControllerService.approveRequest(this.appStateService.activeProjectId, this.appStateService.activeFile.fileId, annotationId).subscribe(ok => {
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);
});
}
});
this._dialogService.acceptSuggestionAnnotation($event, annotation, this.projectId, this.fileId);
});
}
suggestRemoveAnnotation($event: MouseEvent, annotation: Annotations.Annotation) {
$event.stopPropagation();
public suggestRemoveAnnotation($event: MouseEvent, annotation: Annotations.Annotation) {
this.ngZone.run(() => {
const dialogRef = this._dialog.open(ConfirmationDialogComponent, {
width: '400px',
maxWidth: '90vw'
});
const parts = annotation.Id.split(':');
const annotationId = parts[parts.length - 1];
dialogRef.afterClosed().subscribe(result => {
if (result) {
this._manualRedactionControllerService.undo(this.appStateService.activeProjectId, this.appStateService.activeFile.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);
});
}
});
this._dialogService.suggestRemoveAnnotation($event, annotation, this.projectId, this.fileId);
});
}
public downloadFile(type: FileType | string) {
@ -312,8 +269,7 @@ export class FilePreviewScreenComponent implements OnInit {
this.expandedFilters[key] = value;
}
isManuallyAddedAnnotation(annotation: Annotations.Annotation) {
public isManuallyAddedAnnotation(annotation: Annotations.Annotation) {
return annotation.Id.indexOf('request:') >= 0;
}
}

View File

@ -9,15 +9,13 @@ import {
Output,
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 {ViewerSyncService} from '../service/viewer-sync.service';
import {MatDialog} from "@angular/material/dialog";
import {FileDownloadService} from "../service/file-download.service";
import {FileType} from "../model/file-type";
import {AppStateService} from "../../../state/app-state.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 { ViewerSyncService } from '../service/viewer-sync.service';
import { FileDownloadService } from '../service/file-download.service';
import { FileType } from '../model/file-type';
@Component({
@ -36,18 +34,15 @@ export class PdfViewerComponent implements OnInit, AfterViewInit, OnDestroy {
@Output() manualAnnotationRequested = new EventEmitter<ManualRedactionEntry>();
@Output() pageChanged = new EventEmitter<number>();
@ViewChild('viewer', {static: true}) viewer: ElementRef;
@ViewChild('viewer', { static: true }) viewer: ElementRef;
wvInstance: WebViewerInstance;
_fileData: Blob;
constructor(
private readonly _appStateService: AppStateService,
private readonly _viewerSyncService: ViewerSyncService,
private readonly _translateService: TranslateService,
private readonly _fileDownloadService: FileDownloadService,
private readonly _dialog: MatDialog,
private readonly _appConfigService: AppConfigService) {
constructor(private readonly _viewerSyncService: ViewerSyncService,
private readonly _translateService: TranslateService,
private readonly _fileDownloadService: FileDownloadService,
private readonly _appConfigService: AppConfigService) {
}
ngOnInit() {
@ -61,10 +56,10 @@ export class PdfViewerComponent implements OnInit, AfterViewInit, OnDestroy {
ngAfterViewInit(): void {
this._fileDownloadService.loadFile(this.fileType, this.fileId, (data) => {
this._fileData = data
this._fileData = data;
}, () => this._fileData).subscribe(() => {
this._loadViewer(this._fileData);
})
});
}
private _loadViewer(pdfBlob: any) {
@ -98,7 +93,7 @@ export class PdfViewerComponent implements OnInit, AfterViewInit, OnDestroy {
});
instance.docViewer.on('documentLoaded', this.wvDocumentLoadedHandler);
instance.loadDocument(pdfBlob, {filename: this.fileStatus ? this.fileStatus.filename : 'document.pdf'});
instance.loadDocument(pdfBlob, { filename: this.fileStatus ? this.fileStatus.filename : 'document.pdf' });
});
}
@ -128,7 +123,7 @@ export class PdfViewerComponent implements OnInit, AfterViewInit, OnDestroy {
onClick: () => {
const selectedQuads = this.wvInstance.docViewer.getSelectedTextQuads();
const text = this.wvInstance.docViewer.getSelectedText();
const entry: ManualRedactionEntry = {positions: []};
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));
@ -152,7 +147,7 @@ export class PdfViewerComponent implements OnInit, AfterViewInit, OnDestroy {
},
height: height,
width: selectedQuad.x3 - selectedQuad.x4
}
};
}
private _configureHeader() {

View File

@ -69,7 +69,7 @@
</div>
</div>
<div class="flex-1">
<redaction-initials-avatar [username]="_userService.getNameForId(pw.project.ownerId)"
<redaction-initials-avatar [username]="getOwnerName(pw)"
color="lightgray-red"
withName="true"
></redaction-initials-avatar>
@ -85,13 +85,13 @@
[matTooltip]="'project-listing.delete.action.label'|translate">
<mat-icon svgIcon="red:trash"></mat-icon>
</button>
<button mat-icon-button color="accent" (click)="openDetailsDialog($event,pw)"
<button mat-icon-button color="accent" (click)="openProjectDetailsDialog($event,pw)"
[matTooltip]="'project-listing.report.action.label'|translate">
<mat-icon svgIcon="red:report"></mat-icon>
</button>
<!-- <button mat-icon-button (click)="editProject($event,pw.project)">-->
<!-- <mat-icon svgIcon="red:edit"></mat-icon>-->
<!-- </button>-->
<!-- <button mat-icon-button (click)="editProject($event,pw.project)">-->
<!-- <mat-icon svgIcon="red:edit"></mat-icon>-->
<!-- </button>-->
<button color="accent" (click)="openAssignProjectOwnerDialog($event,pw.project)" mat-icon-button
[matTooltip]="'project-listing.assign.action.label'|translate">
<mat-icon svgIcon="red:assign"></mat-icon>

View File

@ -11,18 +11,20 @@
.left-container {
width: calc(100vw - #{$right-container-width} - 130px);
.table-item:hover {
.stats-bar {
display: none;
.table-item {
&:hover {
.stats-bar {
display: none;
}
}
}
.stats-subtitle {
margin-top: 6px;
}
.stats-subtitle {
margin-top: 6px;
}
.stats-bar, .action-buttons {
width: 160px;
.stats-bar, .action-buttons {
width: 160px;
}
}
}

View File

@ -1,17 +1,11 @@
import { Component, OnInit } from '@angular/core';
import { Project, ProjectControllerService } from '@redaction/red-ui-http';
import { MatDialog } from '@angular/material/dialog';
import { AddEditProjectDialogComponent } from './add-edit-project-dialog/add-edit-project-dialog.component';
import { ConfirmationDialogComponent } from '../../common/confirmation-dialog/confirmation-dialog.component';
import { TranslateService } from '@ngx-translate/core';
import { NotificationService } from '../../notification/notification.service';
import { Project } from '@redaction/red-ui-http';
import { AppStateService, ProjectWrapper } from '../../state/app-state.service';
import { UserService } from '../../user/user.service';
import { ProjectDetailsDialogComponent } from '../project-overview-screen/project-details-dialog/project-details-dialog.component';
import { DoughnutChartConfig } from '../../components/simple-doughnut-chart/simple-doughnut-chart.component';
import { SortingOption } from '../../utils/types';
import { groupBy } from '../../utils/functions';
import { AssignOwnerDialogComponent } from '../../components/project-members-dialog/assign-owner-dialog.component';
import { DialogService } from '../../dialogs/dialog.service';
@Component({
selector: 'redaction-project-listing-screen',
@ -30,14 +24,16 @@ export class ProjectListingScreenComponent implements OnInit {
constructor(
public readonly appStateService: AppStateService,
private readonly _userService: UserService,
private readonly _projectControllerService: ProjectControllerService,
private readonly _translateService: TranslateService,
private readonly _notificationService: NotificationService,
private readonly _dialog: MatDialog) {
private readonly _dialogService: DialogService
) {
}
ngOnInit(): void {
this.appStateService.reset();
this._calculateData();
}
private _calculateData() {
this.projectsChartData = [
{ value: this.activeProjects, color: 'ACTIVE', label: 'active' },
{ value: this.inactiveProjects, color: 'DELETED', label: 'archived' }
@ -47,7 +43,6 @@ export class ProjectListingScreenComponent implements OnInit {
for (const key of Object.keys(groups)) {
this.documentsChartData.push({ value: groups[key].length, color: key, label: key });
}
}
public get user() {
@ -70,6 +65,10 @@ export class ProjectListingScreenComponent implements OnInit {
return this.appStateService.allProjects.length - this.activeProjects;
}
public getOwnerName(pw: ProjectWrapper) {
return this._userService.getNameForId(pw.project.ownerId);
}
public documentCount(project: ProjectWrapper) {
return project.files.length;
}
@ -78,43 +77,23 @@ export class ProjectListingScreenComponent implements OnInit {
return 1;
}
public openAddProjectDialog(project?: Project): void {
this._dialog.open(AddEditProjectDialogComponent, {
width: '400px',
maxWidth: '90vw',
data: project
public openAddProjectDialog(): void {
this._dialogService.openAddProjectDialog(() => {
this._calculateData();
});
}
public openDeleteProjectDialog($event: MouseEvent, project: Project) {
$event.stopPropagation();
const dialogRef = this._dialog.open(ConfirmationDialogComponent, {
width: '400px',
maxWidth: '90vw'
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.appStateService.deleteProject(project);
}
this._dialogService.openDeleteProjectDialog($event, project, () => {
this._calculateData();
});
}
public openDetailsDialog($event: MouseEvent, project: ProjectWrapper) {
$event.stopPropagation();
this._dialog.open(ProjectDetailsDialogComponent, {
width: '600px',
maxWidth: '90vw',
data: project
});
public openProjectDetailsDialog($event: MouseEvent, project: ProjectWrapper) {
this._dialogService.openProjectDetailsDialog($event, project);
}
public openAssignProjectOwnerDialog($event: MouseEvent, project: Project) {
$event.stopPropagation();
this._dialog.open(AssignOwnerDialogComponent, {
width: '400px',
maxWidth: '90vw',
data: { type: 'project', projectId: project.projectId }
});
this._dialogService.openAssignProjectOwnerDialog($event, project);
}
}

View File

@ -33,9 +33,12 @@
<div class="flex red-content-inner">
<div class="left-container">
<div class="table-header">
<div class="select-all-container">
<div class="select-oval" [class.active]="areAllFilesSelected()" (click)="toggleSelectAll()"></div>
<span class="all-caps-label">
{{'project-overview.table-header.title.label'| translate:{length: appStateService.activeProject?.files.length || 0} }}
{{'project-overview.table-header.title.label'| translate:{ length: appStateService.activeProject?.files.length || 0 } }}
</span>
</div>
<div class="actions">
<div translate="project-overview.table-header.bulk-select.label"></div>
<mat-form-field appearance="none" class="red-select">
@ -49,6 +52,7 @@
</div>
<div class="table-col-names">
<div class="select-oval placeholder"></div>
<div class="flex-6 small-label min-width" translate="project-overview.table-col-names.name.label"></div>
<div class="flex-4 small-label min-width" translate="project-overview.table-col-names.added-on.label"></div>
<div class="flex-2 small-label min-width" translate="project-overview.table-col-names.needs-work.label"></div>
@ -61,6 +65,11 @@
[class.pointer]="canOpenFile(fileStatus.status)"
*ngFor="let fileStatus of appStateService.activeProject.files | sortBy: sortingOption.order:sortingOption.column; trackBy:fileId"
[routerLink]="canOpenFile(fileStatus.status) ? ['/ui/projects/'+activeProject.projectId+'/file/'+fileStatus.fileId] : []">
<div class="select-oval"
[class.active]="isFileSelected(fileStatus)"
(click)="toggleFileSelected($event, fileStatus)"></div>
<div class="flex-6 table-item-title min-width" [matTooltip]="'['+fileStatus.status+'] '+fileStatus.filename ">
{{ fileStatus.filename }}
</div>
@ -96,7 +105,7 @@
[matTooltip]="'project-overview.report.action.label'|translate">
<mat-icon svgIcon="red:report"></mat-icon>
</button>
<button (click)="assignPeopleToFile($event,fileStatus)" color="accent" mat-icon-button
<button (click)="openAssignFileOwnerDialog($event,fileStatus)" color="accent" mat-icon-button
[matTooltip]="'project-overview.assign.action.label'|translate">
<mat-icon svgIcon="red:assign"></mat-icon>
</button>
@ -168,7 +177,7 @@
</div>
</div>
<div class="mt-20">
<div class="mt-32">
<redaction-simple-doughnut-chart [config]="documentsChartData"
[strokeWidth]="15"
[radius]="70"
@ -176,5 +185,20 @@
direction="row"
></redaction-simple-doughnut-chart>
</div>
<div class="mt-32 legend">
<div>
<redaction-annotation-icon type="hint"></redaction-annotation-icon>
{{ 'project-overview.legend.contains-hints.label' | translate }}
</div>
<div>
<redaction-annotation-icon type="redaction"></redaction-annotation-icon>
{{ 'project-overview.legend.contains-redactions.label' | translate }}
</div>
<div>
<redaction-annotation-icon type="suggestion"></redaction-annotation-icon>
{{ 'project-overview.legend.contains-suggestions.label' | translate }}
</div>
</div>
</div>
</div>

View File

@ -13,6 +13,35 @@
justify-content: flex-end;
}
.select-all-container {
display: flex;
gap: 16px;
align-items: center;
.select-oval {
margin-left: 0;
}
}
.select-oval {
width: 20px;
height: 20px;
border-radius: 50%;
border: 1px solid $grey-5;
background-color: $white;
padding: 0;
margin-left: 16px;
cursor: pointer;
&.active {
background-color: $primary;
}
&.placeholder {
visibility: hidden;
}
}
.table-item {
.needs-work {
display: flex;
@ -41,3 +70,19 @@
gap: 5px;
}
}
.legend {
display: flex;
flex-direction: column;
gap: 8px;
> div {
display: flex;
gap: 8px;
align-items: center;
}
}
.mt-32 {
margin-top: 32px;
}

View File

@ -1,28 +1,17 @@
import {ChangeDetectorRef, Component, OnDestroy, OnInit} from '@angular/core';
import {ActivatedRoute, Router} from '@angular/router';
import {
FileStatus,
FileUploadControllerService,
ProjectControllerService,
ReanalysisControllerService,
StatusControllerService
} from '@redaction/red-ui-http';
import {NotificationService, NotificationType} from '../../notification/notification.service';
import {TranslateService} from '@ngx-translate/core';
import {ConfirmationDialogComponent} from '../../common/confirmation-dialog/confirmation-dialog.component';
import {MatDialog} from '@angular/material/dialog';
import {AppStateService} from '../../state/app-state.service';
import {ProjectDetailsDialogComponent} from './project-details-dialog/project-details-dialog.component';
import {FileDropOverlayService} from '../../upload/file-drop/service/file-drop-overlay.service';
import {FileUploadModel} from '../../upload/model/file-upload.model';
import {FileUploadService} from '../../upload/file-upload.service';
import {UploadStatusOverlayService} from '../../upload/upload-status-dialog/service/upload-status-overlay.service';
import {AddEditProjectDialogComponent} from '../project-listing-screen/add-edit-project-dialog/add-edit-project-dialog.component';
import {UserService} from '../../user/user.service';
import {SortingOption} from '../../utils/types';
import {DoughnutChartConfig} from '../../components/simple-doughnut-chart/simple-doughnut-chart.component';
import {groupBy} from '../../utils/functions';
import {AssignOwnerDialogComponent} from "../../components/project-members-dialog/assign-owner-dialog.component";
import { Component, OnDestroy, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
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';
import { FileUploadModel } from '../../upload/model/file-upload.model';
import { FileUploadService } from '../../upload/file-upload.service';
import { UploadStatusOverlayService } from '../../upload/upload-status-dialog/service/upload-status-overlay.service';
import { UserService } from '../../user/user.service';
import { SortingOption } from '../../utils/types';
import { DoughnutChartConfig } from '../../components/simple-doughnut-chart/simple-doughnut-chart.component';
import { groupBy } from '../../utils/functions';
import { DialogService } from '../../dialogs/dialog.service';
@Component({
@ -32,31 +21,28 @@ import {AssignOwnerDialogComponent} from "../../components/project-members-dialo
})
export class ProjectOverviewScreenComponent implements OnInit, OnDestroy {
private _fileStatusInterval;
private _selectedFileIds: string[] = [];
public sortingOptions: SortingOption[] = [
{label: 'project-overview.sorting.recent.label', order: 'desc', 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'}
{ label: 'project-overview.sorting.recent.label', order: 'desc', 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 _changeDetectorRef: ChangeDetectorRef,
private readonly _activatedRoute: ActivatedRoute,
private readonly _statusControllerService: StatusControllerService,
private readonly _translateService: TranslateService,
private readonly _notificationService: NotificationService,
private readonly _dialog: MatDialog,
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,
private readonly _fileUploadControllerService: FileUploadControllerService,
private readonly _projectControllerService: ProjectControllerService) {
private readonly _fileDropOverlayService: FileDropOverlayService) {
this._activatedRoute.params.subscribe(params => {
this.appStateService.activateProject(params.projectId);
});
@ -91,7 +77,7 @@ export class ProjectOverviewScreenComponent implements OnInit, OnDestroy {
}
public get displayMembers() {
return this.members.slice(0, 6)
return this.members.slice(0, 6);
}
public get overflowCount() {
@ -112,72 +98,66 @@ export class ProjectOverviewScreenComponent implements OnInit, OnDestroy {
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});
this.documentsChartData.push({ value: groups[key].length, color: key, label: key });
}
}
public openDeleteFileDialog($event: MouseEvent, fileStatus: FileStatus) {
public toggleFileSelected($event: MouseEvent, file: FileStatus) {
$event.stopPropagation();
const dialogRef = this._dialog.open(ConfirmationDialogComponent, {
width: '400px',
maxWidth: '90vw',
autoFocus: false
});
const idx = this._selectedFileIds.indexOf(file.fileId);
if (idx === -1) {
this._selectedFileIds.push(file.fileId);
} else {
this._selectedFileIds.splice(idx, 1);
}
}
dialogRef.afterClosed().subscribe(result => {
if (result) {
this._fileUploadControllerService.deleteFile(fileStatus.projectId, fileStatus.fileId).subscribe(() => {
this._getFileStatus();
}, () => {
this._notificationService.showToastNotification(this._translateService.instant('project-overview.delete-file-error.label', fileStatus), null, NotificationType.ERROR);
});
}
public toggleSelectAll() {
if (this.areAllFilesSelected()) {
this._selectedFileIds = [];
} else {
this._selectedFileIds = this.appStateService.activeProject.files.map(file => file.fileId);
}
}
public areAllFilesSelected() {
return 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) {
$event.stopPropagation();
this._dialog.open(ProjectDetailsDialogComponent, {
width: '600px',
maxWidth: '90vw',
data: this.appStateService.activeProject
});
this._dialogService.openProjectDetailsDialog($event, this.appStateService.activeProject);
}
public openEditProjectDialog($event: MouseEvent) {
$event.stopPropagation();
this._dialog.open(AddEditProjectDialogComponent, {
width: '400px',
maxWidth: '90vw',
data: this.appStateService.activeProject.project
});
this._dialogService.openEditProjectDialog($event, this.appStateService.activeProject.project);
}
public openDeleteProjectDialog($event: MouseEvent) {
$event.stopPropagation();
const dialogRef = this._dialog.open(ConfirmationDialogComponent, {
width: '400px',
maxWidth: '90vw'
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.appStateService.deleteProject(this.appStateService.activeProject.project);
this.ngOnDestroy();
this._router.navigate(['/ui/projects']);
}
this._dialogService.openDeleteProjectDialog($event, this.appStateService.activeProject.project, () => {
this._router.navigate(['/ui/projects']);
});
}
public openAssignProjectMembersDialog() {
this._dialog.open(AssignOwnerDialogComponent, {
width: '400px',
maxWidth: '90vw',
autoFocus: false,
data: {type: 'project', project: this.appStateService.activeProject.project }
}).afterClosed().subscribe( ()=>{
this._dialogService.openAssignProjectMembersDialog(this.activeProject, () => {
this._getFileStatus();
})
});
}
public openAssignFileOwnerDialog($event: MouseEvent, file: FileStatus) {
this._dialogService.openAssignFileOwnerDialog($event, file, () => {
this._getFileStatus();
});
}
public reanalyseFile($event: MouseEvent, fileStatus: FileStatus) {
@ -212,17 +192,6 @@ export class ProjectOverviewScreenComponent implements OnInit, OnDestroy {
return fileStatus === 'PROCESSING' || fileStatus === 'REVIEWED' || true;
}
public assignPeopleToFile($event: MouseEvent, fileStatus: FileStatus) {
$event.stopPropagation();
this._dialog.open(AssignOwnerDialogComponent, {
width: '400px',
maxWidth: '90vw',
data: {type: 'file', file: fileStatus}
}).afterClosed().subscribe( ()=>{
this._getFileStatus();
})
}
public getFileOwnerUsername(fileStatus: FileStatus) {
return this._userService.getNameForId(fileStatus.currentReviewer);
}

View File

@ -109,6 +109,10 @@ export class AppStateService {
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) {
@ -124,7 +128,7 @@ export class AppStateService {
async getFiles(projectId: string) {
const files = await this._statusControllerService.getProjectStatus(projectId).toPromise();
const project = this._appState.projects.find(p => p.project.projectId === projectId);
const project = this.getProjectById(projectId);
project.files = files;
this._computeStats();
return files;
@ -152,7 +156,7 @@ export class AppStateService {
}
deleteProject(project: Project) {
this._projectControllerService.deleteProject(project.projectId).subscribe(() => {
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];

View File

@ -372,6 +372,17 @@
},
"no-project": {
"label": "Requested project: {{projectId}} does not exist! <a href='/ui/projects'>Back to Project Listing. <a/>"
},
"legend": {
"contains-hints": {
"label": "Contains hints "
},
"contains-redactions": {
"label": "Contains redactions "
},
"contains-suggestions": {
"label": "Contains suggestions for redaction "
}
}
},
"file-preview": {
@ -457,13 +468,12 @@
"label": "Unassigned"
}
},
"assign-file-owner":{
"assign-file-owner": {
"dialog": {
"single-user": {
"label": "Reviewer"
},
"title":{
"title": {
"label": "Manage File Reviewer"
},
"save": {
@ -471,8 +481,7 @@
}
}
},
"assign-project-owner":{
"assign-project-owner": {
"dialog": {
"single-user": {
"label": "Owner"
@ -480,7 +489,7 @@
"multi-user": {
"label": "Members"
},
"title":{
"title": {
"label": "Manage Project Owner and Members"
},
"save": {
@ -488,7 +497,6 @@
}
}
},
"unassigned": "Unassigned",
"under-review": "Under review",
"under-approval": "Under approval",

View File

@ -1,14 +1,7 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg id="Capa_1" style="enable-background:new 0 0 512 512;" version="1.1" viewBox="0 0 512 512"
x="0px"
xml:space="preserve" xmlns="http://www.w3.org/2000/svg" y="0px">
<g>
<g>
<path d="M437.02,74.98C388.667,26.629,324.38,0,256,0S123.333,26.629,74.98,74.98C26.629,123.333,0,187.62,0,256
s26.629,132.667,74.98,181.02C123.333,485.371,187.62,512,256,512s132.667-26.629,181.02-74.98
C485.371,388.667,512,324.38,512,256S485.371,123.333,437.02,74.98z M256,70c30.327,0,55,24.673,55,55c0,30.327-24.673,55-55,55
c-30.327,0-55-24.673-55-55C201,94.673,225.673,70,256,70z M326,420H186v-30h30V240h-30v-30h110v180h30V420z"/>
</g>
</g>
</svg>
<?xml version="1.0" encoding="UTF-8"?>
<svg width="100px" height="100px" viewBox="0 0 100 100" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>info</title>
<g id="info" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<path d="M50,0 C77.5,0 100,22.5 100,50 C100,77.5 77.5,100 50,100 C22.5,100 0,77.5 0,50 C0,22.5 22.5,0 50,0 Z M50,10 C28,10 10,28 10,50 C10,72 28,90 50,90 C72,90 90,72 90,50 C90,28 72,10 50,10 Z M50,38.25 C57.5,38.25 59.3203,42.6758 58.4805,48.1328 L55,70.7498 L55.0318998,70.7491065 C55.3367202,70.7391667 57.826087,70.5867565 62.5,68.2498 C62.5,68.2498 60,78.2498 50,78.2498 C42.5,78.2498 40.6797,73.824 41.5195,68.367 L45,45.75 L44.9681002,45.7506935 C44.6632798,45.7606333 42.173913,45.9130435 37.5,48.25 C37.5,48.25 40,38.25 50,38.25 Z M52.5,19.75 C56.6445,19.75 60,23.1094 60,27.25 C60,31.3906 56.6445,34.75 52.5,34.75 C48.3555,34.75 45,31.3906 45,27.25 C45,23.1094 48.3555,19.75 52.5,19.75 Z" id="Combined-Shape" fill="#283241" fill-rule="nonzero"></path>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 790 B

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -119,12 +119,6 @@ html, body {
display: block;
}
.details-button {
position: fixed !important;
bottom: 20px;
right: 20px;
}
.detail-row {
opacity: 1;
font-family: Inconsolata, monospace, monospace;
@ -165,6 +159,11 @@ html, body {
.menu {
display: flex;
align-items: center;
overflow: hidden;
&.right {
justify-content: flex-end;
}
}
}
@ -173,17 +172,15 @@ html, body {
color: $accent;
font-weight: 600;
@include line-clamp(1);
max-width: 320px;
mat-icon {
height: 10px;
width: 10px;
padding: 0 8px;
}
min-width: 25px;
&:last-child {
color: $primary;
}
.mat-icon {
vertical-align: middle;
}
}
.divider {

View File

@ -19,11 +19,11 @@
.table-col-names {
display: flex;
text-transform: uppercase;
border-bottom: 1px solid rgba(226, 228, 233, 0.9);
> div {
padding: 8px 16px;
font-weight: 600;
border-bottom: 1px solid rgba(226, 228, 233, 0.9);
}
}

View File

@ -29,6 +29,13 @@ a {
font-weight: 600;
}
.page-title {
font-size: 13px;
font-weight: 600;
line-height: 18px;
text-align: center;
}
.all-caps-label {
text-transform: uppercase;
opacity: 0.7;

View File

@ -1,8 +1,4 @@
mat-slide-toggle {
display: flex !important;
flex-direction: row-reverse;
gap: 8px;
.mat-slide-toggle-bar {
height: 16px !important;
width: 30px !important;