This commit is contained in:
Timo Bejan 2020-10-27 16:16:02 +02:00
parent 69d7f48108
commit 30d40fef03
29 changed files with 3561 additions and 3060 deletions

View File

@ -1,4 +1,6 @@
<div class="flex-row">
<div [className]="colorClass + ' oval ' + size">{{initials}}</div>
<div *ngIf="withName" class="clamp-2">{{username || ('initials-avatar.unassigned.label' | translate)}}</div>
<div [className]="colorClass + ' oval ' + size">{{ initials }}</div>
<div *ngIf="withName" class="clamp-2">
{{ username || ('initials-avatar.unassigned.label' | translate) }}
</div>
</div>

View File

@ -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}`
}
}

View File

@ -1,6 +1,6 @@
<div class="rectangle-container" [ngClass]="{ small: small }">
<div *ngFor="let rect of config" class="section-wrapper" [style]="'flex: ' + rect.length + ';'">
<div [className]="'rectangle ' + rect.color "></div>
<div *ngIf="rect.label" [ngClass]="labelClass">{{ rect.label }}</div>
</div>
<div *ngFor="let rect of config" class="section-wrapper" [style]="'flex: ' + rect.length + ';'">
<div [className]="'rectangle ' + rect.color"></div>
<div *ngIf="rect.label" [ngClass]="labelClass">{{ rect.label }}</div>
</div>
</div>

View File

@ -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);
}
}
}

View File

@ -1,40 +1,51 @@
<section class="dialog">
<form (submit)="saveProject()" [formGroup]="projectForm">
<div
[translate]="
project?.projectId
? 'project-listing.add-edit-dialog.header-edit.label'
: 'project-listing.add-edit-dialog.header-new.label'
"
class="dialog-header heading-l"
></div>
<form (submit)="saveProject()" [formGroup]="projectForm">
<div
[translate]="project?.projectId ? 'project-listing.add-edit-dialog.header-edit.label': 'project-listing.add-edit-dialog.header-new.label'"
class="dialog-header heading-l">
<div class="dialog-content">
<div class="red-input-group">
<label translate="project-listing.add-edit-dialog.form.name.label"></label>
<input formControlName="projectName" name="projectName" type="text" />
</div>
<div class="red-input-group">
<label translate="project-listing.add-edit-dialog.form.description.label"></label>
<textarea
formControlName="description"
name="description"
type="text"
rows="5"
></textarea>
</div>
</div>
<mat-form-field class="mt-20">
<mat-label>{{
'project-listing.add-edit-dialog.form.due-date.label' | translate
}}</mat-label>
<input matInput [matDatepicker]="picker" formControlName="dueDate" />
<mat-datepicker-toggle matSuffix [for]="picker"></mat-datepicker-toggle>
<mat-datepicker #picker></mat-datepicker>
</mat-form-field>
</div>
<div class="dialog-content">
<div class="red-input-group">
<label translate="project-listing.add-edit-dialog.form.name.label"></label>
<input formControlName="projectName" name="projectName" type="text">
</div>
<div class="red-input-group">
<label translate="project-listing.add-edit-dialog.form.description.label"></label>
<textarea formControlName="description" name="description" type="text" rows="5"></textarea>
</div>
<div class="dialog-actions">
<button
[disabled]="projectForm.invalid"
color="primary"
mat-flat-button
translate="project-listing.add-edit-dialog.actions.save.label"
type="submit"
></button>
</div>
</form>
<mat-form-field class="mt-20">
<mat-label>{{'project-listing.add-edit-dialog.form.due-date.label' | translate}}</mat-label>
<input matInput [matDatepicker]="picker" formControlName="dueDate">
<mat-datepicker-toggle matSuffix [for]="picker"></mat-datepicker-toggle>
<mat-datepicker #picker></mat-datepicker>
</mat-form-field>
</div>
<div class="dialog-actions">
<button [disabled]="projectForm.invalid" color="primary" mat-flat-button
translate="project-listing.add-edit-dialog.actions.save.label" type="submit"></button>
</div>
</form>
<button (click)="dialogRef.close()" class="dialog-close" mat-icon-button>
<mat-icon svgIcon="red:close"></mat-icon>
</button>
<button (click)="dialogRef.close()" class="dialog-close" mat-icon-button>
<mat-icon svgIcon="red:close"></mat-icon>
</button>
</section>

View File

@ -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<FileDetailsDialogComponent> {
$event.stopPropagation();
return this._dialog.open(FileDetailsDialogComponent, {
...dialogConfig,
data: file
});
}
public openDeleteFileDialog($event: MouseEvent, projectId: string, fileId: string, cb?: Function): MatDialogRef<ConfirmationDialogComponent> {
$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<FileDetailsDialogComponent> {
$event.stopPropagation();
return this._dialog.open(FileDetailsDialogComponent, {
...dialogConfig,
data: file
});
}
});
}
return ref;
}
public openDeleteFileDialog(
$event: MouseEvent,
projectId: string,
fileId: string,
cb?: Function
): MatDialogRef<ConfirmationDialogComponent> {
$event.stopPropagation();
const ref = this._dialog.open(ConfirmationDialogComponent, dialogConfig);
public openManualRedactionDialog($event: ManualRedactionEntry, cb?: Function): MatDialogRef<ManualRedactionDialogComponent> {
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<ConfirmationDialogComponent> {
$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<ConfirmationDialogComponent> {
$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<ManualRedactionDialogComponent> {
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<AddEditProjectDialogComponent> {
$event.stopPropagation();
return this._dialog.open(AddEditProjectDialogComponent, {
...dialogConfig,
autoFocus: true,
data: project
});
}
return ref;
}
public openDeleteProjectDialog($event: MouseEvent, project: Project, cb?: Function): MatDialogRef<ConfirmationDialogComponent> {
$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<ConfirmationDialogComponent> {
$event.stopPropagation();
public openAssignProjectMembersAndOwnerDialog($event: MouseEvent, project: Project, cb?: Function): MatDialogRef<AssignOwnerDialogComponent> {
$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<AssignOwnerDialogComponent> {
$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<ConfirmationDialogComponent> {
$event.stopPropagation();
public openProjectDetailsDialog($event: MouseEvent, project: ProjectWrapper): MatDialogRef<ProjectDetailsDialogComponent> {
$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<AddEditProjectDialogComponent> {
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<AddEditProjectDialogComponent> {
$event.stopPropagation();
return this._dialog.open(AddEditProjectDialogComponent, {
...dialogConfig,
autoFocus: true,
data: project
});
}
public openDeleteProjectDialog(
$event: MouseEvent,
project: Project,
cb?: Function
): MatDialogRef<ConfirmationDialogComponent> {
$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<AssignOwnerDialogComponent> {
$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<AssignOwnerDialogComponent> {
$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<ProjectDetailsDialogComponent> {
$event.stopPropagation();
return this._dialog.open(ProjectDetailsDialogComponent, {
...dialogConfig,
data: project
});
}
public openAddProjectDialog(cb?: Function): MatDialogRef<AddEditProjectDialogComponent> {
const ref = this._dialog.open(AddEditProjectDialogComponent, {
...dialogConfig,
autoFocus: true
});
ref.afterClosed().subscribe((result) => {
if (result && cb) cb();
});
return ref;
}
}

View File

@ -1,53 +1,53 @@
<section class="dialog">
<form (submit)="handleAddRedaction()" [formGroup]="redactionForm">
<div
class="dialog-header heading-l" translate="manual-redaction.dialog.header.label">
<form (submit)="handleAddRedaction()" [formGroup]="redactionForm">
<div class="dialog-header heading-l" translate="manual-redaction.dialog.header.label"></div>
</div>
<div class="dialog-content">
<div class="red-input-group">
<label translate="manual-redaction.dialog.content.text.label"></label>
</div>
{{ addRedactionRequest.value }}
<div class="dialog-content">
<div class="red-input-group">
<label translate="manual-redaction.dialog.content.reason.label"></label>
<textarea formControlName="reason" name="reason" type="text" rows="2"></textarea>
</div>
<div class="red-input-group">
<label translate="manual-redaction.dialog.content.comment.label"></label>
<textarea formControlName="comment" name="comment" type="text" rows="2"></textarea>
</div>
<div class="red-input-group">
<label translate="manual-redaction.dialog.content.text.label"></label>
</div>
{{ addRedactionRequest.value }}
<div class="red-input-group">
<label translate="manual-redaction.dialog.content.dictionary.label"></label>
<mat-select formControlName="dictionary">
<mat-option
*ngFor="let dictionary of dictionaries | async"
[value]="dictionary.type"
>
{{ dictionary.type }}
</mat-option>
</mat-select>
</div>
<div class="red-input-group">
<label translate="manual-redaction.dialog.content.reason.label"></label>
<textarea formControlName="reason" name="reason" type="text" rows="2"></textarea>
</div>
<div class="red-input-group">
<label translate="manual-redaction.dialog.content.comment.label"></label>
<textarea formControlName="comment" name="comment" type="text" rows="2"></textarea>
</div>
<div class="red-input-group">
<label translate="manual-redaction.dialog.content.dictionary.label"></label>
<mat-select formControlName="dictionary">
<mat-option *ngFor="let dictionary of dictionaries | async" [value]="dictionary.type">
{{dictionary.type}}
</mat-option>
</mat-select>
</div>
<div class="red-input-group">
<mat-checkbox color="primary"
formControlName="addToDictionary">{{'manual-redaction.dialog.content.dictionary.add.label' | translate}}</mat-checkbox>
</div>
</div>
<div class="dialog-actions">
<button color="primary" mat-flat-button [disabled]="!redactionForm.valid"
translate="manual-redaction.dialog.actions.save.label" type="submit"></button>
</div>
</form>
<button (click)="dialogRef.close()" class="dialog-close" mat-icon-button>
<mat-icon svgIcon="red:close"></mat-icon>
</button>
<div class="red-input-group">
<mat-checkbox color="primary" formControlName="addToDictionary">{{
'manual-redaction.dialog.content.dictionary.add.label' | translate
}}</mat-checkbox>
</div>
</div>
<div class="dialog-actions">
<button
color="primary"
mat-flat-button
[disabled]="!redactionForm.valid"
translate="manual-redaction.dialog.actions.save.label"
type="submit"
></button>
</div>
</form>
<button (click)="dialogRef.close()" class="dialog-close" mat-icon-button>
<mat-icon svgIcon="red:close"></mat-icon>
</button>
</section>

View File

@ -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`)
);
}
}
}
}

View File

@ -1,69 +1,110 @@
<div class="red-top-bar">
<div class="top-bar-row">
<div class="menu visible-lt-lg">
<button [matMenuTriggerFor]="menuNav" mat-flat-button>
<mat-icon svgIcon="red:menu"></mat-icon>
</button>
<mat-menu #menuNav="matMenu">
<button mat-menu-item routerLink="/ui/projects"
translate="top-bar.navigation-items.projects.label">
</button>
<button *ngIf="appStateService.activeProject"
[routerLink]="'/ui/projects/'+appStateService.activeProjectId"
mat-menu-item>{{appStateService.activeProject.project.projectName}}</button>
<button *ngIf="appStateService.activeFile"
[routerLink]="'/ui/projects/'+appStateService.activeProjectId+'/file/'+appStateService.activeFile.fileId"
mat-menu-item>{{appStateService.activeFile.filename}}</button>
</mat-menu>
<div class="top-bar-row">
<div class="menu visible-lt-lg">
<button [matMenuTriggerFor]="menuNav" mat-flat-button>
<mat-icon svgIcon="red:menu"></mat-icon>
</button>
<mat-menu #menuNav="matMenu">
<button
mat-menu-item
routerLink="/ui/projects"
translate="top-bar.navigation-items.projects.label"
></button>
<button
*ngIf="appStateService.activeProject"
[routerLink]="'/ui/projects/' + appStateService.activeProjectId"
mat-menu-item
>
{{ appStateService.activeProject.project.projectName }}
</button>
<button
*ngIf="appStateService.activeFile"
[routerLink]="
'/ui/projects/' +
appStateService.activeProjectId +
'/file/' +
appStateService.activeFile.fileId
"
mat-menu-item
>
{{ appStateService.activeFile.filename }}
</button>
</mat-menu>
</div>
<div class="menu flex-2 visible-lg breadcrumbs-container">
<a
class="breadcrumb"
routerLink="/ui/projects"
translate="top-bar.navigation-items.projects.label"
></a>
<div *ngIf="appStateService.activeProject" class="breadcrumb">
<mat-icon svgIcon="red: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: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 flex-1">
<redaction-logo></redaction-logo>
<div class="app-name" translate="app-name.label"></div>
</div>
<div class="menu right flex-2">
<button [matMenuTriggerFor]="menu" mat-button class="arrow-button">
<redaction-initials-avatar
color="red-white"
size="small"
[userId]="user?.id"
[withName]="true"
></redaction-initials-avatar>
<mat-icon svgIcon="red:arrow-down"></mat-icon>
</button>
<mat-menu #menu="matMenu">
<button
[matMenuTriggerFor]="language"
mat-menu-item
translate="top-bar.navigation-items.my-account.children.language.label"
></button>
<mat-menu #language="matMenu">
<button
(click)="changeLanguage('en')"
mat-menu-item
translate="top-bar.navigation-items.my-account.children.language.english.label"
></button>
<button
(click)="changeLanguage('de')"
mat-menu-item
translate="top-bar.navigation-items.my-account.children.language.german.label"
></button>
</mat-menu>
<button (click)="logout()" mat-menu-item>
<mat-icon svgIcon="red:logout"> </mat-icon>
<span translate="top-bar.navigation-items.my-account.children.logout.label">
</span>
</button>
</mat-menu>
</div>
</div>
<div class="menu flex-2 visible-lg breadcrumbs-container">
<a class="breadcrumb" routerLink="/ui/projects"
translate="top-bar.navigation-items.projects.label"></a>
<div *ngIf="appStateService.activeProject" class="breadcrumb">
<mat-icon svgIcon="red: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: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 flex-1">
<redaction-logo></redaction-logo>
<div class="app-name" translate="app-name.label"></div>
</div>
<div class="menu right flex-2">
<button [matMenuTriggerFor]="menu" mat-button class="arrow-button">
<redaction-initials-avatar color="red-white" size="small" [userId]="user?.id" [withName]="true"></redaction-initials-avatar>
<mat-icon svgIcon="red:arrow-down"></mat-icon>
</button>
<mat-menu #menu="matMenu">
<button [matMenuTriggerFor]="language" mat-menu-item
translate="top-bar.navigation-items.my-account.children.language.label"></button>
<mat-menu #language="matMenu">
<button (click)="changeLanguage('en')" mat-menu-item
translate="top-bar.navigation-items.my-account.children.language.english.label"></button>
<button (click)="changeLanguage('de')" mat-menu-item
translate="top-bar.navigation-items.my-account.children.language.german.label"></button>
</mat-menu>
<button (click)="logout()" mat-menu-item>
<mat-icon svgIcon="red:logout">
</mat-icon>
<span translate="top-bar.navigation-items.my-account.children.logout.label">
</span>
</button>
</mat-menu>
</div>
</div>
<div class="divider"></div>
<div class="divider"></div>
</div>
<div class="red-content">
<router-outlet></router-outlet>
<router-outlet></router-outlet>
</div>

View File

@ -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;
}
}
}

View File

@ -1,191 +1,276 @@
<section>
<div class="page-header">
<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 class="arrow-button"
[matMenuTriggerFor]="downloadMenu">
<span translate="file-preview.download.label"></span>
<mat-icon svgIcon="red:arrow-down"></mat-icon>
</button>
<mat-menu #downloadMenu="matMenu" xPosition="before">
<div mat-menu-item
translate="file-preview.download.dropdown.original.label"
(click)="downloadFile('ORIGINAL')"
></div>
<div mat-menu-item
translate="file-preview.download.dropdown.annotated.label"
(click)="downloadFile('ANNOTATED')"
></div>
<div mat-menu-item
translate="file-preview.download.dropdown.redacted.label"
(click)="downloadFile('REDACTED')"
></div>
</mat-menu>
</div>
</div>
<div class="flex red-content-inner">
<div class="left-container">
<redaction-pdf-viewer *ngIf="annotatedFileData && redactedFileData"
[fileData]="redactedView ? redactedFileData : annotatedFileData "
[fileStatus]="appStateService.activeFile"
(keyUp)="handleKeyEvent($event)"
(pageChanged)="viewerPageChanged($event)"
(manualAnnotationRequested)="openManualRedactionDialog($event)"
(annotationSelected)="handleAnnotationSelected($event)"
(annotationsAdded)="handleAnnotationsAdded($event)"
(viewerReady)="viewerReady($event)"></redaction-pdf-viewer>
</div>
<div class="right-fixed-container">
<div class="right-title heading"
translate="file-preview.tabs.annotations.label">
<div>
<button color="accent" mat-button class="arrow-button"
[matMenuTriggerFor]="filterMenu" [ngClass]="{ overlay: hasActiveFilters }">
<span translate="file-preview.filter-menu.label"></span>
<mat-icon svgIcon="red:arrow-down"></mat-icon>
</button>
<div class="dot" *ngIf="hasActiveFilters"></div>
<mat-menu #filterMenu="matMenu" xPosition="before">
<div class="filter-menu-header">
<div class="all-caps-label" translate="file-preview.filter-menu.filter-types.label"></div>
<div class="actions">
<div class="all-caps-label primary pointer" translate="file-preview.filter-menu.all.label"
(click)="setAllFilters(filters, true); applyFilters(); $event.stopPropagation();"></div>
<div class="all-caps-label primary pointer" translate="file-preview.filter-menu.none.label"
(click)="setAllFilters(filters, false); applyFilters(); $event.stopPropagation();"></div>
</div>
</div>
<div *ngFor="let key of filterKeys()">
<div class="mat-menu-item flex" (click)="$event.stopPropagation()">
<div class="arrow-wrapper" *ngIf="hasSubsections(filters[key])">
<mat-icon *ngIf="expandedFilters[key]"
svgIcon="red:arrow-down"
color="accent"
(click)="setExpanded(key, false, $event)">
</mat-icon>
<mat-icon *ngIf="!expandedFilters[key]"
color="accent"
svgIcon="red:arrow-right"
(click)="setExpanded(key, true, $event)">
</mat-icon>
</div>
<mat-checkbox [checked]="isChecked(key)"
[indeterminate]="isIndeterminate(key)"
(change)="setAllFilters(filters[key], $event.checked, hasSubsections(filters[key]) ? null : key)"
color="primary">
<redaction-annotation-icon [type]="key"></redaction-annotation-icon>
{{"file-preview.filter-menu." + key + ".label" | translate }}
</mat-checkbox>
</div>
<div *ngIf="hasSubsections(filters[key]) && expandedFilters[key]">
<div *ngFor="let subkey of filterKeys(key)"
class="padding-left mat-menu-item"
(click)="$event.stopPropagation()"
>
<mat-checkbox [(ngModel)]="filters[key][subkey]" (change)="applyFilters()" color="primary">
<redaction-annotation-icon [type]="key + ' ' + subkey"></redaction-annotation-icon>
{{"file-preview.filter-menu." + key + "." + subkey + ".label" | translate }}
</mat-checkbox>
</div>
</div>
</div>
</mat-menu>
</div>
</div>
<div class="right-content">
<div class="pages" [class.activePanel]="pagesPanelActive"
tabindex="0"
(keyup)="$event.preventDefault()"
(keydown)="$event.preventDefault();"
#quickNavigation>
<div class="page-number pointer"
[ngClass]="{ active: pageNumber === activeViewerPage }"
*ngFor="let pageNumber of displayedPages"
[id]="'quick-nav-page-'+pageNumber" (click)="selectPage(pageNumber)">
{{pageNumber}}
</div>
<div class="page-header">
<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="annotations" [class.activePanel]="!pagesPanelActive" #annotations
tabindex="1"
(keyup)="$event.preventDefault()"
(keydown)="$event.preventDefault();"
>
<div *ngFor="let page of displayedPages">
<div class="page-separator" attr.anotation-page-header="{{page}}">
<span class="all-caps-label"><span translate="page"></span> {{page}}</span>
<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>
<div *ngFor="let annotation of displayedAnnotations[page].annotations"
class="annotation" attr.annotation-id="{{annotation.Id}}"
attr.annotation-page="{{page}}"
[ngClass]="{ active: selectedAnnotation === annotation }"
(click)="selectAnnotation(annotation)"
<button
color="primary"
mat-flat-button
class="arrow-button"
[matMenuTriggerFor]="downloadMenu"
>
<redaction-annotation-icon
[type]="getType(annotation) + ' ' + getDictionary(annotation)"></redaction-annotation-icon>
<div class="flex-1">
<div><strong>{{getType(annotation) | translate}}</strong></div>
<div><strong><span translate="dictionary"></span>: </strong>{{getDictionary(annotation)}}</div>
<div *ngIf="annotation.getContents()"><strong><span translate="content"></span>:
</strong>{{annotation.getContents()}}</div>
<div *ngFor="let comment of annotation['comments']">
<strong><span translate="comment"></span>:</strong> {{ comment.value }}
</div>
</div>
<div class="page-number">
{{annotation.getPageNumber()}}
</div>
<div class="annotation-actions" *ngIf="isManuallyAddedAnnotation(annotation)">
<button mat-icon-button (click)="acceptSuggestionAnnotation($event, annotation)" class="confirm">
<mat-icon svgIcon="red:check-alt"></mat-icon>
</button>
<button mat-icon-button (click)="suggestRemoveAnnotation($event, annotation)">
<mat-icon svgIcon="red:trash"></mat-icon>
</button>
</div>
</div>
</div>
<span translate="file-preview.download.label"></span>
<mat-icon svgIcon="red:arrow-down"></mat-icon>
</button>
<mat-menu #downloadMenu="matMenu" xPosition="before">
<div
mat-menu-item
translate="file-preview.download.dropdown.original.label"
(click)="downloadFile('ORIGINAL')"
></div>
<div
mat-menu-item
translate="file-preview.download.dropdown.annotated.label"
(click)="downloadFile('ANNOTATED')"
></div>
<div
mat-menu-item
translate="file-preview.download.dropdown.redacted.label"
(click)="downloadFile('REDACTED')"
></div>
</mat-menu>
</div>
</div>
<div class="flex red-content-inner">
<div class="left-container">
<redaction-pdf-viewer
*ngIf="annotatedFileData && redactedFileData"
[fileData]="redactedView ? redactedFileData : annotatedFileData"
[fileStatus]="appStateService.activeFile"
(keyUp)="handleKeyEvent($event)"
(pageChanged)="viewerPageChanged($event)"
(manualAnnotationRequested)="openManualRedactionDialog($event)"
(annotationSelected)="handleAnnotationSelected($event)"
(annotationsAdded)="handleAnnotationsAdded($event)"
(viewerReady)="viewerReady($event)"
></redaction-pdf-viewer>
</div>
<div class="right-fixed-container">
<div class="right-title heading" translate="file-preview.tabs.annotations.label">
<div>
<button
color="accent"
mat-button
class="arrow-button"
[matMenuTriggerFor]="filterMenu"
[ngClass]="{ overlay: hasActiveFilters }"
>
<span translate="file-preview.filter-menu.label"></span>
<mat-icon svgIcon="red:arrow-down"></mat-icon>
</button>
<div class="dot" *ngIf="hasActiveFilters"></div>
<mat-menu #filterMenu="matMenu" xPosition="before">
<div class="filter-menu-header">
<div
class="all-caps-label"
translate="file-preview.filter-menu.filter-types.label"
></div>
<div class="actions">
<div
class="all-caps-label primary pointer"
translate="file-preview.filter-menu.all.label"
(click)="
setAllFilters(filters, true);
applyFilters();
$event.stopPropagation()
"
></div>
<div
class="all-caps-label primary pointer"
translate="file-preview.filter-menu.none.label"
(click)="
setAllFilters(filters, false);
applyFilters();
$event.stopPropagation()
"
></div>
</div>
</div>
<div *ngFor="let key of filterKeys()">
<div class="mat-menu-item flex" (click)="$event.stopPropagation()">
<div class="arrow-wrapper" *ngIf="hasSubsections(filters[key])">
<mat-icon
*ngIf="expandedFilters[key]"
svgIcon="red:arrow-down"
color="accent"
(click)="setExpanded(key, false, $event)"
>
</mat-icon>
<mat-icon
*ngIf="!expandedFilters[key]"
color="accent"
svgIcon="red:arrow-right"
(click)="setExpanded(key, true, $event)"
>
</mat-icon>
</div>
<mat-checkbox
[checked]="isChecked(key)"
[indeterminate]="isIndeterminate(key)"
(change)="
setAllFilters(
filters[key],
$event.checked,
hasSubsections(filters[key]) ? null : key
)
"
color="primary"
>
<redaction-annotation-icon
[type]="key"
></redaction-annotation-icon>
{{ 'file-preview.filter-menu.' + key + '.label' | translate }}
</mat-checkbox>
</div>
<div *ngIf="hasSubsections(filters[key]) && expandedFilters[key]">
<div
*ngFor="let subkey of filterKeys(key)"
class="padding-left mat-menu-item"
(click)="$event.stopPropagation()"
>
<mat-checkbox
[(ngModel)]="filters[key][subkey]"
(change)="applyFilters()"
color="primary"
>
<redaction-annotation-icon
[type]="key + ' ' + subkey"
></redaction-annotation-icon>
{{
'file-preview.filter-menu.' +
key +
'.' +
subkey +
'.label' | translate
}}
</mat-checkbox>
</div>
</div>
</div>
</mat-menu>
</div>
</div>
<div class="right-content">
<div
class="pages"
[class.activePanel]="pagesPanelActive"
tabindex="0"
(keyup)="$event.preventDefault()"
(keydown)="$event.preventDefault()"
#quickNavigation
>
<div
class="page-number pointer"
[ngClass]="{ active: pageNumber === activeViewerPage }"
*ngFor="let pageNumber of displayedPages"
[id]="'quick-nav-page-' + pageNumber"
(click)="selectPage(pageNumber)"
>
{{ pageNumber }}
</div>
</div>
<div
class="annotations"
[class.activePanel]="!pagesPanelActive"
#annotations
tabindex="1"
(keyup)="$event.preventDefault()"
(keydown)="$event.preventDefault()"
>
<div *ngFor="let page of displayedPages">
<div class="page-separator" attr.anotation-page-header="{{ page }}">
<span class="all-caps-label"
><span translate="page"></span> {{ page }}</span
>
</div>
<div
*ngFor="let annotation of displayedAnnotations[page].annotations"
class="annotation"
attr.annotation-id="{{ annotation.Id }}"
attr.annotation-page="{{ page }}"
[ngClass]="{ active: selectedAnnotation === annotation }"
(click)="selectAnnotation(annotation)"
>
<redaction-annotation-icon
[type]="getType(annotation) + ' ' + getDictionary(annotation)"
></redaction-annotation-icon>
<div class="flex-1">
<div>
<strong>{{ getType(annotation) | translate }}</strong>
</div>
<div>
<strong><span translate="dictionary"></span>: </strong
>{{ getDictionary(annotation) }}
</div>
<div *ngIf="annotation.getContents()">
<strong><span translate="content"></span>: </strong
>{{ annotation.getContents() }}
</div>
<div *ngFor="let comment of annotation['comments']">
<strong><span translate="comment"></span>:</strong>
{{ comment.value }}
</div>
</div>
<div class="page-number">
{{ annotation.getPageNumber() }}
</div>
<div
class="annotation-actions"
*ngIf="isManuallyAddedAnnotation(annotation)"
>
<button
mat-icon-button
(click)="acceptSuggestionAnnotation($event, annotation)"
class="confirm"
>
<mat-icon svgIcon="red:check-alt"></mat-icon>
</button>
<button
mat-icon-button
(click)="suggestRemoveAnnotation($event, annotation)"
>
<mat-icon svgIcon="red:trash"></mat-icon>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<!--<redaction-full-page-loading-indicator [displayed]="!viewReady"></redaction-full-page-loading-indicator>-->

View File

@ -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;
}
}

View File

@ -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<any>;
private projectId: string;
private _activeViewer: 'ANNOTATED' | 'REDACTED' = 'ANNOTATED';
private instance: WebViewerInstance;
private _dialogRef: MatDialogRef<any>;
@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();
}
}

View File

@ -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<Annotations.Annotation[]>();
@Output() annotationSelected = new EventEmitter<Annotations.Annotation>();
@Output() manualAnnotationRequested = new EventEmitter<ManualRedactionEntry>();
@Output() pageChanged = new EventEmitter<number>();
@Output() keyUp = new EventEmitter<KeyboardEvent>();
@Output() viewerReady = new EventEmitter<WebViewerInstance>();
@Output() fileReady = new EventEmitter();
@Output() annotationsAdded = new EventEmitter<Annotations.Annotation[]>();
@Output() annotationSelected = new EventEmitter<Annotations.Annotation>();
@Output() manualAnnotationRequested = new EventEmitter<ManualRedactionEntry>();
@Output() pageChanged = new EventEmitter<number>();
@Output() keyUp = new EventEmitter<KeyboardEvent>();
@Input() flag = false;
@Output() viewerReady = new EventEmitter<WebViewerInstance>();
@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(<any>{
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(<any>{
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');
}
}
}

View File

@ -1,151 +1,188 @@
<div class="page-header">
<div class="filters flex-row">
<div translate="filters.filter-by.label"></div>
<button mat-button translate="filters.status.label">
<mat-icon svgIcon="red:status"></mat-icon>
<div class="filters flex-row">
<div translate="filters.filter-by.label"></div>
<button mat-button translate="filters.status.label">
<mat-icon svgIcon="red:status"></mat-icon>
</button>
<button mat-button translate="filters.people.label">
<mat-icon svgIcon="red:user"></mat-icon>
</button>
<button mat-button translate="filters.due-date.label">
<mat-icon svgIcon="red:lightning"></mat-icon>
</button>
<button mat-button translate="filters.created-on.label">
<mat-icon svgIcon="red:calendar"></mat-icon>
</button>
<button mat-button translate="filters.project.label">
<mat-icon svgIcon="red:folder"></mat-icon>
</button>
<button mat-button translate="filters.document.label">
<mat-icon svgIcon="red:document"></mat-icon>
</button>
</div>
<button
(click)="openAddProjectDialog()"
color="primary"
mat-flat-button
class="add-project-btn"
>
<mat-icon svgIcon="red:plus"> </mat-icon>
<span translate="project-listing.add-new.label"></span>
</button>
<button mat-button translate="filters.people.label">
<mat-icon svgIcon="red:user"></mat-icon>
</button>
<button mat-button translate="filters.due-date.label">
<mat-icon svgIcon="red:lightning"></mat-icon>
</button>
<button mat-button translate="filters.created-on.label">
<mat-icon svgIcon="red:calendar"></mat-icon>
</button>
<button mat-button translate="filters.project.label">
<mat-icon svgIcon="red:folder"></mat-icon>
</button>
<button mat-button translate="filters.document.label">
<mat-icon svgIcon="red:document"></mat-icon>
</button>
</div>
<button (click)="openAddProjectDialog()" color="primary" mat-flat-button class="add-project-btn">
<mat-icon svgIcon="red:plus">
</mat-icon>
<span translate="project-listing.add-new.label"></span>
</button>
</div>
<div class="flex red-content-inner">
<div class="left-container">
<div class="table-header">
<span class="all-caps-label">
{{'project-listing.table-header.title.label'| translate:{ length: appStateService.allProjects?.length || 0 } }}
</span>
<div class="actions">
<div translate="project-listing.table-header.bulk-select.label"></div>
<mat-form-field appearance="none" class="red-select">
<mat-select [(ngModel)]="sortingOption" panelClass="red-select-panel">
<mat-option *ngFor="let option of sortingOptions" [value]="option">
{{option.label | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="left-container">
<div class="table-header">
<span class="all-caps-label">
{{
'project-listing.table-header.title.label'
| translate: { length: appStateService.allProjects?.length || 0 }
}}
</span>
<div class="actions">
<div translate="project-listing.table-header.bulk-select.label"></div>
<mat-form-field appearance="none" class="red-select">
<mat-select [(ngModel)]="sortingOption" panelClass="red-select-panel">
<mat-option *ngFor="let option of sortingOptions" [value]="option">
{{ option.label | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
<div class="grid-container">
<div class="table-col-name">
<span
class="small-label"
translate="project-listing.table-col-names.name.label"
></span>
</div>
<div class="table-col-name">
<span
class="small-label"
translate="project-listing.table-col-names.owner.label"
></span>
</div>
<div class="table-col-name flex-end">
<span
class="small-label"
translate="project-listing.table-col-names.status.label"
></span>
</div>
<div
class="no-data"
*ngIf="appStateService.allProjects?.length === 0"
translate="project-listing.no-projects.label"
></div>
<div
class="table-item pointer"
[routerLink]="'/ui/projects/' + pw.project.projectId"
*ngFor="
let pw of appStateService.allProjects
| sortBy: sortingOption.order:sortingOption.column
"
>
<div>
<div class="table-item-title table-item-title--large">
{{ pw.project.projectName }}
</div>
<div class="small-label stats-subtitle">
<div>
<mat-icon svgIcon="red:document"></mat-icon>
{{ documentCount(pw) }}
</div>
<div>
<mat-icon svgIcon="red:user"></mat-icon>
{{ userCount(pw) }}
</div>
<div>
<mat-icon svgIcon="red:calendar"></mat-icon>
{{ pw.project.date | date: 'mediumDate' }}
</div>
<div *ngIf="pw.project.dueDate">
<mat-icon svgIcon="red:lightning"></mat-icon>
{{ pw.project.dueDate | date: 'mediumDate' }}
</div>
</div>
</div>
<div>
<redaction-initials-avatar
[userId]="pw.project.ownerId"
withName="true"
></redaction-initials-avatar>
</div>
<div class="status-container">
<redaction-status-bar
[config]="getProjectStatusConfig(pw)"
></redaction-status-bar>
<div class="action-buttons">
<button
mat-icon-button
color="accent"
(click)="openDeleteProjectDialog($event, pw.project)"
[matTooltip]="'project-listing.delete.action.label' | translate"
>
<mat-icon svgIcon="red:trash"></mat-icon>
</button>
<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
color="accent"
(click)="openAssignProjectOwnerDialog($event, pw.project)"
mat-icon-button
[matTooltip]="'project-listing.assign.action.label' | translate"
>
<mat-icon svgIcon="red:assign"></mat-icon>
</button>
</div>
</div>
</div>
</div>
</div>
<div class="grid-container">
<div class="table-col-name">
<span class="small-label" translate="project-listing.table-col-names.name.label"></span>
</div>
<div class="table-col-name">
<span class="small-label" translate="project-listing.table-col-names.owner.label"></span>
</div>
<div class="table-col-name flex-end">
<span class="small-label" translate="project-listing.table-col-names.status.label"></span>
</div>
<div class="no-data" *ngIf="appStateService.allProjects?.length === 0 "
translate="project-listing.no-projects.label"></div>
<div class="table-item pointer"
[routerLink]="'/ui/projects/'+pw.project.projectId"
*ngFor="let pw of appStateService.allProjects | sortBy:sortingOption.order:sortingOption.column"
>
<div class="right-fixed-container">
<div>
<div class="table-item-title table-item-title--large">
{{pw.project.projectName}}
</div>
<div class="small-label stats-subtitle">
<div>
<mat-icon svgIcon="red:document"></mat-icon>
{{documentCount(pw)}}</div>
<div>
<mat-icon svgIcon="red:user"></mat-icon>
{{userCount(pw)}}</div>
<div>
<mat-icon svgIcon="red:calendar"></mat-icon>
{{pw.project.date | date:'mediumDate'}}
<redaction-simple-doughnut-chart
[config]="projectsChartData"
[strokeWidth]="15"
[subtitle]="'project-listing.stats.charts.projects.label'"
></redaction-simple-doughnut-chart>
<div class="project-stats-container">
<div class="project-stats-item">
<mat-icon svgIcon="red:document"></mat-icon>
<div>
<div class="heading">{{ totalPages }}</div>
<div translate="project-listing.stats.analyzed-pages.label"></div>
</div>
</div>
<div class="project-stats-item">
<mat-icon svgIcon="red:user"></mat-icon>
<div>
<div class="heading">{{ totalPeople }}</div>
<div translate="project-listing.stats.total-people.label"></div>
</div>
</div>
</div>
<div *ngIf="pw.project.dueDate">
<mat-icon svgIcon="red:lightning"></mat-icon>
{{pw.project.dueDate | date:'mediumDate'}}
</div>
</div>
</div>
<div>
<redaction-initials-avatar [userId]="pw.project.ownerId"
withName="true"
></redaction-initials-avatar>
<redaction-simple-doughnut-chart
[config]="documentsChartData"
[strokeWidth]="15"
[subtitle]="'project-listing.stats.charts.total-documents.label'"
></redaction-simple-doughnut-chart>
</div>
<div class="status-container">
<redaction-status-bar
[config]="getProjectStatusConfig(pw)"
></redaction-status-bar>
<div class="action-buttons">
<button mat-icon-button color="accent" (click)="openDeleteProjectDialog($event,pw.project)"
[matTooltip]="'project-listing.delete.action.label'|translate">
<mat-icon svgIcon="red:trash"></mat-icon>
</button>
<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 color="accent" (click)="openAssignProjectOwnerDialog($event,pw.project)" mat-icon-button
[matTooltip]="'project-listing.assign.action.label'|translate">
<mat-icon svgIcon="red:assign"></mat-icon>
</button>
</div>
</div>
</div>
</div>
</div>
<div class="right-fixed-container">
<div>
<redaction-simple-doughnut-chart [config]="projectsChartData"
[strokeWidth]="15"
[subtitle]="'project-listing.stats.charts.projects.label'"
></redaction-simple-doughnut-chart>
<div class="project-stats-container">
<div class="project-stats-item">
<mat-icon svgIcon="red:document"></mat-icon>
<div>
<div class="heading">{{totalPages}}</div>
<div translate="project-listing.stats.analyzed-pages.label"></div>
</div>
</div>
<div class="project-stats-item">
<mat-icon svgIcon="red:user"></mat-icon>
<div>
<div class="heading">{{totalPeople}}</div>
<div translate="project-listing.stats.total-people.label"></div>
</div>
</div>
</div>
</div>
<div>
<redaction-simple-doughnut-chart [config]="documentsChartData"
[strokeWidth]="15"
[subtitle]="'project-listing.stats.charts.total-documents.label'"
></redaction-simple-doughnut-chart>
</div>
</div>
</div>

View File

@ -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;
}
}
}
}
}

View File

@ -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 }));
}
}

View File

@ -1,231 +1,317 @@
<div *ngIf="!appStateService.activeProject"
[innerHTML]="'project-overview.no-project.label' | translate:{projectId: activeProject.projectId}"
class="heading-l"></div>
<div
*ngIf="!appStateService.activeProject"
[innerHTML]="
'project-overview.no-project.label' | translate: { projectId: activeProject.projectId }
"
class="heading-l"
></div>
<div *ngIf="appStateService.activeProject" class="page-header">
<div class="filters flex-row">
<div translate="filters.filter-by.label"></div>
<button mat-button translate="filters.status.label">
<mat-icon svgIcon="red:status"></mat-icon>
</button>
<button mat-button translate="filters.people.label">
<mat-icon svgIcon="red:user"></mat-icon>
</button>
<button mat-button translate="filters.due-date.label">
<mat-icon svgIcon="red:lightning"></mat-icon>
</button>
<button mat-button translate="filters.created-on.label">
<mat-icon svgIcon="red:calendar"></mat-icon>
</button>
<button mat-button translate="filters.project.label">
<mat-icon svgIcon="red:folder"></mat-icon>
</button>
<button mat-button translate="filters.document.label">
<mat-icon svgIcon="red:document"></mat-icon>
</button>
</div>
<button (click)="fileInput.click()" color="primary" mat-flat-button
translate="project-overview.upload-document.label"></button>
<input #fileInput (change)="uploadFiles($event.target.files)" class="file-upload-input" multiple="true"
type="file">
<div class="filters flex-row">
<div translate="filters.filter-by.label"></div>
<button mat-button translate="filters.status.label">
<mat-icon svgIcon="red:status"></mat-icon>
</button>
<button mat-button translate="filters.people.label">
<mat-icon svgIcon="red:user"></mat-icon>
</button>
<button mat-button translate="filters.due-date.label">
<mat-icon svgIcon="red:lightning"></mat-icon>
</button>
<button mat-button translate="filters.created-on.label">
<mat-icon svgIcon="red:calendar"></mat-icon>
</button>
<button mat-button translate="filters.project.label">
<mat-icon svgIcon="red:folder"></mat-icon>
</button>
<button mat-button translate="filters.document.label">
<mat-icon svgIcon="red:document"></mat-icon>
</button>
</div>
<button
(click)="fileInput.click()"
color="primary"
mat-flat-button
translate="project-overview.upload-document.label"
></button>
<input
#fileInput
(change)="uploadFiles($event.target.files)"
class="file-upload-input"
multiple="true"
type="file"
/>
</div>
<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 } }}
</span>
</div>
<div class="actions">
<div translate="project-overview.table-header.bulk-select.label"></div>
<mat-form-field appearance="none" class="red-select">
<mat-select [(ngModel)]="sortingOption" panelClass="red-select-panel">
<mat-option *ngFor="let option of sortingOptions" [value]="option">
{{option.label | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<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 }
}}
</span>
</div>
<div class="actions">
<div translate="project-overview.table-header.bulk-select.label"></div>
<mat-form-field appearance="none" class="red-select">
<mat-select [(ngModel)]="sortingOption" panelClass="red-select-panel">
<mat-option *ngFor="let option of sortingOptions" [value]="option">
{{ option.label | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
<div class="grid-container">
<!-- Table column names-->
<div class="table-col-name"></div>
<div class="table-col-name">
<span
class="small-label"
translate="project-overview.table-col-names.name.label"
></span>
</div>
<div class="table-col-name pointer" (click)="toggleSortByAddedOn()">
<span
class="small-label"
translate="project-overview.table-col-names.added-on.label"
></span>
<div class="sort-arrows-container">
<mat-icon
svgIcon="red:arrow-up"
[color]="sortingOption === sortingOptions[0] ? 'primary' : 'currentColor'"
></mat-icon>
<mat-icon
svgIcon="red:arrow-down"
[color]="sortingOption === sortingOptions[1] ? 'primary' : 'currentColor'"
></mat-icon>
</div>
</div>
<div class="table-col-name">
<span
class="small-label"
translate="project-overview.table-col-names.needs-work.label"
></span>
</div>
<div class="table-col-name">
<span
class="small-label"
translate="project-overview.table-col-names.assigned-to.label"
></span>
</div>
<div class="table-col-name flex-end">
<span
class="small-label"
translate="project-overview.table-col-names.status.label"
></span>
</div>
<div
class="table-item"
[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="pr-0">
<div
class="select-oval"
[class.active]="isFileSelected(fileStatus)"
(click)="toggleFileSelected($event, fileStatus)"
></div>
</div>
<div
class="table-item-title"
[matTooltip]="'[' + fileStatus.status + '] ' + fileStatus.filename"
>
{{ fileStatus.filename }}
</div>
<div class="small-label">
{{ fileStatus.added | date: 'd MMM. yyyy, hh:mm a' }}
</div>
<div class="needs-work">
<redaction-annotation-icon type="redaction"></redaction-annotation-icon>
<redaction-annotation-icon type="hint"></redaction-annotation-icon>
</div>
<div class="assigned-to">
<redaction-initials-avatar
withName="true"
[userId]="fileStatus.currentReviewer"
></redaction-initials-avatar>
</div>
<div class="status-container">
<redaction-status-bar
[config]="[
{
color:
fileStatus.status === 'PROCESSED'
? 'finished'
: fileStatus.status === 'ERROR'
? 'under-approval'
: 'under-review',
length: 1
}
]"
></redaction-status-bar>
<div class="action-buttons">
<button
(click)="openDeleteFileDialog($event, fileStatus)"
color="accent"
mat-icon-button
[matTooltip]="'project-overview.delete.action.label' | translate"
>
<mat-icon svgIcon="red:trash"></mat-icon>
</button>
<button
(click)="$event.stopPropagation()"
color="accent"
mat-icon-button
[matTooltip]="'project-overview.report.action.label' | translate"
>
<mat-icon svgIcon="red:report"></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>
<button
(click)="reanalyseFile($event, fileStatus)"
color="accent"
mat-icon-button
[matTooltip]="'project-overview.bar-charts.action.label' | translate"
>
<mat-icon svgIcon="red:analyse"></mat-icon>
</button>
</div>
</div>
</div>
</div>
</div>
<div class="grid-container">
<!-- Table column names-->
<div class="table-col-name"></div>
<div class="table-col-name">
<span class="small-label" translate="project-overview.table-col-names.name.label"></span>
</div>
<div class="table-col-name pointer" (click)="toggleSortByAddedOn()">
<span class="small-label" translate="project-overview.table-col-names.added-on.label"></span>
<div class="sort-arrows-container">
<mat-icon svgIcon="red:arrow-up"
[color]="sortingOption === sortingOptions[0] ? 'primary' : 'currentColor'"></mat-icon>
<mat-icon svgIcon="red:arrow-down"
[color]="sortingOption === sortingOptions[1] ? 'primary' : 'currentColor'"></mat-icon>
</div>
</div>
<div class="table-col-name">
<span class="small-label" translate="project-overview.table-col-names.needs-work.label"></span>
</div>
<div class="table-col-name">
<span class="small-label" translate="project-overview.table-col-names.assigned-to.label"></span>
</div>
<div class="table-col-name flex-end">
<span class="small-label" translate="project-overview.table-col-names.status.label"></span>
</div>
<div class="table-item"
[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="pr-0">
<div class="select-oval"
[class.active]="isFileSelected(fileStatus)"
(click)="toggleFileSelected($event, fileStatus)"></div>
</div>
<div class="table-item-title" [matTooltip]="'['+fileStatus.status+'] '+fileStatus.filename ">
{{ fileStatus.filename }}
</div>
<div class="small-label">
{{ fileStatus.added | date:'d MMM. yyyy, hh:mm a' }}
</div>
<div class="needs-work">
<redaction-annotation-icon type="redaction"></redaction-annotation-icon>
<redaction-annotation-icon type="hint"></redaction-annotation-icon>
</div>
<div class="assigned-to">
<redaction-initials-avatar
withName="true"
[userId]="fileStatus.currentReviewer"
></redaction-initials-avatar>
</div>
<div class="status-container">
<redaction-status-bar
[config]="[{ color: (fileStatus.status === 'PROCESSED' ? 'finished': fileStatus.status ==='ERROR'?'under-approval' : 'under-review'), length: 1 }]"
></redaction-status-bar>
<div class="action-buttons">
<button (click)="openDeleteFileDialog($event,fileStatus)" color="accent" mat-icon-button
[matTooltip]="'project-overview.delete.action.label'|translate">
<mat-icon svgIcon="red:trash"></mat-icon>
<div class="project-details-container right-fixed-container">
<div class="actions-row">
<button mat-icon-button (click)="openDeleteProjectDialog($event)">
<mat-icon svgIcon="red:trash"></mat-icon>
</button>
<button (click)="$event.stopPropagation()" color="accent" mat-icon-button
[matTooltip]="'project-overview.report.action.label'|translate">
<mat-icon svgIcon="red:report"></mat-icon>
<button mat-icon-button (click)="openEditProjectDialog($event)">
<mat-icon svgIcon="red:edit"></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 mat-icon-button (click)="openDetailsDialog($event)">
<mat-icon svgIcon="red:report"></mat-icon>
</button>
<button (click)="reanalyseFile($event,fileStatus)" color="accent" mat-icon-button
[matTooltip]="'project-overview.bar-charts.action.label'|translate">
<mat-icon svgIcon="red:analyse"></mat-icon>
</button>
</div>
</div>
</div>
</div>
</div>
<div class="project-details-container right-fixed-container">
<div class="actions-row">
<button mat-icon-button (click)="openDeleteProjectDialog($event)">
<mat-icon svgIcon="red:trash"></mat-icon>
</button>
<button mat-icon-button (click)="openEditProjectDialog($event)">
<mat-icon svgIcon="red:edit"></mat-icon>
</button>
<button mat-icon-button (click)="openDetailsDialog($event)">
<mat-icon svgIcon="red:report"></mat-icon>
</button>
</div>
<div class="small-label stats-subtitle mt-20">
<div>
<mat-icon svgIcon="red:pages"></mat-icon>
{{ appStateService.activeProject.files.length }}
</div>
<div>
<mat-icon svgIcon="red:user"></mat-icon>
{{ appStateService.activeProject.project.memberIds.length }}
</div>
<div>
<mat-icon svgIcon="red:calendar"></mat-icon>
{{ appStateService.activeProject.project.date | date:'d MMM. yyyy' }}
</div>
<div *ngIf="appStateService.activeProject.project.dueDate">
<mat-icon svgIcon="red:lightning"></mat-icon>
{{appStateService.activeProject.project.dueDate | date:'mediumDate'}}
</div>
</div>
<div class="heading-xl mt-20">
{{ appStateService.activeProject.project.projectName }}
</div>
<div class="owner flex-row mt-20">
<redaction-initials-avatar [userId]="activeProject.ownerId"
size="large"
withName="true"
></redaction-initials-avatar>
</div>
<div class="mt-20">
{{ appStateService.activeProject.project.description }}
</div>
<div class="project-team mt-20">
<div class="all-caps-label" translate="project-overview.project-details.project-team.label"></div>
<div class="flex mt-20 members-container">
<div *ngFor="let userId of displayMembers" class="member">
<redaction-initials-avatar [userId]="userId" size="large"></redaction-initials-avatar>
</div>
<div class="member" *ngIf="overflowCount">
<div class="oval large white-dark">+{{overflowCount}}</div>
</div>
<div class="member pointer" (click)="openAssignProjectMembersDialog()">
<div class="oval red-white large">+</div>
<div class="small-label stats-subtitle mt-20">
<div>
<mat-icon svgIcon="red:pages"></mat-icon>
{{ appStateService.activeProject.files.length }}
</div>
<div>
<mat-icon svgIcon="red:user"></mat-icon>
{{ appStateService.activeProject.project.memberIds.length }}
</div>
<div>
<mat-icon svgIcon="red:calendar"></mat-icon>
{{ appStateService.activeProject.project.date | date: 'd MMM. yyyy' }}
</div>
<div *ngIf="appStateService.activeProject.project.dueDate">
<mat-icon svgIcon="red:lightning"></mat-icon>
{{ appStateService.activeProject.project.dueDate | date: 'mediumDate' }}
</div>
</div>
</div>
</div>
<div class="heading-xl mt-20">
{{ appStateService.activeProject.project.projectName }}
</div>
<div class="mt-32">
<redaction-simple-doughnut-chart [config]="documentsChartData"
[strokeWidth]="15"
[radius]="70"
[subtitle]="'project-overview.project-details.charts.total-documents.label'"
direction="row"
></redaction-simple-doughnut-chart>
</div>
<div class="owner flex-row mt-20">
<redaction-initials-avatar
[userId]="activeProject.ownerId"
size="large"
withName="true"
></redaction-initials-avatar>
</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 class="mt-20">
{{ appStateService.activeProject.project.description }}
</div>
<div class="project-team mt-20">
<div
class="all-caps-label"
translate="project-overview.project-details.project-team.label"
></div>
<div class="flex mt-20 members-container">
<div *ngFor="let userId of displayMembers" class="member">
<redaction-initials-avatar
[userId]="userId"
size="large"
></redaction-initials-avatar>
</div>
<div class="member" *ngIf="overflowCount">
<div class="oval large white-dark">+{{ overflowCount }}</div>
</div>
<div class="member pointer" (click)="openAssignProjectMembersDialog()">
<div class="oval red-white large">+</div>
</div>
</div>
</div>
<div class="mt-32">
<redaction-simple-doughnut-chart
[config]="documentsChartData"
[strokeWidth]="15"
[radius]="70"
[subtitle]="'project-overview.project-details.charts.total-documents.label'"
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>
</div>

View File

@ -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;
}

View File

@ -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];
}
}

View File

@ -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<FileStatus>();
public fileStatusChanged = new EventEmitter<FileStatus>();
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<string>();
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<string>();
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();
}
}

File diff suppressed because it is too large Load Diff

View File

@ -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;
}
}

View File

@ -1,5 +1,5 @@
.mat-dialog-container {
border-radius: 8px;
border-radius: 8px;
}
.dialog {

View File

@ -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;
}
}
}
}

View File

@ -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;
}
}
}

View File

@ -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 */
}
}

View File

@ -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;
}

View File

@ -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;
}
}
}
}
}