blah
This commit is contained in:
parent
69d7f48108
commit
30d40fef03
@ -1,4 +1,6 @@
|
|||||||
<div class="flex-row">
|
<div class="flex-row">
|
||||||
<div [className]="colorClass + ' oval ' + size">{{initials}}</div>
|
<div [className]="colorClass + ' oval ' + size">{{ initials }}</div>
|
||||||
<div *ngIf="withName" class="clamp-2">{{username || ('initials-avatar.unassigned.label' | translate)}}</div>
|
<div *ngIf="withName" class="clamp-2">
|
||||||
|
{{ username || ('initials-avatar.unassigned.label' | translate) }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -3,57 +3,56 @@ import { UserService } from '../../user/user.service';
|
|||||||
import { User } from '@redaction/red-ui-http';
|
import { User } from '@redaction/red-ui-http';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'redaction-initials-avatar',
|
selector: 'redaction-initials-avatar',
|
||||||
templateUrl: './initials-avatar.component.html',
|
templateUrl: './initials-avatar.component.html',
|
||||||
styleUrls: ['./initials-avatar.component.scss']
|
styleUrls: ['./initials-avatar.component.scss']
|
||||||
})
|
})
|
||||||
export class InitialsAvatarComponent implements OnInit, OnChanges {
|
export class InitialsAvatarComponent implements OnInit, OnChanges {
|
||||||
@Input()
|
@Input()
|
||||||
public userId: string;
|
public userId: string;
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
public color = 'lightgray';
|
public color = 'lightgray';
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
public size: 'small' | 'large' = 'small';
|
public size: 'small' | 'large' = 'small';
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
public withName = false;
|
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 {
|
ngOnChanges(): void {
|
||||||
this._user = this._userService.getUserById(this.userId);
|
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 '?'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return this._userService.getName(this._user)
|
public get username(): string {
|
||||||
.split(' ')
|
return this._userService.getName(this._user);
|
||||||
.filter(value => value !== ' ')
|
}
|
||||||
.filter((value, idx) => idx < 2)
|
|
||||||
.map((str) => str[0])
|
public get initials(): string {
|
||||||
.join('');
|
if (!this._user) {
|
||||||
}
|
return '?';
|
||||||
|
}
|
||||||
public get colorClass() {
|
|
||||||
if (this.color.includes('-')) {
|
return this._userService
|
||||||
return this.color;
|
.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}`
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
<div class="rectangle-container" [ngClass]="{ small: small }">
|
<div class="rectangle-container" [ngClass]="{ small: small }">
|
||||||
<div *ngFor="let rect of config" class="section-wrapper" [style]="'flex: ' + rect.length + ';'">
|
<div *ngFor="let rect of config" class="section-wrapper" [style]="'flex: ' + rect.length + ';'">
|
||||||
<div [className]="'rectangle ' + rect.color "></div>
|
<div [className]="'rectangle ' + rect.color"></div>
|
||||||
<div *ngIf="rect.label" [ngClass]="labelClass">{{ rect.label }}</div>
|
<div *ngIf="rect.label" [ngClass]="labelClass">{{ rect.label }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,78 +1,78 @@
|
|||||||
@import '../../../assets/styles/red-variables';
|
@import '../../../assets/styles/red-variables';
|
||||||
|
|
||||||
.rectangle-container {
|
.rectangle-container {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-width: 12px;
|
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 {
|
.rectangle {
|
||||||
width: 12px;
|
height: 4px;
|
||||||
}
|
|
||||||
|
|
||||||
.section-wrapper {
|
&.UNASSIGNED {
|
||||||
display: flex;
|
background-color: $grey-5;
|
||||||
align-items: center;
|
}
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-wrapper:first-child {
|
&.UNDER_REVIEW {
|
||||||
.rectangle {
|
background-color: $yellow-1;
|
||||||
border-radius: 6px 0 0 6px;
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-wrapper:last-child {
|
&.UNDER_APPROVAL {
|
||||||
.rectangle {
|
background-color: $red-1;
|
||||||
border-radius: 0 6px 6px 0;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
&:first-child {
|
&.APPROVED {
|
||||||
.rectangle {
|
background-color: $blue-2;
|
||||||
border-radius: 6px;
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.rectangle {
|
&.SUBMITTED {
|
||||||
height: 4px;
|
background-color: $blue-3;
|
||||||
|
}
|
||||||
|
|
||||||
&.UNASSIGNED {
|
&.EFSA {
|
||||||
background-color: $grey-5;
|
background-color: $blue-4;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.UNDER_REVIEW {
|
&.FINISHED {
|
||||||
background-color: $yellow-1;
|
background-color: $green-2;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.UNDER_APPROVAL {
|
&.ACTIVE {
|
||||||
background-color: $red-1;
|
background-color: $primary;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.APPROVED {
|
&.ARCHIVED {
|
||||||
background-color: $blue-2;
|
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,40 +1,51 @@
|
|||||||
<section class="dialog">
|
<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 class="dialog-content">
|
||||||
<div
|
<div class="red-input-group">
|
||||||
[translate]="project?.projectId ? 'project-listing.add-edit-dialog.header-edit.label': 'project-listing.add-edit-dialog.header-new.label'"
|
<label translate="project-listing.add-edit-dialog.form.name.label"></label>
|
||||||
class="dialog-header heading-l">
|
<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="dialog-actions">
|
||||||
<div class="red-input-group">
|
<button
|
||||||
<label translate="project-listing.add-edit-dialog.form.name.label"></label>
|
[disabled]="projectForm.invalid"
|
||||||
<input formControlName="projectName" name="projectName" type="text">
|
color="primary"
|
||||||
</div>
|
mat-flat-button
|
||||||
<div class="red-input-group">
|
translate="project-listing.add-edit-dialog.actions.save.label"
|
||||||
<label translate="project-listing.add-edit-dialog.form.description.label"></label>
|
type="submit"
|
||||||
<textarea formControlName="description" name="description" type="text" rows="5"></textarea>
|
></button>
|
||||||
</div>
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
<mat-form-field class="mt-20">
|
<button (click)="dialogRef.close()" class="dialog-close" mat-icon-button>
|
||||||
<mat-label>{{'project-listing.add-edit-dialog.form.due-date.label' | translate}}</mat-label>
|
<mat-icon svgIcon="red:close"></mat-icon>
|
||||||
<input matInput [matDatepicker]="picker" formControlName="dueDate">
|
</button>
|
||||||
<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>
|
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@ -2,11 +2,11 @@ import { Injectable } from '@angular/core';
|
|||||||
import { FileDetailsDialogComponent } from './file-details-dialog/file-details-dialog.component';
|
import { FileDetailsDialogComponent } from './file-details-dialog/file-details-dialog.component';
|
||||||
import { MatDialog, MatDialogRef } from '@angular/material/dialog';
|
import { MatDialog, MatDialogRef } from '@angular/material/dialog';
|
||||||
import {
|
import {
|
||||||
FileStatus,
|
FileStatus,
|
||||||
FileUploadControllerService,
|
FileUploadControllerService,
|
||||||
ManualRedactionControllerService,
|
ManualRedactionControllerService,
|
||||||
ManualRedactionEntry,
|
ManualRedactionEntry,
|
||||||
Project
|
Project
|
||||||
} from '@redaction/red-ui-http';
|
} from '@redaction/red-ui-http';
|
||||||
import { ConfirmationDialogComponent } from '../common/confirmation-dialog/confirmation-dialog.component';
|
import { ConfirmationDialogComponent } from '../common/confirmation-dialog/confirmation-dialog.component';
|
||||||
import { NotificationService, NotificationType } from '../notification/notification.service';
|
import { NotificationService, NotificationType } from '../notification/notification.service';
|
||||||
@ -19,180 +19,258 @@ import { ManualRedactionDialogComponent } from './manual-redaction-dialog/manual
|
|||||||
import { Annotations } from '@pdftron/webviewer';
|
import { Annotations } from '@pdftron/webviewer';
|
||||||
|
|
||||||
const dialogConfig = {
|
const dialogConfig = {
|
||||||
width: '600px',
|
width: '600px',
|
||||||
maxWidth: '90vw',
|
maxWidth: '90vw',
|
||||||
autoFocus: false
|
autoFocus: false
|
||||||
};
|
};
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
})
|
})
|
||||||
export class DialogService {
|
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,
|
public openFileDetailsDialog(
|
||||||
private readonly _translateService: TranslateService,
|
$event: MouseEvent,
|
||||||
private readonly _appStateService: AppStateService,
|
file: FileStatus
|
||||||
private readonly _fileUploadControllerService: FileUploadControllerService,
|
): MatDialogRef<FileDetailsDialogComponent> {
|
||||||
private readonly _notificationService: NotificationService,
|
$event.stopPropagation();
|
||||||
private readonly _manualRedactionControllerService: ManualRedactionControllerService) {
|
return this._dialog.open(FileDetailsDialogComponent, {
|
||||||
|
...dialogConfig,
|
||||||
}
|
data: file
|
||||||
|
|
||||||
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);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
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> {
|
ref.afterClosed().subscribe((result) => {
|
||||||
const ref = this._dialog.open(ManualRedactionDialogComponent, {
|
if (result) {
|
||||||
...dialogConfig,
|
const file = this._appStateService.getFileById(projectId, fileId);
|
||||||
autoFocus: true,
|
this._fileUploadControllerService.deleteFile(file.projectId, file.fileId).subscribe(
|
||||||
data: $event
|
async () => {
|
||||||
});
|
await this._appStateService.reloadActiveProjectFiles();
|
||||||
|
if (cb) cb();
|
||||||
ref.afterClosed().subscribe(result => {
|
},
|
||||||
if (cb) {
|
() => {
|
||||||
cb(result);
|
this._notificationService.showToastNotification(
|
||||||
}
|
this._translateService.instant('delete-file-error.label', file),
|
||||||
});
|
null,
|
||||||
|
NotificationType.ERROR
|
||||||
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);
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return ref;
|
return ref;
|
||||||
}
|
}
|
||||||
|
|
||||||
public suggestRemoveAnnotation($event: MouseEvent, annotation: Annotations.Annotation, projectId: string, fileId: string): MatDialogRef<ConfirmationDialogComponent> {
|
public openManualRedactionDialog(
|
||||||
$event.stopPropagation();
|
$event: ManualRedactionEntry,
|
||||||
|
cb?: Function
|
||||||
const parts = annotation.Id.split(':');
|
): MatDialogRef<ManualRedactionDialogComponent> {
|
||||||
const annotationId = parts[parts.length - 1];
|
const ref = this._dialog.open(ManualRedactionDialogComponent, {
|
||||||
|
...dialogConfig,
|
||||||
const ref = this._dialog.open(ConfirmationDialogComponent, {
|
autoFocus: true,
|
||||||
width: '400px',
|
data: $event
|
||||||
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);
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return ref;
|
ref.afterClosed().subscribe((result) => {
|
||||||
}
|
if (cb) {
|
||||||
|
cb(result);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
public openEditProjectDialog($event: MouseEvent, project: Project): MatDialogRef<AddEditProjectDialogComponent> {
|
return ref;
|
||||||
$event.stopPropagation();
|
}
|
||||||
return this._dialog.open(AddEditProjectDialogComponent, {
|
|
||||||
...dialogConfig,
|
|
||||||
autoFocus: true,
|
|
||||||
data: project
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public openDeleteProjectDialog($event: MouseEvent, project: Project, cb?: Function): MatDialogRef<ConfirmationDialogComponent> {
|
public acceptSuggestionAnnotation(
|
||||||
$event.stopPropagation();
|
$event: MouseEvent,
|
||||||
const ref = this._dialog.open(ConfirmationDialogComponent, dialogConfig);
|
annotation: Annotations.Annotation,
|
||||||
ref.afterClosed().subscribe(async result => {
|
projectId: string,
|
||||||
if (result) {
|
fileId: string
|
||||||
await this._appStateService.deleteProject(project);
|
): MatDialogRef<ConfirmationDialogComponent> {
|
||||||
if (cb) cb();
|
$event.stopPropagation();
|
||||||
}
|
|
||||||
});
|
|
||||||
return ref;
|
|
||||||
}
|
|
||||||
|
|
||||||
public openAssignProjectMembersAndOwnerDialog($event: MouseEvent, project: Project, cb?: Function): MatDialogRef<AssignOwnerDialogComponent> {
|
const parts = annotation.Id.split(':');
|
||||||
$event?.stopPropagation();
|
const annotationId = parts[parts.length - 1];
|
||||||
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> {
|
const ref = this._dialog.open(ConfirmationDialogComponent, dialogConfig);
|
||||||
$event.stopPropagation();
|
ref.afterClosed().subscribe((result) => {
|
||||||
const ref = this._dialog.open(AssignOwnerDialogComponent, {
|
if (result) {
|
||||||
...dialogConfig,
|
this._manualRedactionControllerService
|
||||||
data: { type: 'file', file: file }
|
.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(() => {
|
return ref;
|
||||||
if (cb) cb();
|
}
|
||||||
});
|
|
||||||
|
|
||||||
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> {
|
const parts = annotation.Id.split(':');
|
||||||
$event.stopPropagation();
|
const annotationId = parts[parts.length - 1];
|
||||||
return this._dialog.open(ProjectDetailsDialogComponent, {
|
|
||||||
...dialogConfig,
|
|
||||||
data: project
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public openAddProjectDialog(cb?: Function): MatDialogRef<AddEditProjectDialogComponent> {
|
const ref = this._dialog.open(ConfirmationDialogComponent, {
|
||||||
const ref = this._dialog.open(AddEditProjectDialogComponent, {
|
width: '400px',
|
||||||
...dialogConfig,
|
maxWidth: '90vw'
|
||||||
autoFocus: true
|
});
|
||||||
});
|
|
||||||
|
|
||||||
ref.afterClosed().subscribe(result => {
|
ref.afterClosed().subscribe((result) => {
|
||||||
if (result && cb) cb();
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,53 +1,53 @@
|
|||||||
<section class="dialog">
|
<section class="dialog">
|
||||||
<form (submit)="handleAddRedaction()" [formGroup]="redactionForm">
|
<form (submit)="handleAddRedaction()" [formGroup]="redactionForm">
|
||||||
<div
|
<div class="dialog-header heading-l" translate="manual-redaction.dialog.header.label"></div>
|
||||||
class="dialog-header heading-l" translate="manual-redaction.dialog.header.label">
|
|
||||||
|
|
||||||
</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">
|
<div class="red-input-group">
|
||||||
<label translate="manual-redaction.dialog.content.text.label"></label>
|
<label translate="manual-redaction.dialog.content.dictionary.label"></label>
|
||||||
</div>
|
<mat-select formControlName="dictionary">
|
||||||
{{ addRedactionRequest.value }}
|
<mat-option
|
||||||
|
*ngFor="let dictionary of dictionaries | async"
|
||||||
|
[value]="dictionary.type"
|
||||||
|
>
|
||||||
|
{{ dictionary.type }}
|
||||||
|
</mat-option>
|
||||||
|
</mat-select>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="red-input-group">
|
<div class="red-input-group">
|
||||||
<label translate="manual-redaction.dialog.content.reason.label"></label>
|
<mat-checkbox color="primary" formControlName="addToDictionary">{{
|
||||||
<textarea formControlName="reason" name="reason" type="text" rows="2"></textarea>
|
'manual-redaction.dialog.content.dictionary.add.label' | translate
|
||||||
</div>
|
}}</mat-checkbox>
|
||||||
|
</div>
|
||||||
<div class="red-input-group">
|
</div>
|
||||||
<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="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>
|
</section>
|
||||||
|
|||||||
@ -1,36 +1,55 @@
|
|||||||
import {NgModule} from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import {CommonModule} from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import {MatIconModule, MatIconRegistry} from '@angular/material/icon';
|
import { MatIconModule, MatIconRegistry } from '@angular/material/icon';
|
||||||
import {DomSanitizer} from '@angular/platform-browser';
|
import { DomSanitizer } from '@angular/platform-browser';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [CommonModule, MatIconModule],
|
imports: [CommonModule, MatIconModule],
|
||||||
declarations: [],
|
declarations: [],
|
||||||
exports: [MatIconModule]
|
exports: [MatIconModule]
|
||||||
})
|
})
|
||||||
export class IconsModule {
|
export class IconsModule {
|
||||||
constructor(
|
constructor(private iconRegistry: MatIconRegistry, private sanitizer: DomSanitizer) {
|
||||||
private iconRegistry: MatIconRegistry,
|
const icons = [
|
||||||
private sanitizer: DomSanitizer
|
'add',
|
||||||
) {
|
'analyse',
|
||||||
const icons = [
|
'arrow-down',
|
||||||
'add', 'analyse', 'arrow-down', 'arrow-up', 'arrow-right', 'assign', 'calendar',
|
'arrow-up',
|
||||||
'check', 'close', 'document', 'double-chevron-right', 'download',
|
'arrow-right',
|
||||||
'edit', 'error', 'folder', 'info', 'lightning', 'logout', 'menu', 'pages',
|
'assign',
|
||||||
'plus', 'preview', 'refresh', 'report', 'secret', 'sort-asc', 'sort-desc',
|
'calendar',
|
||||||
'status', 'trash', 'user', 'check-alt',
|
'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) {
|
for (const icon of icons) {
|
||||||
iconRegistry.addSvgIconInNamespace(
|
iconRegistry.addSvgIconInNamespace(
|
||||||
'red',
|
'red',
|
||||||
icon,
|
icon,
|
||||||
sanitizer.bypassSecurityTrustResourceUrl(
|
sanitizer.bypassSecurityTrustResourceUrl(`/assets/icons/general/${icon}.svg`)
|
||||||
`/assets/icons/general/${icon}.svg`
|
);
|
||||||
)
|
}
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,69 +1,110 @@
|
|||||||
<div class="red-top-bar">
|
<div class="red-top-bar">
|
||||||
<div class="top-bar-row">
|
<div class="top-bar-row">
|
||||||
<div class="menu visible-lt-lg">
|
<div class="menu visible-lt-lg">
|
||||||
<button [matMenuTriggerFor]="menuNav" mat-flat-button>
|
<button [matMenuTriggerFor]="menuNav" mat-flat-button>
|
||||||
<mat-icon svgIcon="red:menu"></mat-icon>
|
<mat-icon svgIcon="red:menu"></mat-icon>
|
||||||
</button>
|
</button>
|
||||||
<mat-menu #menuNav="matMenu">
|
<mat-menu #menuNav="matMenu">
|
||||||
<button mat-menu-item routerLink="/ui/projects"
|
<button
|
||||||
translate="top-bar.navigation-items.projects.label">
|
mat-menu-item
|
||||||
</button>
|
routerLink="/ui/projects"
|
||||||
<button *ngIf="appStateService.activeProject"
|
translate="top-bar.navigation-items.projects.label"
|
||||||
[routerLink]="'/ui/projects/'+appStateService.activeProjectId"
|
></button>
|
||||||
mat-menu-item>{{appStateService.activeProject.project.projectName}}</button>
|
<button
|
||||||
<button *ngIf="appStateService.activeFile"
|
*ngIf="appStateService.activeProject"
|
||||||
[routerLink]="'/ui/projects/'+appStateService.activeProjectId+'/file/'+appStateService.activeFile.fileId"
|
[routerLink]="'/ui/projects/' + appStateService.activeProjectId"
|
||||||
mat-menu-item>{{appStateService.activeFile.filename}}</button>
|
mat-menu-item
|
||||||
</mat-menu>
|
>
|
||||||
|
{{ 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>
|
||||||
<div class="menu flex-2 visible-lg breadcrumbs-container">
|
<div class="divider"></div>
|
||||||
<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>
|
</div>
|
||||||
<div class="red-content">
|
<div class="red-content">
|
||||||
<router-outlet></router-outlet>
|
<router-outlet></router-outlet>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,25 +1,25 @@
|
|||||||
@import "../../../assets/styles/red-variables";
|
@import '../../../assets/styles/red-variables';
|
||||||
@import "../../../assets/styles/red-mixins";
|
@import '../../../assets/styles/red-mixins';
|
||||||
|
|
||||||
.breadcrumbs-container {
|
.breadcrumbs-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
|
||||||
.breadcrumb {
|
.breadcrumb {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: $accent;
|
color: $accent;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|
||||||
&:last-child {
|
&:last-child {
|
||||||
color: $primary;
|
color: $primary;
|
||||||
@include line-clamp(1);
|
@include line-clamp(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-icon {
|
||||||
|
vertical-align: middle;
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mat-icon {
|
|
||||||
vertical-align: middle;
|
|
||||||
width: 6px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,191 +1,276 @@
|
|||||||
<section>
|
<section>
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<mat-slide-toggle color="primary"
|
<mat-slide-toggle color="primary" labelPosition="after" [(ngModel)]="redactedView">
|
||||||
labelPosition="after"
|
{{ 'file-preview.view-toggle.label' | translate }}
|
||||||
[(ngModel)]="redactedView">
|
</mat-slide-toggle>
|
||||||
{{ "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>
|
</div>
|
||||||
|
|
||||||
<div class="annotations" [class.activePanel]="!pagesPanelActive" #annotations
|
<div class="flex-1 filename page-title">
|
||||||
tabindex="1"
|
{{ appStateService.activeFile.filename }}
|
||||||
(keyup)="$event.preventDefault()"
|
</div>
|
||||||
(keydown)="$event.preventDefault();"
|
|
||||||
>
|
<div class="flex-1 actions-container">
|
||||||
<div *ngFor="let page of displayedPages">
|
<div class="actions-row">
|
||||||
<div class="page-separator" attr.anotation-page-header="{{page}}">
|
<button mat-icon-button (click)="openDeleteFileDialog($event)">
|
||||||
<span class="all-caps-label"><span translate="page"></span> {{page}}</span>
|
<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>
|
||||||
|
|
||||||
<div *ngFor="let annotation of displayedAnnotations[page].annotations"
|
<button
|
||||||
class="annotation" attr.annotation-id="{{annotation.Id}}"
|
color="primary"
|
||||||
attr.annotation-page="{{page}}"
|
mat-flat-button
|
||||||
[ngClass]="{ active: selectedAnnotation === annotation }"
|
class="arrow-button"
|
||||||
(click)="selectAnnotation(annotation)"
|
[matMenuTriggerFor]="downloadMenu"
|
||||||
>
|
>
|
||||||
|
<span translate="file-preview.download.label"></span>
|
||||||
<redaction-annotation-icon
|
<mat-icon svgIcon="red:arrow-down"></mat-icon>
|
||||||
[type]="getType(annotation) + ' ' + getDictionary(annotation)"></redaction-annotation-icon>
|
</button>
|
||||||
<div class="flex-1">
|
<mat-menu #downloadMenu="matMenu" xPosition="before">
|
||||||
<div><strong>{{getType(annotation) | translate}}</strong></div>
|
<div
|
||||||
<div><strong><span translate="dictionary"></span>: </strong>{{getDictionary(annotation)}}</div>
|
mat-menu-item
|
||||||
<div *ngIf="annotation.getContents()"><strong><span translate="content"></span>:
|
translate="file-preview.download.dropdown.original.label"
|
||||||
</strong>{{annotation.getContents()}}</div>
|
(click)="downloadFile('ORIGINAL')"
|
||||||
<div *ngFor="let comment of annotation['comments']">
|
></div>
|
||||||
<strong><span translate="comment"></span>:</strong> {{ comment.value }}
|
<div
|
||||||
</div>
|
mat-menu-item
|
||||||
</div>
|
translate="file-preview.download.dropdown.annotated.label"
|
||||||
|
(click)="downloadFile('ANNOTATED')"
|
||||||
<div class="page-number">
|
></div>
|
||||||
{{annotation.getPageNumber()}}
|
<div
|
||||||
</div>
|
mat-menu-item
|
||||||
|
translate="file-preview.download.dropdown.redacted.label"
|
||||||
<div class="annotation-actions" *ngIf="isManuallyAddedAnnotation(annotation)">
|
(click)="downloadFile('REDACTED')"
|
||||||
<button mat-icon-button (click)="acceptSuggestionAnnotation($event, annotation)" class="confirm">
|
></div>
|
||||||
<mat-icon svgIcon="red:check-alt"></mat-icon>
|
</mat-menu>
|
||||||
</button>
|
</div>
|
||||||
<button mat-icon-button (click)="suggestRemoveAnnotation($event, annotation)">
|
</div>
|
||||||
<mat-icon svgIcon="red:trash"></mat-icon>
|
|
||||||
</button>
|
<div class="flex red-content-inner">
|
||||||
</div>
|
<div class="left-container">
|
||||||
</div>
|
<redaction-pdf-viewer
|
||||||
</div>
|
*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>
|
</div>
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!--<redaction-full-page-loading-indicator [displayed]="!viewReady"></redaction-full-page-loading-indicator>-->
|
<!--<redaction-full-page-loading-indicator [displayed]="!viewReady"></redaction-full-page-loading-indicator>-->
|
||||||
|
|||||||
@ -1,172 +1,179 @@
|
|||||||
@import "../../../../assets/styles/red-variables";
|
@import '../../../../assets/styles/red-variables';
|
||||||
@import "../../../../assets/styles/red-mixins";
|
@import '../../../../assets/styles/red-mixins';
|
||||||
|
|
||||||
redaction-pdf-viewer {
|
redaction-pdf-viewer {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: calc(100vw - #{$right-container-width});
|
width: calc(100vw - #{$right-container-width});
|
||||||
height: calc(100vh - 110px);
|
height: calc(100vh - 110px);
|
||||||
top: 110px;
|
top: 110px;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.actions-container {
|
.actions-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filename {
|
.filename {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.right-fixed-container {
|
.right-fixed-container {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
width: $right-container-width;
|
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);
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
display: flex;
|
|
||||||
|
|
||||||
.pages, .annotations {
|
.right-title {
|
||||||
overflow-y: scroll;
|
height: 70px;
|
||||||
@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;
|
display: flex;
|
||||||
align-items: flex-end;
|
|
||||||
background-color: $grey-6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.annotation {
|
|
||||||
border-bottom: 1px solid $separator;
|
border-bottom: 1px solid $separator;
|
||||||
padding: 10px;
|
align-items: center;
|
||||||
font-size: 12px;
|
justify-content: space-between;
|
||||||
cursor: pointer;
|
padding: 0 24px;
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
gap: 10px;
|
|
||||||
border-left: 2px solid transparent;
|
|
||||||
|
|
||||||
redaction-annotation-icon {
|
> div {
|
||||||
margin-top: 6px;
|
position: relative;
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
.dot {
|
||||||
background-color: #F9FAFB;
|
background: $primary;
|
||||||
|
height: 10px;
|
||||||
.annotation-actions {
|
width: 10px;
|
||||||
background: linear-gradient(to right, transparent 0%, #F9FAFB, #F9FAFB, #F9FAFB);
|
border-radius: 50%;
|
||||||
display: flex;
|
position: absolute;
|
||||||
align-items: center;
|
top: 0;
|
||||||
justify-content: flex-end;
|
left: 0;
|
||||||
width: 120px;
|
|
||||||
padding-right: 16px;
|
|
||||||
|
|
||||||
.confirm{
|
|
||||||
color: $green-2;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.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 {
|
&.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 {
|
.filter-menu-header {
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 7px 15px 15px;
|
|
||||||
width: 350px;
|
|
||||||
|
|
||||||
.actions {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
justify-content: space-between;
|
||||||
}
|
padding: 7px 15px 15px;
|
||||||
|
width: 350px;
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { ManualRedactionEntry, ReanalysisControllerService } from '@redaction/red-ui-http';
|
import { ManualRedactionEntry, ReanalysisControllerService } from '@redaction/red-ui-http';
|
||||||
import { AppStateService } from '../../../state/app-state.service';
|
import { AppStateService } from '../../../state/app-state.service';
|
||||||
@ -17,393 +25,440 @@ import { DialogService } from '../../../dialogs/dialog.service';
|
|||||||
import { MatDialogRef, MatDialogState } from '@angular/material/dialog';
|
import { MatDialogRef, MatDialogState } from '@angular/material/dialog';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'redaction-file-preview-screen',
|
selector: 'redaction-file-preview-screen',
|
||||||
templateUrl: './file-preview-screen.component.html',
|
templateUrl: './file-preview-screen.component.html',
|
||||||
styleUrls: ['./file-preview-screen.component.scss']
|
styleUrls: ['./file-preview-screen.component.scss']
|
||||||
})
|
})
|
||||||
export class FilePreviewScreenComponent implements OnInit {
|
export class FilePreviewScreenComponent implements OnInit {
|
||||||
|
private projectId: string;
|
||||||
|
private _activeViewer: 'ANNOTATED' | 'REDACTED' = 'ANNOTATED';
|
||||||
|
private instance: WebViewerInstance;
|
||||||
|
private _dialogRef: MatDialogRef<any>;
|
||||||
|
|
||||||
private projectId: string;
|
@ViewChild(PdfViewerComponent) private _viewerComponent: PdfViewerComponent;
|
||||||
private _activeViewer: 'ANNOTATED' | 'REDACTED' = 'ANNOTATED';
|
@ViewChild('annotations') private _annotationsElement: ElementRef;
|
||||||
private instance: WebViewerInstance;
|
@ViewChild('quickNavigation') private _quickNavigationElement: ElementRef;
|
||||||
private _dialogRef: MatDialogRef<any>;
|
|
||||||
|
|
||||||
@ViewChild(PdfViewerComponent) private _viewerComponent: PdfViewerComponent;
|
public annotatedFileData: Blob;
|
||||||
@ViewChild('annotations') private _annotationsElement: ElementRef;
|
public redactedFileData: Blob;
|
||||||
@ViewChild('quickNavigation') private _quickNavigationElement: ElementRef;
|
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;
|
constructor(
|
||||||
public redactedFileData: Blob;
|
public readonly appStateService: AppStateService,
|
||||||
public fileId: string;
|
private readonly _changeDetectorRef: ChangeDetectorRef,
|
||||||
public annotations: Annotations.Annotation[] = [];
|
private readonly _activatedRoute: ActivatedRoute,
|
||||||
public displayedAnnotations: { [key: number]: { annotations: Annotations.Annotation[] } } = {};
|
private readonly _dialogService: DialogService,
|
||||||
public selectedAnnotation: Annotations.Annotation;
|
private readonly _router: Router,
|
||||||
public filters: AnnotationFilters;
|
private readonly _userService: UserService,
|
||||||
public expandedFilters: AnnotationFilters = { hint: false };
|
private readonly _fileDownloadService: FileDownloadService,
|
||||||
public pagesPanelActive = true;
|
private readonly _reanalysisControllerService: ReanalysisControllerService,
|
||||||
|
private readonly _filtersService: FiltersService,
|
||||||
constructor(
|
private ngZone: NgZone
|
||||||
public readonly appStateService: AppStateService,
|
) {
|
||||||
private readonly _changeDetectorRef: ChangeDetectorRef,
|
this._activatedRoute.params.subscribe((params) => {
|
||||||
private readonly _activatedRoute: ActivatedRoute,
|
this.projectId = params.projectId;
|
||||||
private readonly _dialogService: DialogService,
|
this.fileId = params.fileId;
|
||||||
private readonly _router: Router,
|
this.appStateService.activateFile(this.projectId, this.fileId);
|
||||||
private readonly _userService: UserService,
|
});
|
||||||
private readonly _fileDownloadService: FileDownloadService,
|
this.filters = _filtersService.filters;
|
||||||
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]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Object.keys(this.filters);
|
public get user() {
|
||||||
}
|
return this._userService.user;
|
||||||
|
}
|
||||||
|
|
||||||
public get redactedView() {
|
public filterKeys(key?: string) {
|
||||||
return this._activeViewer === 'REDACTED';
|
if (key) {
|
||||||
}
|
return Object.keys(this.filters[key]);
|
||||||
|
}
|
||||||
|
|
||||||
public set redactedView(value: boolean) {
|
return Object.keys(this.filters);
|
||||||
this._activeViewer = value ? 'REDACTED' : 'ANNOTATED';
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public ngOnInit(): void {
|
public get redactedView() {
|
||||||
// PDFTRON cache fix
|
return this._activeViewer === 'REDACTED';
|
||||||
localStorage.clear();
|
}
|
||||||
this._reloadFiles();
|
|
||||||
this.appStateService.fileStatusChanged.subscribe((fileStatus) => {
|
public set redactedView(value: boolean) {
|
||||||
if (fileStatus.fileId === this.fileId) {
|
this._activeViewer = value ? 'REDACTED' : 'ANNOTATED';
|
||||||
console.log(fileStatus);
|
}
|
||||||
|
|
||||||
|
public ngOnInit(): void {
|
||||||
|
// PDFTRON cache fix
|
||||||
|
localStorage.clear();
|
||||||
this._reloadFiles();
|
this._reloadFiles();
|
||||||
}
|
this.appStateService.fileStatusChanged.subscribe((fileStatus) => {
|
||||||
});
|
if (fileStatus.fileId === this.fileId) {
|
||||||
}
|
console.log(fileStatus);
|
||||||
|
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;
|
|
||||||
}
|
}
|
||||||
const elements: any[] = this._annotationsElement.nativeElement.querySelectorAll(`div[annotation-id="${this.selectedAnnotation.Id}"].active`);
|
|
||||||
this._scrollToFirstElement(elements);
|
|
||||||
}
|
|
||||||
|
|
||||||
public selectPage(pageNumber: number) {
|
private _reloadFiles() {
|
||||||
this._viewerComponent.navigateToPage(pageNumber);
|
this._fileDownloadService
|
||||||
}
|
.loadFile('ANNOTATED', this.fileId, (data) => {
|
||||||
|
this.annotatedFileData = data;
|
||||||
public openManualRedactionDialog($event: ManualRedactionEntry) {
|
})
|
||||||
this.ngZone.run(() => {
|
.subscribe(() => {});
|
||||||
this._dialogRef = this._dialogService.openManualRedactionDialog($event, () => {
|
this._fileDownloadService
|
||||||
});
|
.loadFile('REDACTED', this.fileId, (data) => {
|
||||||
});
|
this.redactedFileData = data;
|
||||||
|
})
|
||||||
}
|
.subscribe(() => {});
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
const elements: any[] = this._annotationsElement.nativeElement.querySelectorAll(`div[anotation-page-header="${this.activeViewerPage}"]`);
|
|
||||||
this._scrollToFirstElement(elements);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _scrollToFirstElement(elements: HTMLElement[]) {
|
public openFileDetailsDialog($event: MouseEvent) {
|
||||||
if (elements.length > 0) {
|
this._dialogRef = this._dialogService.openFileDetailsDialog(
|
||||||
scrollIntoView(elements[0], {
|
$event,
|
||||||
behavior: 'smooth',
|
this.appStateService.activeFile
|
||||||
scrollMode: 'if-needed',
|
);
|
||||||
block: 'start',
|
|
||||||
inline: 'start'
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public getType(annotation: Annotations.Annotation): string {
|
public reanalyseFile($event: MouseEvent) {
|
||||||
return AnnotationUtils.getType(annotation);
|
$event.stopPropagation();
|
||||||
}
|
this._reanalysisControllerService
|
||||||
|
.reanalyzeFile(this.appStateService.activeProject.project.projectId, this.fileId)
|
||||||
|
.subscribe(async () => {
|
||||||
|
await this.appStateService.reloadActiveProjectFiles();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public getDictionary(annotation: Annotations.Annotation): string {
|
public openDeleteFileDialog($event: MouseEvent) {
|
||||||
return AnnotationUtils.getDictionary(annotation);
|
this._dialogRef = this._dialogService.openDeleteFileDialog(
|
||||||
}
|
$event,
|
||||||
|
this.projectId,
|
||||||
|
this.fileId,
|
||||||
|
() => {
|
||||||
|
this._router.navigate([`/ui/projects/${this.projectId}`]);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public acceptSuggestionAnnotation($event: MouseEvent, annotation: Annotations.Annotation) {
|
public openAssignFileOwnerDialog($event: MouseEvent) {
|
||||||
this.ngZone.run(() => {
|
const file = this.appStateService.getFileById(this.projectId, this.fileId);
|
||||||
this._dialogRef = this._dialogService.acceptSuggestionAnnotation($event, annotation, this.projectId, this.fileId);
|
this._dialogRef = this._dialogService.openAssignFileOwnerDialog($event, file);
|
||||||
});
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public suggestRemoveAnnotation($event: MouseEvent, annotation: Annotations.Annotation) {
|
public get activeViewer() {
|
||||||
this.ngZone.run(() => {
|
return this.instance;
|
||||||
this._dialogRef = this._dialogService.suggestRemoveAnnotation($event, annotation, this.projectId, this.fileId);
|
}
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public downloadFile(type: FileType | string) {
|
public applyFilters() {
|
||||||
this._fileDownloadService.loadFile(type, this.fileId).subscribe(data => {
|
this.displayedAnnotations = AnnotationUtils.parseAnnotations(
|
||||||
saveAs(data, this.appStateService.activeFile.filename);
|
this.annotations,
|
||||||
});
|
this.filters
|
||||||
}
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public setAllFilters(filter: AnnotationFilters, value: boolean, rootKey?: string) {
|
public get displayedPages(): number[] {
|
||||||
if (rootKey) {
|
return Object.keys(this.displayedAnnotations).map((key) => Number(key));
|
||||||
this.filters[rootKey] = value;
|
}
|
||||||
} else {
|
|
||||||
|
|
||||||
for (const key of Object.keys(filter)) {
|
public handleAnnotationSelected(annotation: Annotations.Annotation) {
|
||||||
if (AnnotationUtils.hasSubsections(filter[key])) {
|
this.selectedAnnotation = annotation;
|
||||||
this.setAllFilters(filter[key], value);
|
this.scrollToSelectedAnnotation();
|
||||||
} else {
|
this._changeDetectorRef.detectChanges();
|
||||||
filter[key] = value;
|
}
|
||||||
|
|
||||||
|
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 selectPage(pageNumber: number) {
|
||||||
}
|
this._viewerComponent.navigateToPage(pageNumber);
|
||||||
|
|
||||||
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') {
|
public openManualRedactionDialog($event: ManualRedactionEntry) {
|
||||||
this.pagesPanelActive = !this.pagesPanelActive;
|
this.ngZone.run(() => {
|
||||||
this._changeDetectorRef.detectChanges();
|
this._dialogRef = this._dialogService.openManualRedactionDialog($event, () => {});
|
||||||
return;
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.pagesPanelActive) {
|
get activeViewerPage() {
|
||||||
this._navigateAnnotations($event);
|
return this.instance.docViewer.getCurrentPage();
|
||||||
} else {
|
|
||||||
this._navigatePages($event);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private _navigateAnnotations($event: KeyboardEvent) {
|
@debounce()
|
||||||
if (!this.selectedAnnotation || this.activeViewerPage !== this.selectedAnnotation.getPageNumber()) {
|
private _scrollViews() {
|
||||||
const pageIdx = this.displayedPages.indexOf(this.activeViewerPage);
|
this._scrollQuickNavigation();
|
||||||
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();
|
this._scrollAnnotations();
|
||||||
break;
|
|
||||||
}
|
|
||||||
--idx;
|
|
||||||
}
|
}
|
||||||
return idx >= 0 ? this.displayedPages[idx] : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
viewerPageChanged($event: number) {
|
private _scrollQuickNavigation() {
|
||||||
this._scrollViews();
|
const elements: any[] = this._quickNavigationElement.nativeElement.querySelectorAll(
|
||||||
this._changeDetectorRef.detectChanges();
|
`#quick-nav-page-${this.activeViewerPage}`
|
||||||
}
|
);
|
||||||
|
this._scrollToFirstElement(elements);
|
||||||
|
}
|
||||||
|
|
||||||
viewerReady($event: WebViewerInstance) {
|
private _scrollAnnotations() {
|
||||||
this.instance = $event;
|
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
|
public getType(annotation: Annotations.Annotation): string {
|
||||||
annotations.forEach(a => {
|
return AnnotationUtils.getType(annotation);
|
||||||
a['comments'] = a['Mi'] ? a['Mi'].map(m => {
|
}
|
||||||
return { value: m.eC };
|
|
||||||
}) : [];
|
|
||||||
});
|
|
||||||
|
|
||||||
AnnotationUtils.addAnnotations(this.annotations, annotations);
|
public getDictionary(annotation: Annotations.Annotation): string {
|
||||||
this.applyFilters();
|
return AnnotationUtils.getDictionary(annotation);
|
||||||
this._changeDetectorRef.detectChanges();
|
}
|
||||||
}
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,252 +1,249 @@
|
|||||||
import {
|
import {
|
||||||
AfterViewInit,
|
AfterViewInit,
|
||||||
Component,
|
Component,
|
||||||
ElementRef,
|
ElementRef,
|
||||||
EventEmitter,
|
EventEmitter,
|
||||||
Input,
|
Input,
|
||||||
OnChanges,
|
OnChanges,
|
||||||
OnInit,
|
OnInit,
|
||||||
Output,
|
Output,
|
||||||
SimpleChanges,
|
SimpleChanges,
|
||||||
ViewChild
|
ViewChild
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import {AppConfigKey, AppConfigService} from '../../../app-config/app-config.service';
|
import { AppConfigKey, AppConfigService } from '../../../app-config/app-config.service';
|
||||||
import {FileStatus, ManualRedactionEntry, Rectangle} from '@redaction/red-ui-http';
|
import { FileStatus, ManualRedactionEntry, Rectangle } from '@redaction/red-ui-http';
|
||||||
import WebViewer, {Annotations, WebViewerInstance} from '@pdftron/webviewer';
|
import WebViewer, { Annotations, WebViewerInstance } from '@pdftron/webviewer';
|
||||||
import {TranslateService} from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
import {FileDownloadService} from '../service/file-download.service';
|
import { FileDownloadService } from '../service/file-download.service';
|
||||||
|
|
||||||
export interface ViewerState {
|
export interface ViewerState {
|
||||||
|
displayMode?: any;
|
||||||
displayMode?: any;
|
layoutMode?: any;
|
||||||
layoutMode?: any;
|
pageNumber?: any;
|
||||||
pageNumber?: any;
|
scrollTop?: any;
|
||||||
scrollTop?: any;
|
scrollLeft?: any;
|
||||||
scrollLeft?: any;
|
zoom?: any;
|
||||||
zoom?: any;
|
leftPanelState?: any;
|
||||||
leftPanelState?: any;
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'redaction-pdf-viewer',
|
selector: 'redaction-pdf-viewer',
|
||||||
templateUrl: './pdf-viewer.component.html',
|
templateUrl: './pdf-viewer.component.html',
|
||||||
styleUrls: ['./pdf-viewer.component.scss']
|
styleUrls: ['./pdf-viewer.component.scss']
|
||||||
})
|
})
|
||||||
export class PdfViewerComponent implements OnInit, AfterViewInit, OnChanges {
|
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;
|
@Output() fileReady = new EventEmitter();
|
||||||
@Input() fileStatus: FileStatus;
|
@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();
|
@Input() flag = false;
|
||||||
@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>();
|
@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;
|
ngOnInit() {
|
||||||
instance: WebViewerInstance;
|
this._restoreViewerState = this._restoreViewerState.bind(this);
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
ngAfterViewInit(): void {
|
ngOnChanges(changes: SimpleChanges): void {
|
||||||
this._loadViewer(this.fileData);
|
if (changes.fileData && !changes.fileData.firstChange) {
|
||||||
}
|
this._changeDocument();
|
||||||
|
|
||||||
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) => {
|
ngAfterViewInit(): void {
|
||||||
if (action === 'deselected') {
|
this._loadViewer(this.fileData);
|
||||||
this.annotationSelected.emit(null);
|
}
|
||||||
|
|
||||||
|
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 {
|
} 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');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,151 +1,188 @@
|
|||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<div class="filters flex-row">
|
<div class="filters flex-row">
|
||||||
<div translate="filters.filter-by.label"></div>
|
<div translate="filters.filter-by.label"></div>
|
||||||
<button mat-button translate="filters.status.label">
|
<button mat-button translate="filters.status.label">
|
||||||
<mat-icon svgIcon="red:status"></mat-icon>
|
<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>
|
||||||
<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>
|
||||||
|
|
||||||
<div class="flex red-content-inner">
|
<div class="flex red-content-inner">
|
||||||
<div class="left-container">
|
<div class="left-container">
|
||||||
<div class="table-header">
|
<div class="table-header">
|
||||||
<span class="all-caps-label">
|
<span class="all-caps-label">
|
||||||
{{'project-listing.table-header.title.label'| translate:{ length: appStateService.allProjects?.length || 0 } }}
|
{{
|
||||||
</span>
|
'project-listing.table-header.title.label'
|
||||||
<div class="actions">
|
| translate: { length: appStateService.allProjects?.length || 0 }
|
||||||
<div translate="project-listing.table-header.bulk-select.label"></div>
|
}}
|
||||||
<mat-form-field appearance="none" class="red-select">
|
</span>
|
||||||
<mat-select [(ngModel)]="sortingOption" panelClass="red-select-panel">
|
<div class="actions">
|
||||||
<mat-option *ngFor="let option of sortingOptions" [value]="option">
|
<div translate="project-listing.table-header.bulk-select.label"></div>
|
||||||
{{option.label | translate }}
|
<mat-form-field appearance="none" class="red-select">
|
||||||
</mat-option>
|
<mat-select [(ngModel)]="sortingOption" panelClass="red-select-panel">
|
||||||
</mat-select>
|
<mat-option *ngFor="let option of sortingOptions" [value]="option">
|
||||||
</mat-form-field>
|
{{ option.label | translate }}
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
<div class="grid-container">
|
<div class="right-fixed-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>
|
||||||
<div class="table-item-title table-item-title--large">
|
<redaction-simple-doughnut-chart
|
||||||
{{pw.project.projectName}}
|
[config]="projectsChartData"
|
||||||
</div>
|
[strokeWidth]="15"
|
||||||
<div class="small-label stats-subtitle">
|
[subtitle]="'project-listing.stats.charts.projects.label'"
|
||||||
<div>
|
></redaction-simple-doughnut-chart>
|
||||||
<mat-icon svgIcon="red:document"></mat-icon>
|
|
||||||
{{documentCount(pw)}}</div>
|
<div class="project-stats-container">
|
||||||
<div>
|
<div class="project-stats-item">
|
||||||
<mat-icon svgIcon="red:user"></mat-icon>
|
<mat-icon svgIcon="red:document"></mat-icon>
|
||||||
{{userCount(pw)}}</div>
|
<div>
|
||||||
<div>
|
<div class="heading">{{ totalPages }}</div>
|
||||||
<mat-icon svgIcon="red:calendar"></mat-icon>
|
<div translate="project-listing.stats.analyzed-pages.label"></div>
|
||||||
{{pw.project.date | date:'mediumDate'}}
|
</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 *ngIf="pw.project.dueDate">
|
|
||||||
<mat-icon svgIcon="red:lightning"></mat-icon>
|
|
||||||
{{pw.project.dueDate | date:'mediumDate'}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<redaction-initials-avatar [userId]="pw.project.ownerId"
|
<redaction-simple-doughnut-chart
|
||||||
withName="true"
|
[config]="documentsChartData"
|
||||||
></redaction-initials-avatar>
|
[strokeWidth]="15"
|
||||||
|
[subtitle]="'project-listing.stats.charts.total-documents.label'"
|
||||||
|
></redaction-simple-doughnut-chart>
|
||||||
</div>
|
</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>
|
|
||||||
|
|
||||||
<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>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -1,58 +1,58 @@
|
|||||||
@import "../../../assets/styles/red-mixins";
|
@import '../../../assets/styles/red-mixins';
|
||||||
|
|
||||||
.add-project-btn {
|
.add-project-btn {
|
||||||
mat-icon {
|
mat-icon {
|
||||||
width: 14px;
|
width: 14px;
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
color: $white;
|
color: $white;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.left-container {
|
.left-container {
|
||||||
width: calc(100vw - #{$right-container-width} - 130px);
|
width: calc(100vw - #{$right-container-width} - 130px);
|
||||||
|
|
||||||
.grid-container {
|
.grid-container {
|
||||||
grid-template-columns: 2fr 1fr auto;
|
grid-template-columns: 2fr 1fr auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stats-subtitle {
|
.stats-subtitle {
|
||||||
margin-top: 6px;
|
margin-top: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-container {
|
.status-container {
|
||||||
width: 160px;
|
width: 160px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.right-fixed-container {
|
.right-fixed-container {
|
||||||
display: flex;
|
|
||||||
width: 470px;
|
|
||||||
padding-top: 50px;
|
|
||||||
|
|
||||||
> div {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
width: 470px;
|
||||||
align-items: center;
|
padding-top: 50px;
|
||||||
}
|
|
||||||
|
|
||||||
.project-stats-container {
|
> div {
|
||||||
width: fit-content;
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
.project-stats-item {
|
flex-direction: column;
|
||||||
display: flex;
|
align-items: center;
|
||||||
width: fit-content;
|
}
|
||||||
gap: 5px;
|
|
||||||
margin-top: 25px;
|
.project-stats-container {
|
||||||
|
width: fit-content;
|
||||||
&:first-of-type {
|
|
||||||
margin-top: 50px;
|
.project-stats-item {
|
||||||
}
|
display: flex;
|
||||||
|
width: fit-content;
|
||||||
mat-icon {
|
gap: 5px;
|
||||||
height: 16px;
|
margin-top: 25px;
|
||||||
margin-top: 2px;
|
|
||||||
}
|
&:first-of-type {
|
||||||
|
margin-top: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
mat-icon {
|
||||||
|
height: 16px;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,112 +1,120 @@
|
|||||||
import {Component, OnInit} from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
import {Project} from '@redaction/red-ui-http';
|
import { Project } from '@redaction/red-ui-http';
|
||||||
import {AppStateService, ProjectWrapper} from '../../state/app-state.service';
|
import { AppStateService, ProjectWrapper } from '../../state/app-state.service';
|
||||||
import {UserService} from '../../user/user.service';
|
import { UserService } from '../../user/user.service';
|
||||||
import {DoughnutChartConfig} from '../../components/simple-doughnut-chart/simple-doughnut-chart.component';
|
import { DoughnutChartConfig } from '../../components/simple-doughnut-chart/simple-doughnut-chart.component';
|
||||||
import {SortingOption} from '../../utils/types';
|
import { SortingOption } from '../../utils/types';
|
||||||
import {groupBy} from '../../utils/functions';
|
import { groupBy } from '../../utils/functions';
|
||||||
import {DialogService} from '../../dialogs/dialog.service';
|
import { DialogService } from '../../dialogs/dialog.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'redaction-project-listing-screen',
|
selector: 'redaction-project-listing-screen',
|
||||||
templateUrl: './project-listing-screen.component.html',
|
templateUrl: './project-listing-screen.component.html',
|
||||||
styleUrls: ['./project-listing-screen.component.scss']
|
styleUrls: ['./project-listing-screen.component.scss']
|
||||||
})
|
})
|
||||||
export class ProjectListingScreenComponent implements OnInit {
|
export class ProjectListingScreenComponent implements OnInit {
|
||||||
public projectsChartData: DoughnutChartConfig [] = [];
|
public projectsChartData: DoughnutChartConfig[] = [];
|
||||||
public documentsChartData: DoughnutChartConfig [] = [];
|
public documentsChartData: DoughnutChartConfig[] = [];
|
||||||
public sortingOptions: SortingOption[] = [
|
public sortingOptions: SortingOption[] = [
|
||||||
{label: 'project-listing.sorting.recent.label', order: 'desc', column: 'projectDate'},
|
{ label: 'project-listing.sorting.recent.label', order: 'desc', column: 'projectDate' },
|
||||||
{label: 'project-listing.sorting.alphabetically.label', order: 'asc', column: 'project.projectName'}
|
{
|
||||||
];
|
label: 'project-listing.sorting.alphabetically.label',
|
||||||
public sortingOption: SortingOption = this.sortingOptions[0];
|
order: 'asc',
|
||||||
|
column: 'project.projectName'
|
||||||
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'}
|
|
||||||
];
|
];
|
||||||
const groups = groupBy(this.appStateService.aggregatedFiles, 'status');
|
public sortingOption: SortingOption = this.sortingOptions[0];
|
||||||
this.documentsChartData = [];
|
|
||||||
for (const key of Object.keys(groups)) {
|
constructor(
|
||||||
this.documentsChartData.push({value: groups[key].length, color: key, label: key});
|
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() {
|
private _calculateData() {
|
||||||
return this._userService.user;
|
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() {
|
public get user() {
|
||||||
return this.appStateService.totalAnalysedPages;
|
return this._userService.user;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get totalPeople() {
|
public get totalPages() {
|
||||||
return this.appStateService.totalPeople;
|
return this.appStateService.totalAnalysedPages;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get activeProjects() {
|
public get totalPeople() {
|
||||||
return this.appStateService.allProjects.reduce((i, p) => i + (p.project.status === Project.StatusEnum.ACTIVE ? 1 : 0), 0);
|
return this.appStateService.totalPeople;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get inactiveProjects() {
|
public get activeProjects() {
|
||||||
return this.appStateService.allProjects.length - this.activeProjects;
|
return this.appStateService.allProjects.reduce(
|
||||||
}
|
(i, p) => i + (p.project.status === Project.StatusEnum.ACTIVE ? 1 : 0),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public documentCount(project: ProjectWrapper) {
|
public get inactiveProjects() {
|
||||||
return project.files.length;
|
return this.appStateService.allProjects.length - this.activeProjects;
|
||||||
}
|
}
|
||||||
|
|
||||||
public userCount(project: ProjectWrapper) {
|
public documentCount(project: ProjectWrapper) {
|
||||||
return 1;
|
return project.files.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
public openAddProjectDialog(): void {
|
public userCount(project: ProjectWrapper) {
|
||||||
this._dialogService.openAddProjectDialog(() => {
|
return 1;
|
||||||
this._calculateData();
|
}
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public openDeleteProjectDialog($event: MouseEvent, project: Project) {
|
public openAddProjectDialog(): void {
|
||||||
this._dialogService.openDeleteProjectDialog($event, project, () => {
|
this._dialogService.openAddProjectDialog(() => {
|
||||||
this._calculateData();
|
this._calculateData();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public openProjectDetailsDialog($event: MouseEvent, project: ProjectWrapper) {
|
public openDeleteProjectDialog($event: MouseEvent, project: Project) {
|
||||||
this._dialogService.openProjectDetailsDialog($event, project);
|
this._dialogService.openDeleteProjectDialog($event, project, () => {
|
||||||
}
|
this._calculateData();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public openAssignProjectOwnerDialog($event: MouseEvent, project: Project) {
|
public openProjectDetailsDialog($event: MouseEvent, project: ProjectWrapper) {
|
||||||
this._dialogService.openAssignProjectMembersAndOwnerDialog($event, project);
|
this._dialogService.openProjectDetailsDialog($event, project);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getProjectStatusConfig(pw: ProjectWrapper) {
|
public openAssignProjectOwnerDialog($event: MouseEvent, project: Project) {
|
||||||
const obj = pw.files.reduce((acc, file) => {
|
this._dialogService.openAssignProjectMembersAndOwnerDialog($event, project);
|
||||||
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}));
|
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 }));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,231 +1,317 @@
|
|||||||
<div *ngIf="!appStateService.activeProject"
|
<div
|
||||||
[innerHTML]="'project-overview.no-project.label' | translate:{projectId: activeProject.projectId}"
|
*ngIf="!appStateService.activeProject"
|
||||||
class="heading-l"></div>
|
[innerHTML]="
|
||||||
|
'project-overview.no-project.label' | translate: { projectId: activeProject.projectId }
|
||||||
|
"
|
||||||
|
class="heading-l"
|
||||||
|
></div>
|
||||||
|
|
||||||
<div *ngIf="appStateService.activeProject" class="page-header">
|
<div *ngIf="appStateService.activeProject" class="page-header">
|
||||||
<div class="filters flex-row">
|
<div class="filters flex-row">
|
||||||
<div translate="filters.filter-by.label"></div>
|
<div translate="filters.filter-by.label"></div>
|
||||||
<button mat-button translate="filters.status.label">
|
<button mat-button translate="filters.status.label">
|
||||||
<mat-icon svgIcon="red:status"></mat-icon>
|
<mat-icon svgIcon="red:status"></mat-icon>
|
||||||
</button>
|
</button>
|
||||||
<button mat-button translate="filters.people.label">
|
<button mat-button translate="filters.people.label">
|
||||||
<mat-icon svgIcon="red:user"></mat-icon>
|
<mat-icon svgIcon="red:user"></mat-icon>
|
||||||
</button>
|
</button>
|
||||||
<button mat-button translate="filters.due-date.label">
|
<button mat-button translate="filters.due-date.label">
|
||||||
<mat-icon svgIcon="red:lightning"></mat-icon>
|
<mat-icon svgIcon="red:lightning"></mat-icon>
|
||||||
</button>
|
</button>
|
||||||
<button mat-button translate="filters.created-on.label">
|
<button mat-button translate="filters.created-on.label">
|
||||||
<mat-icon svgIcon="red:calendar"></mat-icon>
|
<mat-icon svgIcon="red:calendar"></mat-icon>
|
||||||
</button>
|
</button>
|
||||||
<button mat-button translate="filters.project.label">
|
<button mat-button translate="filters.project.label">
|
||||||
<mat-icon svgIcon="red:folder"></mat-icon>
|
<mat-icon svgIcon="red:folder"></mat-icon>
|
||||||
</button>
|
</button>
|
||||||
<button mat-button translate="filters.document.label">
|
<button mat-button translate="filters.document.label">
|
||||||
<mat-icon svgIcon="red:document"></mat-icon>
|
<mat-icon svgIcon="red:document"></mat-icon>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button (click)="fileInput.click()" color="primary" mat-flat-button
|
<button
|
||||||
translate="project-overview.upload-document.label"></button>
|
(click)="fileInput.click()"
|
||||||
<input #fileInput (change)="uploadFiles($event.target.files)" class="file-upload-input" multiple="true"
|
color="primary"
|
||||||
type="file">
|
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>
|
||||||
|
|
||||||
<div class="flex red-content-inner">
|
<div class="flex red-content-inner">
|
||||||
<div class="left-container">
|
<div class="left-container">
|
||||||
<div class="table-header">
|
<div class="table-header">
|
||||||
<div class="select-all-container">
|
<div class="select-all-container">
|
||||||
<div class="select-oval" [class.active]="areAllFilesSelected()" (click)="toggleSelectAll()"></div>
|
<div
|
||||||
<span class="all-caps-label">
|
class="select-oval"
|
||||||
{{'project-overview.table-header.title.label'| translate:{ length: appStateService.activeProject?.files.length || 0 } }}
|
[class.active]="areAllFilesSelected()"
|
||||||
</span>
|
(click)="toggleSelectAll()"
|
||||||
</div>
|
></div>
|
||||||
<div class="actions">
|
<span class="all-caps-label">
|
||||||
<div translate="project-overview.table-header.bulk-select.label"></div>
|
{{
|
||||||
<mat-form-field appearance="none" class="red-select">
|
'project-overview.table-header.title.label'
|
||||||
<mat-select [(ngModel)]="sortingOption" panelClass="red-select-panel">
|
| translate
|
||||||
<mat-option *ngFor="let option of sortingOptions" [value]="option">
|
: { length: appStateService.activeProject?.files.length || 0 }
|
||||||
{{option.label | translate }}
|
}}
|
||||||
</mat-option>
|
</span>
|
||||||
</mat-select>
|
</div>
|
||||||
</mat-form-field>
|
<div class="actions">
|
||||||
</div>
|
<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>
|
||||||
|
|
||||||
<div class="grid-container">
|
<div class="project-details-container right-fixed-container">
|
||||||
<!-- Table column names-->
|
<div class="actions-row">
|
||||||
<div class="table-col-name"></div>
|
<button mat-icon-button (click)="openDeleteProjectDialog($event)">
|
||||||
|
<mat-icon svgIcon="red:trash"></mat-icon>
|
||||||
<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>
|
||||||
<button (click)="$event.stopPropagation()" color="accent" mat-icon-button
|
<button mat-icon-button (click)="openEditProjectDialog($event)">
|
||||||
[matTooltip]="'project-overview.report.action.label'|translate">
|
<mat-icon svgIcon="red:edit"></mat-icon>
|
||||||
<mat-icon svgIcon="red:report"></mat-icon>
|
|
||||||
</button>
|
</button>
|
||||||
<button (click)="openAssignFileOwnerDialog($event,fileStatus)" color="accent" mat-icon-button
|
<button mat-icon-button (click)="openDetailsDialog($event)">
|
||||||
[matTooltip]="'project-overview.assign.action.label'|translate">
|
<mat-icon svgIcon="red:report"></mat-icon>
|
||||||
<mat-icon svgIcon="red:assign"></mat-icon>
|
|
||||||
</button>
|
</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 class="small-label stats-subtitle mt-20">
|
||||||
</div>
|
<div>
|
||||||
</div>
|
<mat-icon svgIcon="red:pages"></mat-icon>
|
||||||
|
{{ appStateService.activeProject.files.length }}
|
||||||
<div class="project-details-container right-fixed-container">
|
</div>
|
||||||
<div class="actions-row">
|
<div>
|
||||||
<button mat-icon-button (click)="openDeleteProjectDialog($event)">
|
<mat-icon svgIcon="red:user"></mat-icon>
|
||||||
<mat-icon svgIcon="red:trash"></mat-icon>
|
{{ appStateService.activeProject.project.memberIds.length }}
|
||||||
</button>
|
</div>
|
||||||
<button mat-icon-button (click)="openEditProjectDialog($event)">
|
<div>
|
||||||
<mat-icon svgIcon="red:edit"></mat-icon>
|
<mat-icon svgIcon="red:calendar"></mat-icon>
|
||||||
</button>
|
{{ appStateService.activeProject.project.date | date: 'd MMM. yyyy' }}
|
||||||
<button mat-icon-button (click)="openDetailsDialog($event)">
|
</div>
|
||||||
<mat-icon svgIcon="red:report"></mat-icon>
|
<div *ngIf="appStateService.activeProject.project.dueDate">
|
||||||
</button>
|
<mat-icon svgIcon="red:lightning"></mat-icon>
|
||||||
</div>
|
{{ appStateService.activeProject.project.dueDate | date: 'mediumDate' }}
|
||||||
|
</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>
|
</div>
|
||||||
|
|
||||||
</div>
|
<div class="heading-xl mt-20">
|
||||||
</div>
|
{{ appStateService.activeProject.project.projectName }}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="mt-32">
|
<div class="owner flex-row mt-20">
|
||||||
<redaction-simple-doughnut-chart [config]="documentsChartData"
|
<redaction-initials-avatar
|
||||||
[strokeWidth]="15"
|
[userId]="activeProject.ownerId"
|
||||||
[radius]="70"
|
size="large"
|
||||||
[subtitle]="'project-overview.project-details.charts.total-documents.label'"
|
withName="true"
|
||||||
direction="row"
|
></redaction-initials-avatar>
|
||||||
></redaction-simple-doughnut-chart>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-32 legend">
|
<div class="mt-20">
|
||||||
<div>
|
{{ appStateService.activeProject.project.description }}
|
||||||
<redaction-annotation-icon type="hint"></redaction-annotation-icon>
|
</div>
|
||||||
{{ 'project-overview.legend.contains-hints.label' | translate }}
|
|
||||||
</div>
|
<div class="project-team mt-20">
|
||||||
<div>
|
<div
|
||||||
<redaction-annotation-icon type="redaction"></redaction-annotation-icon>
|
class="all-caps-label"
|
||||||
{{ 'project-overview.legend.contains-redactions.label' | translate }}
|
translate="project-overview.project-details.project-team.label"
|
||||||
</div>
|
></div>
|
||||||
<div>
|
<div class="flex mt-20 members-container">
|
||||||
<redaction-annotation-icon type="suggestion"></redaction-annotation-icon>
|
<div *ngFor="let userId of displayMembers" class="member">
|
||||||
{{ 'project-overview.legend.contains-suggestions.label' | translate }}
|
<redaction-initials-avatar
|
||||||
</div>
|
[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>
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,76 +1,76 @@
|
|||||||
@import "../../../assets/styles/red-variables";
|
@import '../../../assets/styles/red-variables';
|
||||||
|
|
||||||
.file-upload-input {
|
.file-upload-input {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.select-all-container {
|
.select-all-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
.select-oval {
|
.select-oval {
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.pr-0 {
|
.pr-0 {
|
||||||
padding-right: 0 !important;
|
padding-right: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.select-oval {
|
.select-oval {
|
||||||
width: 20px;
|
width: 20px;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
border: 1px solid $grey-5;
|
border: 1px solid $grey-5;
|
||||||
background-color: $white;
|
background-color: $white;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
background-color: $primary;
|
background-color: $primary;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.placeholder {
|
&.placeholder {
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.grid-container {
|
.grid-container {
|
||||||
grid-template-columns: auto 3fr 2fr 1fr 2fr auto;
|
grid-template-columns: auto 3fr 2fr 1fr 2fr auto;
|
||||||
|
|
||||||
.table-item {
|
.table-item {
|
||||||
.table-item-title {
|
.table-item-title {
|
||||||
line-height: 80px;
|
line-height: 80px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.needs-work {
|
.needs-work {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.project-details-container {
|
.project-details-container {
|
||||||
.members-container {
|
.members-container {
|
||||||
gap: 5px;
|
gap: 5px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.legend {
|
.legend {
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 8px;
|
|
||||||
|
|
||||||
> div {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
align-items: center;
|
|
||||||
}
|
> div {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mt-32 {
|
.mt-32 {
|
||||||
margin-top: 32px;
|
margin-top: 32px;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,10 @@
|
|||||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
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 { NotificationService } from '../../notification/notification.service';
|
||||||
import { AppStateService } from '../../state/app-state.service';
|
import { AppStateService } from '../../state/app-state.service';
|
||||||
import { FileDropOverlayService } from '../../upload/file-drop/service/file-drop-overlay.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 { groupBy } from '../../utils/functions';
|
||||||
import { DialogService } from '../../dialogs/dialog.service';
|
import { DialogService } from '../../dialogs/dialog.service';
|
||||||
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'redaction-project-overview-screen',
|
selector: 'redaction-project-overview-screen',
|
||||||
templateUrl: './project-overview-screen.component.html',
|
templateUrl: './project-overview-screen.component.html',
|
||||||
styleUrls: ['./project-overview-screen.component.scss']
|
styleUrls: ['./project-overview-screen.component.scss']
|
||||||
})
|
})
|
||||||
export class ProjectOverviewScreenComponent implements OnInit, OnDestroy {
|
export class ProjectOverviewScreenComponent implements OnInit, OnDestroy {
|
||||||
private _selectedFileIds: string[] = [];
|
private _selectedFileIds: string[] = [];
|
||||||
|
|
||||||
public sortingOptions: SortingOption[] = [
|
public sortingOptions: SortingOption[] = [
|
||||||
{ label: 'project-overview.sorting.recent.label', order: 'desc', column: 'lastUpdated' },
|
{ label: 'project-overview.sorting.recent.label', order: 'desc', column: 'lastUpdated' },
|
||||||
{ label: 'project-overview.sorting.oldest.label', order: 'asc', 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.alphabetically.label',
|
||||||
{ label: 'project-overview.sorting.number-of-analyses.label', order: 'desc', column: 'numberOfAnalyses' }
|
order: 'asc',
|
||||||
];
|
column: 'filename'
|
||||||
public sortingOption: SortingOption = this.sortingOptions[0];
|
},
|
||||||
public documentsChartData: DoughnutChartConfig[] = [];
|
{
|
||||||
|
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,
|
constructor(
|
||||||
private readonly _activatedRoute: ActivatedRoute,
|
public readonly appStateService: AppStateService,
|
||||||
private readonly _statusControllerService: StatusControllerService,
|
private readonly _activatedRoute: ActivatedRoute,
|
||||||
private readonly _notificationService: NotificationService,
|
private readonly _statusControllerService: StatusControllerService,
|
||||||
private readonly _dialogService: DialogService,
|
private readonly _notificationService: NotificationService,
|
||||||
private readonly _fileUploadService: FileUploadService,
|
private readonly _dialogService: DialogService,
|
||||||
private readonly _uploadStatusOverlayService: UploadStatusOverlayService,
|
private readonly _fileUploadService: FileUploadService,
|
||||||
private readonly _reanalysisControllerService: ReanalysisControllerService,
|
private readonly _uploadStatusOverlayService: UploadStatusOverlayService,
|
||||||
private readonly _router: Router,
|
private readonly _reanalysisControllerService: ReanalysisControllerService,
|
||||||
private readonly _userService: UserService,
|
private readonly _router: Router,
|
||||||
private readonly _fileDropOverlayService: FileDropOverlayService) {
|
private readonly _userService: UserService,
|
||||||
this._activatedRoute.params.subscribe(params => {
|
private readonly _fileDropOverlayService: FileDropOverlayService
|
||||||
this.appStateService.activateProject(params.projectId);
|
) {
|
||||||
});
|
this._activatedRoute.params.subscribe((params) => {
|
||||||
|
this.appStateService.activateProject(params.projectId);
|
||||||
|
});
|
||||||
|
|
||||||
this.appStateService.fileStatusChanged.subscribe(() => {
|
this.appStateService.fileStatusChanged.subscribe(() => {
|
||||||
this._calculateChartConfig();
|
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._fileUploadService.uploadFiles(uploadFiles);
|
ngOnInit(): void {
|
||||||
this._uploadStatusOverlayService.openStatusOverlay();
|
this._fileDropOverlayService.initFileDropHandling();
|
||||||
}
|
this._calculateChartConfig();
|
||||||
|
}
|
||||||
|
|
||||||
public canOpenFile(fileStatus: FileStatus): boolean {
|
ngOnDestroy(): void {
|
||||||
// TODO check correct condition for this
|
this._fileDropOverlayService.cleanupFileDropHandling();
|
||||||
return fileStatus === 'PROCESSING' || fileStatus === 'REVIEWED' || true;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public toggleSortByAddedOn() {
|
public get activeProject() {
|
||||||
const sortedByRecent: boolean = (this.sortingOption === this.sortingOptions[0]);
|
return this.appStateService.activeProject.project;
|
||||||
this.sortingOption = sortedByRecent ? this.sortingOptions[1] : this.sortingOptions[0];
|
}
|
||||||
}
|
|
||||||
|
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];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,261 +1,298 @@
|
|||||||
import {EventEmitter, Injectable} from "@angular/core";
|
import { EventEmitter, Injectable } from '@angular/core';
|
||||||
import {
|
import {
|
||||||
FileStatus,
|
FileStatus,
|
||||||
Project,
|
Project,
|
||||||
ProjectControllerService,
|
ProjectControllerService,
|
||||||
ReanalysisControllerService,
|
ReanalysisControllerService,
|
||||||
StatusControllerService
|
StatusControllerService
|
||||||
} from "@redaction/red-ui-http";
|
} from '@redaction/red-ui-http';
|
||||||
import {NotificationService, NotificationType} from "../notification/notification.service";
|
import { NotificationService, NotificationType } from '../notification/notification.service';
|
||||||
import {TranslateService} from "@ngx-translate/core";
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
import {Router} from "@angular/router";
|
import { Router } from '@angular/router';
|
||||||
import {UserService} from "../user/user.service";
|
import { UserService } from '../user/user.service';
|
||||||
import {interval} from "rxjs";
|
import { interval } from 'rxjs';
|
||||||
import {tap} from "rxjs/operators";
|
import { tap } from 'rxjs/operators';
|
||||||
|
|
||||||
|
|
||||||
export interface AppState {
|
export interface AppState {
|
||||||
|
projects: ProjectWrapper[];
|
||||||
projects: ProjectWrapper[];
|
activeProject: ProjectWrapper;
|
||||||
activeProject: ProjectWrapper;
|
activeFile: FileStatus;
|
||||||
activeFile: FileStatus;
|
totalAnalysedPages?: number;
|
||||||
totalAnalysedPages?: number;
|
totalDocuments?: number;
|
||||||
totalDocuments?: number;
|
totalPeople?: number;
|
||||||
totalPeople?: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ProjectWrapper {
|
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({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
})
|
})
|
||||||
export class AppStateService {
|
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(
|
interval(5000)
|
||||||
private readonly _router: Router,
|
.pipe(
|
||||||
private readonly _userService: UserService,
|
tap(() => {
|
||||||
private readonly _projectControllerService: ProjectControllerService,
|
this.reloadActiveProjectFiles();
|
||||||
private readonly _notificationService: NotificationService,
|
})
|
||||||
private readonly _reanalysisControllerService: ReanalysisControllerService,
|
)
|
||||||
private readonly _translateService: TranslateService,
|
.subscribe();
|
||||||
private readonly _statusControllerService: StatusControllerService) {
|
|
||||||
this._appState = {
|
interval(30000)
|
||||||
projects: [],
|
.pipe(
|
||||||
activeProject: null,
|
tap(() => {
|
||||||
activeFile: null
|
this.loadAllProjects();
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.subscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
interval(5000).pipe(tap(() => {
|
get isActiveProjectOwner() {
|
||||||
this.reloadActiveProjectFiles();
|
return this._appState.activeProject?.project?.ownerId === this._userService.userId;
|
||||||
})).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();
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private _getExistingFiles(project: Project) {
|
get isActiveProjectMember() {
|
||||||
const found = this._appState.projects.find(p => p.project.projectId === project.projectId);
|
return (
|
||||||
return found ? found.files : [];
|
this._appState.activeProject?.project?.memberIds?.indexOf(this._userService.userId) >= 0
|
||||||
}
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async getFiles(project?: ProjectWrapper) {
|
get isActiveFileDocumentReviewer() {
|
||||||
const files = await this._statusControllerService.getProjectStatus(project.project.projectId).toPromise();
|
return this._appState.activeFile?.currentReviewer === this._userService.userId;
|
||||||
const oldFiles = [...project.files];
|
}
|
||||||
|
|
||||||
for (const file of files) {
|
get aggregatedFiles(): FileStatus[] {
|
||||||
let found = false;
|
const result: FileStatus[] = [];
|
||||||
for (const oldFile of oldFiles) {
|
this._appState.projects.forEach((p) => {
|
||||||
if (oldFile.fileId === file.fileId) {
|
result.push(...p.files);
|
||||||
// emit when analysis count changed
|
});
|
||||||
if (oldFile.numberOfAnalyses !== file.numberOfAnalyses) {
|
return result;
|
||||||
this.fileStatusChanged.emit(oldFile);
|
}
|
||||||
}
|
|
||||||
found = true;
|
get activeProjectId(): string {
|
||||||
break;
|
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;
|
private _getExistingFiles(project: Project) {
|
||||||
|
const found = this._appState.projects.find(
|
||||||
this._computeStats();
|
(p) => p.project.projectId === project.projectId
|
||||||
return files;
|
);
|
||||||
}
|
return found ? found.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']);
|
|
||||||
}
|
}
|
||||||
return this._appState.activeProject;
|
|
||||||
}
|
|
||||||
|
|
||||||
activateFile(projectId: string, fileId: string) {
|
async getFiles(project?: ProjectWrapper) {
|
||||||
this._appState.activeFile = null;
|
const files = await this._statusControllerService
|
||||||
this._appState.activeProject = this._appState.projects.find(p => p.project.projectId === projectId);
|
.getProjectStatus(project.project.projectId)
|
||||||
this._appState.activeFile = this._appState.activeProject.files.find(f => f.fileId === fileId);
|
.toPromise();
|
||||||
}
|
const oldFiles = [...project.files];
|
||||||
|
|
||||||
reset() {
|
for (const file of files) {
|
||||||
this._appState.activeFile = null;
|
let found = false;
|
||||||
this._appState.activeProject = null;
|
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) {
|
project.files = files;
|
||||||
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) {
|
this._computeStats();
|
||||||
try {
|
return files;
|
||||||
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() {
|
activateProject(projectId: string) {
|
||||||
let totalAnalysedPages = 0;
|
this._appState.activeFile = null;
|
||||||
let totalDocuments = 0;
|
this._appState.activeProject = this._appState.projects.find(
|
||||||
const totalPeople = new Set<string>();
|
(p) => p.project.projectId === projectId
|
||||||
this._appState.projects.forEach(p => {
|
);
|
||||||
totalDocuments += p.files.length;
|
if (!this._appState.activeProject) {
|
||||||
if (p.project.memberIds) {
|
this._router.navigate(['/ui/projects']);
|
||||||
p.project.memberIds.forEach(m => totalPeople.add(m));
|
}
|
||||||
}
|
return this._appState.activeProject;
|
||||||
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() {
|
activateFile(projectId: string, fileId: string) {
|
||||||
if (!this._appState.projects.length) {
|
this._appState.activeFile = null;
|
||||||
await this.loadAllProjects();
|
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() {
|
reset() {
|
||||||
await this._reanalysisControllerService.reanalyzeFile(this._appState.activeProject.project.projectId, this._appState.activeFile.fileId).toPromise();
|
this._appState.activeFile = null;
|
||||||
await this.reloadActiveProjectFiles();
|
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
@ -13,17 +13,17 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.mat-checkbox-layout {
|
.mat-checkbox-layout {
|
||||||
.mat-checkbox-inner-container {
|
.mat-checkbox-inner-container {
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mat-checkbox-label {
|
.mat-checkbox-label {
|
||||||
font-family: 'Inter', sans-serif;
|
font-family: 'Inter', sans-serif;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: $accent;
|
color: $accent;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
.mat-dialog-container {
|
.mat-dialog-container {
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dialog {
|
.dialog {
|
||||||
|
|||||||
@ -1,63 +1,65 @@
|
|||||||
@import "red-variables";
|
@import 'red-variables';
|
||||||
@import "red-mixins";
|
@import 'red-mixins';
|
||||||
|
|
||||||
.red-input-group {
|
.red-input-group {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
margin-top: 13px;
|
margin-top: 13px;
|
||||||
|
|
||||||
&:first-child {
|
&:first-child {
|
||||||
margin-top: 0;
|
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mat-select-value {
|
input,
|
||||||
vertical-align: middle;
|
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 {
|
mat-select {
|
||||||
resize: none;
|
width: 220px;
|
||||||
padding-top: 7px;
|
|
||||||
padding-bottom: 7px;
|
|
||||||
@include no-scroll-bar();
|
|
||||||
}
|
|
||||||
|
|
||||||
label {
|
.mat-select-trigger {
|
||||||
opacity: 0.7;
|
height: 32px;
|
||||||
font-size: 11px;
|
}
|
||||||
letter-spacing: 0;
|
|
||||||
line-height: 14px;
|
|
||||||
margin-bottom: 2px;
|
|
||||||
|
|
||||||
&.mat-checkbox-layout {
|
.mat-select-value {
|
||||||
opacity: 1;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,29 +1,29 @@
|
|||||||
@import "red-variables";
|
@import 'red-variables';
|
||||||
|
|
||||||
.mat-menu-panel {
|
.mat-menu-panel {
|
||||||
border-radius: 8px !important;
|
border-radius: 8px !important;
|
||||||
box-shadow: 0 2px 6px 0 rgba(40, 50, 65, 0.3);
|
box-shadow: 0 2px 6px 0 rgba(40, 50, 65, 0.3);
|
||||||
max-width: none !important;
|
max-width: none !important;
|
||||||
|
|
||||||
.mat-menu-item {
|
.mat-menu-item {
|
||||||
font-family: 'Inter', sans-serif;
|
font-family: 'Inter', sans-serif;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: $accent;
|
color: $accent;
|
||||||
|
|
||||||
.arrow-wrapper {
|
.arrow-wrapper {
|
||||||
width: 16px;
|
width: 16px;
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
mat-icon {
|
mat-icon {
|
||||||
width: 9px;
|
width: 9px;
|
||||||
height: 9px;
|
height: 9px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.padding-left {
|
||||||
|
padding-left: 64px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.padding-left {
|
|
||||||
padding-left: 64px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,10 +10,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@mixin no-scroll-bar {
|
@mixin no-scroll-bar {
|
||||||
scrollbar-width: none; /* Firefox */
|
scrollbar-width: none; /* Firefox */
|
||||||
-ms-overflow-style: none; /* IE 10+ */
|
-ms-overflow-style: none; /* IE 10+ */
|
||||||
&::-webkit-scrollbar {
|
&::-webkit-scrollbar {
|
||||||
width: 0;
|
width: 0;
|
||||||
background: transparent; /* Chrome/Safari/Webkit */
|
background: transparent; /* Chrome/Safari/Webkit */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,181 +1,182 @@
|
|||||||
@import "red-variables";
|
@import 'red-variables';
|
||||||
|
|
||||||
html, body {
|
html,
|
||||||
margin: 0;
|
body {
|
||||||
padding: 0;
|
margin: 0;
|
||||||
height: 100vh;
|
padding: 0;
|
||||||
font-family: Inter, sans-serif;
|
height: 100vh;
|
||||||
color: $accent;
|
font-family: Inter, sans-serif;
|
||||||
font-size: 13px;
|
color: $accent;
|
||||||
line-height: 16px;
|
font-size: 13px;
|
||||||
|
line-height: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-header {
|
.page-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 0 24px;
|
padding: 0 24px;
|
||||||
height: 50px;
|
height: 50px;
|
||||||
box-shadow: 0 2px 4px 0 $grey-4;
|
box-shadow: 0 2px 4px 0 $grey-4;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 61px;
|
top: 61px;
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
background-color: $white;
|
background-color: $white;
|
||||||
z-index: 3;
|
z-index: 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
.red-content-inner {
|
.red-content-inner {
|
||||||
margin-top: 50px;
|
margin-top: 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.right-fixed-container {
|
.right-fixed-container {
|
||||||
border-left: 1px solid $grey-4;
|
border-left: 1px solid $grey-4;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: $right-container-inside-width;
|
width: $right-container-inside-width;
|
||||||
padding: $right-container-padding;
|
padding: $right-container-padding;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
right: 0;
|
right: 0;
|
||||||
|
|
||||||
.actions-row {
|
.actions-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
|
|
||||||
> div {
|
> div {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
|
|
||||||
mat-icon {
|
mat-icon {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
width: 14px;
|
width: 14px;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: $primary;
|
color: $primary;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.filters {
|
.filters {
|
||||||
div {
|
div {
|
||||||
margin-right: 6px;
|
margin-right: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
flex-direction: row-reverse;
|
flex-direction: row-reverse;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.flex-row {
|
.flex-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.flex {
|
.flex {
|
||||||
display: flex !important;
|
display: flex !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.flex-end {
|
.flex-end {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
.flex-1 {
|
.flex-1 {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.flex-2 {
|
.flex-2 {
|
||||||
flex: 2;
|
flex: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mt-5 {
|
.mt-5 {
|
||||||
margin-top: 5px;
|
margin-top: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mt-20 {
|
.mt-20 {
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.left-container {
|
.left-container {
|
||||||
height: calc(100vh - 61px - 50px);
|
height: calc(100vh - 61px - 50px);
|
||||||
width: calc(100vw - #{$right-container-width});
|
width: calc(100vw - #{$right-container-width});
|
||||||
}
|
}
|
||||||
|
|
||||||
.break-20 {
|
.break-20 {
|
||||||
height: 20px;
|
height: 20px;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-row {
|
.detail-row {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
font-family: Inconsolata, monospace, monospace;
|
font-family: Inconsolata, monospace, monospace;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
letter-spacing: 0;
|
letter-spacing: 0;
|
||||||
line-height: 14px;
|
line-height: 14px;
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.red-top-bar {
|
.red-top-bar {
|
||||||
height: 61px;
|
height: 61px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-height: 61px;
|
max-height: 61px;
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
.top-bar-row {
|
|
||||||
height: 60px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
flex-direction: column;
|
||||||
padding: 0 24px;
|
|
||||||
|
|
||||||
.center {
|
.top-bar-row {
|
||||||
display: flex;
|
height: 60px;
|
||||||
align-items: center;
|
display: flex;
|
||||||
justify-content: center;
|
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 {
|
.divider {
|
||||||
margin-left: 16px;
|
height: 1px;
|
||||||
height: 20px;
|
opacity: 0.15;
|
||||||
font-size: 16px;
|
background-color: $grey-1;
|
||||||
font-weight: 600;
|
|
||||||
letter-spacing: 0;
|
|
||||||
line-height: 20px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.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 {
|
.red-content {
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
height: calc(100vh - 61px);
|
height: calc(100vh - 61px);
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hidden {
|
.hidden {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.not-visible {
|
.not-visible {
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pointer {
|
.pointer {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,98 +1,97 @@
|
|||||||
@import "red-variables";
|
@import 'red-variables';
|
||||||
@import "red-mixins";
|
@import 'red-mixins';
|
||||||
|
|
||||||
.table-header {
|
.table-header {
|
||||||
background-color: rgba(226, 228, 233, 0.9);
|
background-color: rgba(226, 228, 233, 0.9);
|
||||||
height: 50px;
|
height: 50px;
|
||||||
padding: 0 16px;
|
padding: 0 16px;
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
.actions {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 25px;
|
|
||||||
}
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 25px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.grid-container {
|
.grid-container {
|
||||||
display: grid;
|
display: grid;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
.no-data {
|
.no-data {
|
||||||
grid-column: 1/-1;
|
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-item-title {
|
.table-col-name {
|
||||||
font-weight: 600;
|
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;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user