blah
This commit is contained in:
parent
69d7f48108
commit
30d40fef03
@ -1,4 +1,6 @@
|
||||
<div class="flex-row">
|
||||
<div [className]="colorClass + ' oval ' + size">{{initials}}</div>
|
||||
<div *ngIf="withName" class="clamp-2">{{username || ('initials-avatar.unassigned.label' | translate)}}</div>
|
||||
<div [className]="colorClass + ' oval ' + size">{{ initials }}</div>
|
||||
<div *ngIf="withName" class="clamp-2">
|
||||
{{ username || ('initials-avatar.unassigned.label' | translate) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -3,57 +3,56 @@ import { UserService } from '../../user/user.service';
|
||||
import { User } from '@redaction/red-ui-http';
|
||||
|
||||
@Component({
|
||||
selector: 'redaction-initials-avatar',
|
||||
templateUrl: './initials-avatar.component.html',
|
||||
styleUrls: ['./initials-avatar.component.scss']
|
||||
selector: 'redaction-initials-avatar',
|
||||
templateUrl: './initials-avatar.component.html',
|
||||
styleUrls: ['./initials-avatar.component.scss']
|
||||
})
|
||||
export class InitialsAvatarComponent implements OnInit, OnChanges {
|
||||
@Input()
|
||||
public userId: string;
|
||||
@Input()
|
||||
public userId: string;
|
||||
|
||||
@Input()
|
||||
public color = 'lightgray';
|
||||
@Input()
|
||||
public color = 'lightgray';
|
||||
|
||||
@Input()
|
||||
public size: 'small' | 'large' = 'small';
|
||||
@Input()
|
||||
public size: 'small' | 'large' = 'small';
|
||||
|
||||
@Input()
|
||||
public withName = false;
|
||||
@Input()
|
||||
public withName = false;
|
||||
|
||||
public _user: User;
|
||||
public _user: User;
|
||||
|
||||
constructor(private readonly _userService: UserService) {
|
||||
}
|
||||
constructor(private readonly _userService: UserService) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
}
|
||||
ngOnInit(): void {}
|
||||
|
||||
ngOnChanges(): void {
|
||||
this._user = this._userService.getUserById(this.userId);
|
||||
}
|
||||
|
||||
public get username(): string {
|
||||
return this._userService.getName(this._user);
|
||||
}
|
||||
|
||||
public get initials(): string {
|
||||
if (!this._user) {
|
||||
return '?'
|
||||
ngOnChanges(): void {
|
||||
this._user = this._userService.getUserById(this.userId);
|
||||
}
|
||||
|
||||
return this._userService.getName(this._user)
|
||||
.split(' ')
|
||||
.filter(value => value !== ' ')
|
||||
.filter((value, idx) => idx < 2)
|
||||
.map((str) => str[0])
|
||||
.join('');
|
||||
}
|
||||
|
||||
public get colorClass() {
|
||||
if (this.color.includes('-')) {
|
||||
return this.color;
|
||||
public get username(): string {
|
||||
return this._userService.getName(this._user);
|
||||
}
|
||||
|
||||
public get initials(): string {
|
||||
if (!this._user) {
|
||||
return '?';
|
||||
}
|
||||
|
||||
return this._userService
|
||||
.getName(this._user)
|
||||
.split(' ')
|
||||
.filter((value) => value !== ' ')
|
||||
.filter((value, idx) => idx < 2)
|
||||
.map((str) => str[0])
|
||||
.join('');
|
||||
}
|
||||
|
||||
public get colorClass() {
|
||||
if (this.color.includes('-')) {
|
||||
return this.color;
|
||||
}
|
||||
const textColor = !this._user || !this._userService.isManager(this._user) ? 'dark' : 'red';
|
||||
return `${this.color}-${textColor}`;
|
||||
}
|
||||
const textColor = !this._user || !this._userService.isManager(this._user) ? 'dark' : 'red';
|
||||
return `${this.color}-${textColor}`
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
<div class="rectangle-container" [ngClass]="{ small: small }">
|
||||
<div *ngFor="let rect of config" class="section-wrapper" [style]="'flex: ' + rect.length + ';'">
|
||||
<div [className]="'rectangle ' + rect.color "></div>
|
||||
<div *ngIf="rect.label" [ngClass]="labelClass">{{ rect.label }}</div>
|
||||
</div>
|
||||
<div *ngFor="let rect of config" class="section-wrapper" [style]="'flex: ' + rect.length + ';'">
|
||||
<div [className]="'rectangle ' + rect.color"></div>
|
||||
<div *ngIf="rect.label" [ngClass]="labelClass">{{ rect.label }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,78 +1,78 @@
|
||||
@import '../../../assets/styles/red-variables';
|
||||
|
||||
.rectangle-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
min-width: 12px;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
min-width: 12px;
|
||||
|
||||
&.small {
|
||||
.rectangle {
|
||||
width: 12px;
|
||||
}
|
||||
|
||||
.section-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.section-wrapper:first-child {
|
||||
.rectangle {
|
||||
border-radius: 6px 0 0 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.section-wrapper:last-child {
|
||||
.rectangle {
|
||||
border-radius: 0 6px 6px 0;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
.rectangle {
|
||||
border-radius: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.small {
|
||||
.rectangle {
|
||||
width: 12px;
|
||||
}
|
||||
height: 4px;
|
||||
|
||||
.section-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
&.UNASSIGNED {
|
||||
background-color: $grey-5;
|
||||
}
|
||||
|
||||
.section-wrapper:first-child {
|
||||
.rectangle {
|
||||
border-radius: 6px 0 0 6px;
|
||||
}
|
||||
}
|
||||
&.UNDER_REVIEW {
|
||||
background-color: $yellow-1;
|
||||
}
|
||||
|
||||
.section-wrapper:last-child {
|
||||
.rectangle {
|
||||
border-radius: 0 6px 6px 0;
|
||||
}
|
||||
&.UNDER_APPROVAL {
|
||||
background-color: $red-1;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
.rectangle {
|
||||
border-radius: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
&.APPROVED {
|
||||
background-color: $blue-2;
|
||||
}
|
||||
|
||||
.rectangle {
|
||||
height: 4px;
|
||||
&.SUBMITTED {
|
||||
background-color: $blue-3;
|
||||
}
|
||||
|
||||
&.UNASSIGNED {
|
||||
background-color: $grey-5;
|
||||
}
|
||||
&.EFSA {
|
||||
background-color: $blue-4;
|
||||
}
|
||||
|
||||
&.UNDER_REVIEW {
|
||||
background-color: $yellow-1;
|
||||
}
|
||||
&.FINISHED {
|
||||
background-color: $green-2;
|
||||
}
|
||||
|
||||
&.UNDER_APPROVAL {
|
||||
background-color: $red-1;
|
||||
}
|
||||
&.ACTIVE {
|
||||
background-color: $primary;
|
||||
}
|
||||
|
||||
&.APPROVED {
|
||||
background-color: $blue-2;
|
||||
&.ARCHIVED {
|
||||
background-color: rgba($red-1, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
&.SUBMITTED {
|
||||
background-color: $blue-3;
|
||||
}
|
||||
|
||||
&.EFSA {
|
||||
background-color: $blue-4;
|
||||
}
|
||||
|
||||
&.FINISHED {
|
||||
background-color: $green-2;
|
||||
}
|
||||
|
||||
&.ACTIVE {
|
||||
background-color: $primary;
|
||||
}
|
||||
|
||||
&.ARCHIVED {
|
||||
background-color: rgba($red-1, 0.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,40 +1,51 @@
|
||||
<section class="dialog">
|
||||
<form (submit)="saveProject()" [formGroup]="projectForm">
|
||||
<div
|
||||
[translate]="
|
||||
project?.projectId
|
||||
? 'project-listing.add-edit-dialog.header-edit.label'
|
||||
: 'project-listing.add-edit-dialog.header-new.label'
|
||||
"
|
||||
class="dialog-header heading-l"
|
||||
></div>
|
||||
|
||||
<form (submit)="saveProject()" [formGroup]="projectForm">
|
||||
<div
|
||||
[translate]="project?.projectId ? 'project-listing.add-edit-dialog.header-edit.label': 'project-listing.add-edit-dialog.header-new.label'"
|
||||
class="dialog-header heading-l">
|
||||
<div class="dialog-content">
|
||||
<div class="red-input-group">
|
||||
<label translate="project-listing.add-edit-dialog.form.name.label"></label>
|
||||
<input formControlName="projectName" name="projectName" type="text" />
|
||||
</div>
|
||||
<div class="red-input-group">
|
||||
<label translate="project-listing.add-edit-dialog.form.description.label"></label>
|
||||
<textarea
|
||||
formControlName="description"
|
||||
name="description"
|
||||
type="text"
|
||||
rows="5"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<mat-form-field class="mt-20">
|
||||
<mat-label>{{
|
||||
'project-listing.add-edit-dialog.form.due-date.label' | translate
|
||||
}}</mat-label>
|
||||
<input matInput [matDatepicker]="picker" formControlName="dueDate" />
|
||||
<mat-datepicker-toggle matSuffix [for]="picker"></mat-datepicker-toggle>
|
||||
<mat-datepicker #picker></mat-datepicker>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="dialog-content">
|
||||
<div class="red-input-group">
|
||||
<label translate="project-listing.add-edit-dialog.form.name.label"></label>
|
||||
<input formControlName="projectName" name="projectName" type="text">
|
||||
</div>
|
||||
<div class="red-input-group">
|
||||
<label translate="project-listing.add-edit-dialog.form.description.label"></label>
|
||||
<textarea formControlName="description" name="description" type="text" rows="5"></textarea>
|
||||
</div>
|
||||
<div class="dialog-actions">
|
||||
<button
|
||||
[disabled]="projectForm.invalid"
|
||||
color="primary"
|
||||
mat-flat-button
|
||||
translate="project-listing.add-edit-dialog.actions.save.label"
|
||||
type="submit"
|
||||
></button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<mat-form-field class="mt-20">
|
||||
<mat-label>{{'project-listing.add-edit-dialog.form.due-date.label' | translate}}</mat-label>
|
||||
<input matInput [matDatepicker]="picker" formControlName="dueDate">
|
||||
<mat-datepicker-toggle matSuffix [for]="picker"></mat-datepicker-toggle>
|
||||
<mat-datepicker #picker></mat-datepicker>
|
||||
</mat-form-field>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<div class="dialog-actions">
|
||||
<button [disabled]="projectForm.invalid" color="primary" mat-flat-button
|
||||
translate="project-listing.add-edit-dialog.actions.save.label" type="submit"></button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
|
||||
<button (click)="dialogRef.close()" class="dialog-close" mat-icon-button>
|
||||
<mat-icon svgIcon="red:close"></mat-icon>
|
||||
</button>
|
||||
<button (click)="dialogRef.close()" class="dialog-close" mat-icon-button>
|
||||
<mat-icon svgIcon="red:close"></mat-icon>
|
||||
</button>
|
||||
</section>
|
||||
|
||||
@ -2,11 +2,11 @@ import { Injectable } from '@angular/core';
|
||||
import { FileDetailsDialogComponent } from './file-details-dialog/file-details-dialog.component';
|
||||
import { MatDialog, MatDialogRef } from '@angular/material/dialog';
|
||||
import {
|
||||
FileStatus,
|
||||
FileUploadControllerService,
|
||||
ManualRedactionControllerService,
|
||||
ManualRedactionEntry,
|
||||
Project
|
||||
FileStatus,
|
||||
FileUploadControllerService,
|
||||
ManualRedactionControllerService,
|
||||
ManualRedactionEntry,
|
||||
Project
|
||||
} from '@redaction/red-ui-http';
|
||||
import { ConfirmationDialogComponent } from '../common/confirmation-dialog/confirmation-dialog.component';
|
||||
import { NotificationService, NotificationType } from '../notification/notification.service';
|
||||
@ -19,180 +19,258 @@ import { ManualRedactionDialogComponent } from './manual-redaction-dialog/manual
|
||||
import { Annotations } from '@pdftron/webviewer';
|
||||
|
||||
const dialogConfig = {
|
||||
width: '600px',
|
||||
maxWidth: '90vw',
|
||||
autoFocus: false
|
||||
width: '600px',
|
||||
maxWidth: '90vw',
|
||||
autoFocus: false
|
||||
};
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class DialogService {
|
||||
constructor(
|
||||
private readonly _dialog: MatDialog,
|
||||
private readonly _translateService: TranslateService,
|
||||
private readonly _appStateService: AppStateService,
|
||||
private readonly _fileUploadControllerService: FileUploadControllerService,
|
||||
private readonly _notificationService: NotificationService,
|
||||
private readonly _manualRedactionControllerService: ManualRedactionControllerService
|
||||
) {}
|
||||
|
||||
constructor(private readonly _dialog: MatDialog,
|
||||
private readonly _translateService: TranslateService,
|
||||
private readonly _appStateService: AppStateService,
|
||||
private readonly _fileUploadControllerService: FileUploadControllerService,
|
||||
private readonly _notificationService: NotificationService,
|
||||
private readonly _manualRedactionControllerService: ManualRedactionControllerService) {
|
||||
|
||||
}
|
||||
|
||||
public openFileDetailsDialog($event: MouseEvent, file: FileStatus): MatDialogRef<FileDetailsDialogComponent> {
|
||||
$event.stopPropagation();
|
||||
return this._dialog.open(FileDetailsDialogComponent, {
|
||||
...dialogConfig,
|
||||
data: file
|
||||
});
|
||||
}
|
||||
|
||||
public openDeleteFileDialog($event: MouseEvent, projectId: string, fileId: string, cb?: Function): MatDialogRef<ConfirmationDialogComponent> {
|
||||
$event.stopPropagation();
|
||||
const ref = this._dialog.open(ConfirmationDialogComponent, dialogConfig);
|
||||
|
||||
ref.afterClosed().subscribe(result => {
|
||||
if (result) {
|
||||
const file = this._appStateService.getFileById(projectId, fileId);
|
||||
this._fileUploadControllerService.deleteFile(file.projectId, file.fileId).subscribe(async () => {
|
||||
await this._appStateService.reloadActiveProjectFiles();
|
||||
if (cb) cb();
|
||||
}, () => {
|
||||
this._notificationService.showToastNotification(
|
||||
this._translateService.instant('delete-file-error.label', file),
|
||||
null,
|
||||
NotificationType.ERROR);
|
||||
public openFileDetailsDialog(
|
||||
$event: MouseEvent,
|
||||
file: FileStatus
|
||||
): MatDialogRef<FileDetailsDialogComponent> {
|
||||
$event.stopPropagation();
|
||||
return this._dialog.open(FileDetailsDialogComponent, {
|
||||
...dialogConfig,
|
||||
data: file
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return ref;
|
||||
}
|
||||
public openDeleteFileDialog(
|
||||
$event: MouseEvent,
|
||||
projectId: string,
|
||||
fileId: string,
|
||||
cb?: Function
|
||||
): MatDialogRef<ConfirmationDialogComponent> {
|
||||
$event.stopPropagation();
|
||||
const ref = this._dialog.open(ConfirmationDialogComponent, dialogConfig);
|
||||
|
||||
public openManualRedactionDialog($event: ManualRedactionEntry, cb?: Function): MatDialogRef<ManualRedactionDialogComponent> {
|
||||
const ref = this._dialog.open(ManualRedactionDialogComponent, {
|
||||
...dialogConfig,
|
||||
autoFocus: true,
|
||||
data: $event
|
||||
});
|
||||
|
||||
ref.afterClosed().subscribe(result => {
|
||||
if (cb) {
|
||||
cb(result);
|
||||
}
|
||||
});
|
||||
|
||||
return ref;
|
||||
}
|
||||
|
||||
public acceptSuggestionAnnotation($event: MouseEvent, annotation: Annotations.Annotation, projectId: string, fileId: string): MatDialogRef<ConfirmationDialogComponent> {
|
||||
$event.stopPropagation();
|
||||
|
||||
const parts = annotation.Id.split(':');
|
||||
const annotationId = parts[parts.length - 1];
|
||||
|
||||
const ref = this._dialog.open(ConfirmationDialogComponent, dialogConfig);
|
||||
ref.afterClosed().subscribe(result => {
|
||||
if (result) {
|
||||
this._manualRedactionControllerService.approveRequest(projectId, fileId, annotationId).subscribe(() => {
|
||||
this._notificationService.showToastNotification(this._translateService.instant('manual-redaction.confirm-annotation.success.label'), null, NotificationType.SUCCESS);
|
||||
}, (err) => {
|
||||
this._notificationService.showToastNotification(this._translateService.instant('manual-redaction.confirm-annotation.failed.label', err), null, NotificationType.ERROR);
|
||||
ref.afterClosed().subscribe((result) => {
|
||||
if (result) {
|
||||
const file = this._appStateService.getFileById(projectId, fileId);
|
||||
this._fileUploadControllerService.deleteFile(file.projectId, file.fileId).subscribe(
|
||||
async () => {
|
||||
await this._appStateService.reloadActiveProjectFiles();
|
||||
if (cb) cb();
|
||||
},
|
||||
() => {
|
||||
this._notificationService.showToastNotification(
|
||||
this._translateService.instant('delete-file-error.label', file),
|
||||
null,
|
||||
NotificationType.ERROR
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return ref;
|
||||
}
|
||||
return ref;
|
||||
}
|
||||
|
||||
public suggestRemoveAnnotation($event: MouseEvent, annotation: Annotations.Annotation, projectId: string, fileId: string): MatDialogRef<ConfirmationDialogComponent> {
|
||||
$event.stopPropagation();
|
||||
|
||||
const parts = annotation.Id.split(':');
|
||||
const annotationId = parts[parts.length - 1];
|
||||
|
||||
const ref = this._dialog.open(ConfirmationDialogComponent, {
|
||||
width: '400px',
|
||||
maxWidth: '90vw'
|
||||
});
|
||||
|
||||
ref.afterClosed().subscribe(result => {
|
||||
if (result) {
|
||||
this._manualRedactionControllerService.undo(projectId, fileId, annotationId).subscribe(ok => {
|
||||
this._notificationService.showToastNotification(this._translateService.instant('manual-redaction.remove-annotation.success.label'), null, NotificationType.SUCCESS);
|
||||
}, (err) => {
|
||||
this._notificationService.showToastNotification(this._translateService.instant('manual-redaction.remove-annotation.failed.label', err), null, NotificationType.ERROR);
|
||||
public openManualRedactionDialog(
|
||||
$event: ManualRedactionEntry,
|
||||
cb?: Function
|
||||
): MatDialogRef<ManualRedactionDialogComponent> {
|
||||
const ref = this._dialog.open(ManualRedactionDialogComponent, {
|
||||
...dialogConfig,
|
||||
autoFocus: true,
|
||||
data: $event
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return ref;
|
||||
}
|
||||
ref.afterClosed().subscribe((result) => {
|
||||
if (cb) {
|
||||
cb(result);
|
||||
}
|
||||
});
|
||||
|
||||
public openEditProjectDialog($event: MouseEvent, project: Project): MatDialogRef<AddEditProjectDialogComponent> {
|
||||
$event.stopPropagation();
|
||||
return this._dialog.open(AddEditProjectDialogComponent, {
|
||||
...dialogConfig,
|
||||
autoFocus: true,
|
||||
data: project
|
||||
});
|
||||
}
|
||||
return ref;
|
||||
}
|
||||
|
||||
public openDeleteProjectDialog($event: MouseEvent, project: Project, cb?: Function): MatDialogRef<ConfirmationDialogComponent> {
|
||||
$event.stopPropagation();
|
||||
const ref = this._dialog.open(ConfirmationDialogComponent, dialogConfig);
|
||||
ref.afterClosed().subscribe(async result => {
|
||||
if (result) {
|
||||
await this._appStateService.deleteProject(project);
|
||||
if (cb) cb();
|
||||
}
|
||||
});
|
||||
return ref;
|
||||
}
|
||||
public acceptSuggestionAnnotation(
|
||||
$event: MouseEvent,
|
||||
annotation: Annotations.Annotation,
|
||||
projectId: string,
|
||||
fileId: string
|
||||
): MatDialogRef<ConfirmationDialogComponent> {
|
||||
$event.stopPropagation();
|
||||
|
||||
public openAssignProjectMembersAndOwnerDialog($event: MouseEvent, project: Project, cb?: Function): MatDialogRef<AssignOwnerDialogComponent> {
|
||||
$event?.stopPropagation();
|
||||
const ref = this._dialog.open(AssignOwnerDialogComponent, {
|
||||
...dialogConfig,
|
||||
data: { type: 'project', project: project }
|
||||
});
|
||||
ref.afterClosed().subscribe(result => {
|
||||
if (result && cb) cb();
|
||||
});
|
||||
return ref;
|
||||
}
|
||||
const parts = annotation.Id.split(':');
|
||||
const annotationId = parts[parts.length - 1];
|
||||
|
||||
public openAssignFileOwnerDialog($event: MouseEvent, file: FileStatus, cb?: Function): MatDialogRef<AssignOwnerDialogComponent> {
|
||||
$event.stopPropagation();
|
||||
const ref = this._dialog.open(AssignOwnerDialogComponent, {
|
||||
...dialogConfig,
|
||||
data: { type: 'file', file: file }
|
||||
});
|
||||
const ref = this._dialog.open(ConfirmationDialogComponent, dialogConfig);
|
||||
ref.afterClosed().subscribe((result) => {
|
||||
if (result) {
|
||||
this._manualRedactionControllerService
|
||||
.approveRequest(projectId, fileId, annotationId)
|
||||
.subscribe(
|
||||
() => {
|
||||
this._notificationService.showToastNotification(
|
||||
this._translateService.instant(
|
||||
'manual-redaction.confirm-annotation.success.label'
|
||||
),
|
||||
null,
|
||||
NotificationType.SUCCESS
|
||||
);
|
||||
},
|
||||
(err) => {
|
||||
this._notificationService.showToastNotification(
|
||||
this._translateService.instant(
|
||||
'manual-redaction.confirm-annotation.failed.label',
|
||||
err
|
||||
),
|
||||
null,
|
||||
NotificationType.ERROR
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
ref.afterClosed().subscribe(() => {
|
||||
if (cb) cb();
|
||||
});
|
||||
return ref;
|
||||
}
|
||||
|
||||
return ref;
|
||||
}
|
||||
public suggestRemoveAnnotation(
|
||||
$event: MouseEvent,
|
||||
annotation: Annotations.Annotation,
|
||||
projectId: string,
|
||||
fileId: string
|
||||
): MatDialogRef<ConfirmationDialogComponent> {
|
||||
$event.stopPropagation();
|
||||
|
||||
public openProjectDetailsDialog($event: MouseEvent, project: ProjectWrapper): MatDialogRef<ProjectDetailsDialogComponent> {
|
||||
$event.stopPropagation();
|
||||
return this._dialog.open(ProjectDetailsDialogComponent, {
|
||||
...dialogConfig,
|
||||
data: project
|
||||
});
|
||||
}
|
||||
const parts = annotation.Id.split(':');
|
||||
const annotationId = parts[parts.length - 1];
|
||||
|
||||
public openAddProjectDialog(cb?: Function): MatDialogRef<AddEditProjectDialogComponent> {
|
||||
const ref = this._dialog.open(AddEditProjectDialogComponent, {
|
||||
...dialogConfig,
|
||||
autoFocus: true
|
||||
});
|
||||
const ref = this._dialog.open(ConfirmationDialogComponent, {
|
||||
width: '400px',
|
||||
maxWidth: '90vw'
|
||||
});
|
||||
|
||||
ref.afterClosed().subscribe(result => {
|
||||
if (result && cb) cb();
|
||||
});
|
||||
ref.afterClosed().subscribe((result) => {
|
||||
if (result) {
|
||||
this._manualRedactionControllerService
|
||||
.undo(projectId, fileId, annotationId)
|
||||
.subscribe(
|
||||
(ok) => {
|
||||
this._notificationService.showToastNotification(
|
||||
this._translateService.instant(
|
||||
'manual-redaction.remove-annotation.success.label'
|
||||
),
|
||||
null,
|
||||
NotificationType.SUCCESS
|
||||
);
|
||||
},
|
||||
(err) => {
|
||||
this._notificationService.showToastNotification(
|
||||
this._translateService.instant(
|
||||
'manual-redaction.remove-annotation.failed.label',
|
||||
err
|
||||
),
|
||||
null,
|
||||
NotificationType.ERROR
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return ref;
|
||||
}
|
||||
return ref;
|
||||
}
|
||||
|
||||
public openEditProjectDialog(
|
||||
$event: MouseEvent,
|
||||
project: Project
|
||||
): MatDialogRef<AddEditProjectDialogComponent> {
|
||||
$event.stopPropagation();
|
||||
return this._dialog.open(AddEditProjectDialogComponent, {
|
||||
...dialogConfig,
|
||||
autoFocus: true,
|
||||
data: project
|
||||
});
|
||||
}
|
||||
|
||||
public openDeleteProjectDialog(
|
||||
$event: MouseEvent,
|
||||
project: Project,
|
||||
cb?: Function
|
||||
): MatDialogRef<ConfirmationDialogComponent> {
|
||||
$event.stopPropagation();
|
||||
const ref = this._dialog.open(ConfirmationDialogComponent, dialogConfig);
|
||||
ref.afterClosed().subscribe(async (result) => {
|
||||
if (result) {
|
||||
await this._appStateService.deleteProject(project);
|
||||
if (cb) cb();
|
||||
}
|
||||
});
|
||||
return ref;
|
||||
}
|
||||
|
||||
public openAssignProjectMembersAndOwnerDialog(
|
||||
$event: MouseEvent,
|
||||
project: Project,
|
||||
cb?: Function
|
||||
): MatDialogRef<AssignOwnerDialogComponent> {
|
||||
$event?.stopPropagation();
|
||||
const ref = this._dialog.open(AssignOwnerDialogComponent, {
|
||||
...dialogConfig,
|
||||
data: { type: 'project', project: project }
|
||||
});
|
||||
ref.afterClosed().subscribe((result) => {
|
||||
if (result && cb) cb();
|
||||
});
|
||||
return ref;
|
||||
}
|
||||
|
||||
public openAssignFileOwnerDialog(
|
||||
$event: MouseEvent,
|
||||
file: FileStatus,
|
||||
cb?: Function
|
||||
): MatDialogRef<AssignOwnerDialogComponent> {
|
||||
$event.stopPropagation();
|
||||
const ref = this._dialog.open(AssignOwnerDialogComponent, {
|
||||
...dialogConfig,
|
||||
data: { type: 'file', file: file }
|
||||
});
|
||||
|
||||
ref.afterClosed().subscribe(() => {
|
||||
if (cb) cb();
|
||||
});
|
||||
|
||||
return ref;
|
||||
}
|
||||
|
||||
public openProjectDetailsDialog(
|
||||
$event: MouseEvent,
|
||||
project: ProjectWrapper
|
||||
): MatDialogRef<ProjectDetailsDialogComponent> {
|
||||
$event.stopPropagation();
|
||||
return this._dialog.open(ProjectDetailsDialogComponent, {
|
||||
...dialogConfig,
|
||||
data: project
|
||||
});
|
||||
}
|
||||
|
||||
public openAddProjectDialog(cb?: Function): MatDialogRef<AddEditProjectDialogComponent> {
|
||||
const ref = this._dialog.open(AddEditProjectDialogComponent, {
|
||||
...dialogConfig,
|
||||
autoFocus: true
|
||||
});
|
||||
|
||||
ref.afterClosed().subscribe((result) => {
|
||||
if (result && cb) cb();
|
||||
});
|
||||
|
||||
return ref;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,53 +1,53 @@
|
||||
<section class="dialog">
|
||||
<form (submit)="handleAddRedaction()" [formGroup]="redactionForm">
|
||||
<div
|
||||
class="dialog-header heading-l" translate="manual-redaction.dialog.header.label">
|
||||
<form (submit)="handleAddRedaction()" [formGroup]="redactionForm">
|
||||
<div class="dialog-header heading-l" translate="manual-redaction.dialog.header.label"></div>
|
||||
|
||||
</div>
|
||||
<div class="dialog-content">
|
||||
<div class="red-input-group">
|
||||
<label translate="manual-redaction.dialog.content.text.label"></label>
|
||||
</div>
|
||||
{{ addRedactionRequest.value }}
|
||||
|
||||
<div class="dialog-content">
|
||||
<div class="red-input-group">
|
||||
<label translate="manual-redaction.dialog.content.reason.label"></label>
|
||||
<textarea formControlName="reason" name="reason" type="text" rows="2"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="red-input-group">
|
||||
<label translate="manual-redaction.dialog.content.comment.label"></label>
|
||||
<textarea formControlName="comment" name="comment" type="text" rows="2"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="red-input-group">
|
||||
<label translate="manual-redaction.dialog.content.text.label"></label>
|
||||
</div>
|
||||
{{ addRedactionRequest.value }}
|
||||
<div class="red-input-group">
|
||||
<label translate="manual-redaction.dialog.content.dictionary.label"></label>
|
||||
<mat-select formControlName="dictionary">
|
||||
<mat-option
|
||||
*ngFor="let dictionary of dictionaries | async"
|
||||
[value]="dictionary.type"
|
||||
>
|
||||
{{ dictionary.type }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</div>
|
||||
|
||||
<div class="red-input-group">
|
||||
<label translate="manual-redaction.dialog.content.reason.label"></label>
|
||||
<textarea formControlName="reason" name="reason" type="text" rows="2"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="red-input-group">
|
||||
<label translate="manual-redaction.dialog.content.comment.label"></label>
|
||||
<textarea formControlName="comment" name="comment" type="text" rows="2"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="red-input-group">
|
||||
<label translate="manual-redaction.dialog.content.dictionary.label"></label>
|
||||
<mat-select formControlName="dictionary">
|
||||
<mat-option *ngFor="let dictionary of dictionaries | async" [value]="dictionary.type">
|
||||
{{dictionary.type}}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</div>
|
||||
|
||||
<div class="red-input-group">
|
||||
<mat-checkbox color="primary"
|
||||
formControlName="addToDictionary">{{'manual-redaction.dialog.content.dictionary.add.label' | translate}}</mat-checkbox>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<div class="dialog-actions">
|
||||
<button color="primary" mat-flat-button [disabled]="!redactionForm.valid"
|
||||
translate="manual-redaction.dialog.actions.save.label" type="submit"></button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
<button (click)="dialogRef.close()" class="dialog-close" mat-icon-button>
|
||||
<mat-icon svgIcon="red:close"></mat-icon>
|
||||
</button>
|
||||
<div class="red-input-group">
|
||||
<mat-checkbox color="primary" formControlName="addToDictionary">{{
|
||||
'manual-redaction.dialog.content.dictionary.add.label' | translate
|
||||
}}</mat-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dialog-actions">
|
||||
<button
|
||||
color="primary"
|
||||
mat-flat-button
|
||||
[disabled]="!redactionForm.valid"
|
||||
translate="manual-redaction.dialog.actions.save.label"
|
||||
type="submit"
|
||||
></button>
|
||||
</div>
|
||||
</form>
|
||||
<button (click)="dialogRef.close()" class="dialog-close" mat-icon-button>
|
||||
<mat-icon svgIcon="red:close"></mat-icon>
|
||||
</button>
|
||||
</section>
|
||||
|
||||
@ -1,36 +1,55 @@
|
||||
import {NgModule} from '@angular/core';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {MatIconModule, MatIconRegistry} from '@angular/material/icon';
|
||||
import {DomSanitizer} from '@angular/platform-browser';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { MatIconModule, MatIconRegistry } from '@angular/material/icon';
|
||||
import { DomSanitizer } from '@angular/platform-browser';
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, MatIconModule],
|
||||
declarations: [],
|
||||
exports: [MatIconModule]
|
||||
imports: [CommonModule, MatIconModule],
|
||||
declarations: [],
|
||||
exports: [MatIconModule]
|
||||
})
|
||||
export class IconsModule {
|
||||
constructor(
|
||||
private iconRegistry: MatIconRegistry,
|
||||
private sanitizer: DomSanitizer
|
||||
) {
|
||||
const icons = [
|
||||
'add', 'analyse', 'arrow-down', 'arrow-up', 'arrow-right', 'assign', 'calendar',
|
||||
'check', 'close', 'document', 'double-chevron-right', 'download',
|
||||
'edit', 'error', 'folder', 'info', 'lightning', 'logout', 'menu', 'pages',
|
||||
'plus', 'preview', 'refresh', 'report', 'secret', 'sort-asc', 'sort-desc',
|
||||
'status', 'trash', 'user', 'check-alt',
|
||||
];
|
||||
constructor(private iconRegistry: MatIconRegistry, private sanitizer: DomSanitizer) {
|
||||
const icons = [
|
||||
'add',
|
||||
'analyse',
|
||||
'arrow-down',
|
||||
'arrow-up',
|
||||
'arrow-right',
|
||||
'assign',
|
||||
'calendar',
|
||||
'check',
|
||||
'close',
|
||||
'document',
|
||||
'double-chevron-right',
|
||||
'download',
|
||||
'edit',
|
||||
'error',
|
||||
'folder',
|
||||
'info',
|
||||
'lightning',
|
||||
'logout',
|
||||
'menu',
|
||||
'pages',
|
||||
'plus',
|
||||
'preview',
|
||||
'refresh',
|
||||
'report',
|
||||
'secret',
|
||||
'sort-asc',
|
||||
'sort-desc',
|
||||
'status',
|
||||
'trash',
|
||||
'user',
|
||||
'check-alt'
|
||||
];
|
||||
|
||||
for (const icon of icons) {
|
||||
iconRegistry.addSvgIconInNamespace(
|
||||
'red',
|
||||
icon,
|
||||
sanitizer.bypassSecurityTrustResourceUrl(
|
||||
`/assets/icons/general/${icon}.svg`
|
||||
)
|
||||
);
|
||||
for (const icon of icons) {
|
||||
iconRegistry.addSvgIconInNamespace(
|
||||
'red',
|
||||
icon,
|
||||
sanitizer.bypassSecurityTrustResourceUrl(`/assets/icons/general/${icon}.svg`)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@ -1,69 +1,110 @@
|
||||
<div class="red-top-bar">
|
||||
<div class="top-bar-row">
|
||||
<div class="menu visible-lt-lg">
|
||||
<button [matMenuTriggerFor]="menuNav" mat-flat-button>
|
||||
<mat-icon svgIcon="red:menu"></mat-icon>
|
||||
</button>
|
||||
<mat-menu #menuNav="matMenu">
|
||||
<button mat-menu-item routerLink="/ui/projects"
|
||||
translate="top-bar.navigation-items.projects.label">
|
||||
</button>
|
||||
<button *ngIf="appStateService.activeProject"
|
||||
[routerLink]="'/ui/projects/'+appStateService.activeProjectId"
|
||||
mat-menu-item>{{appStateService.activeProject.project.projectName}}</button>
|
||||
<button *ngIf="appStateService.activeFile"
|
||||
[routerLink]="'/ui/projects/'+appStateService.activeProjectId+'/file/'+appStateService.activeFile.fileId"
|
||||
mat-menu-item>{{appStateService.activeFile.filename}}</button>
|
||||
</mat-menu>
|
||||
<div class="top-bar-row">
|
||||
<div class="menu visible-lt-lg">
|
||||
<button [matMenuTriggerFor]="menuNav" mat-flat-button>
|
||||
<mat-icon svgIcon="red:menu"></mat-icon>
|
||||
</button>
|
||||
<mat-menu #menuNav="matMenu">
|
||||
<button
|
||||
mat-menu-item
|
||||
routerLink="/ui/projects"
|
||||
translate="top-bar.navigation-items.projects.label"
|
||||
></button>
|
||||
<button
|
||||
*ngIf="appStateService.activeProject"
|
||||
[routerLink]="'/ui/projects/' + appStateService.activeProjectId"
|
||||
mat-menu-item
|
||||
>
|
||||
{{ appStateService.activeProject.project.projectName }}
|
||||
</button>
|
||||
<button
|
||||
*ngIf="appStateService.activeFile"
|
||||
[routerLink]="
|
||||
'/ui/projects/' +
|
||||
appStateService.activeProjectId +
|
||||
'/file/' +
|
||||
appStateService.activeFile.fileId
|
||||
"
|
||||
mat-menu-item
|
||||
>
|
||||
{{ appStateService.activeFile.filename }}
|
||||
</button>
|
||||
</mat-menu>
|
||||
</div>
|
||||
<div class="menu flex-2 visible-lg breadcrumbs-container">
|
||||
<a
|
||||
class="breadcrumb"
|
||||
routerLink="/ui/projects"
|
||||
translate="top-bar.navigation-items.projects.label"
|
||||
></a>
|
||||
<div *ngIf="appStateService.activeProject" class="breadcrumb">
|
||||
<mat-icon svgIcon="red:arrow-right"></mat-icon>
|
||||
</div>
|
||||
<a
|
||||
*ngIf="appStateService.activeProject"
|
||||
class="breadcrumb"
|
||||
[routerLink]="'/ui/projects/' + appStateService.activeProjectId"
|
||||
>
|
||||
{{ appStateService.activeProject.project.projectName }}
|
||||
</a>
|
||||
<div *ngIf="appStateService.activeFile" class="breadcrumb">
|
||||
<mat-icon svgIcon="red:arrow-right"></mat-icon>
|
||||
</div>
|
||||
<a
|
||||
*ngIf="appStateService.activeFile"
|
||||
class="breadcrumb"
|
||||
[routerLink]="
|
||||
'/ui/projects/' +
|
||||
appStateService.activeProjectId +
|
||||
'/file/' +
|
||||
appStateService.activeFile.fileId
|
||||
"
|
||||
>
|
||||
{{ appStateService.activeFile.filename }}
|
||||
</a>
|
||||
</div>
|
||||
<div class="center flex-1">
|
||||
<redaction-logo></redaction-logo>
|
||||
<div class="app-name" translate="app-name.label"></div>
|
||||
</div>
|
||||
<div class="menu right flex-2">
|
||||
<button [matMenuTriggerFor]="menu" mat-button class="arrow-button">
|
||||
<redaction-initials-avatar
|
||||
color="red-white"
|
||||
size="small"
|
||||
[userId]="user?.id"
|
||||
[withName]="true"
|
||||
></redaction-initials-avatar>
|
||||
<mat-icon svgIcon="red:arrow-down"></mat-icon>
|
||||
</button>
|
||||
<mat-menu #menu="matMenu">
|
||||
<button
|
||||
[matMenuTriggerFor]="language"
|
||||
mat-menu-item
|
||||
translate="top-bar.navigation-items.my-account.children.language.label"
|
||||
></button>
|
||||
<mat-menu #language="matMenu">
|
||||
<button
|
||||
(click)="changeLanguage('en')"
|
||||
mat-menu-item
|
||||
translate="top-bar.navigation-items.my-account.children.language.english.label"
|
||||
></button>
|
||||
<button
|
||||
(click)="changeLanguage('de')"
|
||||
mat-menu-item
|
||||
translate="top-bar.navigation-items.my-account.children.language.german.label"
|
||||
></button>
|
||||
</mat-menu>
|
||||
<button (click)="logout()" mat-menu-item>
|
||||
<mat-icon svgIcon="red:logout"> </mat-icon>
|
||||
<span translate="top-bar.navigation-items.my-account.children.logout.label">
|
||||
</span>
|
||||
</button>
|
||||
</mat-menu>
|
||||
</div>
|
||||
</div>
|
||||
<div class="menu flex-2 visible-lg breadcrumbs-container">
|
||||
<a class="breadcrumb" routerLink="/ui/projects"
|
||||
translate="top-bar.navigation-items.projects.label"></a>
|
||||
<div *ngIf="appStateService.activeProject" class="breadcrumb">
|
||||
<mat-icon svgIcon="red:arrow-right"></mat-icon>
|
||||
</div>
|
||||
<a *ngIf="appStateService.activeProject" class="breadcrumb"
|
||||
[routerLink]="'/ui/projects/'+appStateService.activeProjectId">
|
||||
{{appStateService.activeProject.project.projectName}}
|
||||
</a>
|
||||
<div *ngIf="appStateService.activeFile" class="breadcrumb">
|
||||
<mat-icon svgIcon="red:arrow-right"></mat-icon>
|
||||
</div>
|
||||
<a *ngIf="appStateService.activeFile" class="breadcrumb"
|
||||
[routerLink]="'/ui/projects/'+appStateService.activeProjectId+'/file/'+appStateService.activeFile.fileId">
|
||||
{{appStateService.activeFile.filename}}
|
||||
</a>
|
||||
</div>
|
||||
<div class="center flex-1">
|
||||
<redaction-logo></redaction-logo>
|
||||
<div class="app-name" translate="app-name.label"></div>
|
||||
</div>
|
||||
<div class="menu right flex-2">
|
||||
<button [matMenuTriggerFor]="menu" mat-button class="arrow-button">
|
||||
<redaction-initials-avatar color="red-white" size="small" [userId]="user?.id" [withName]="true"></redaction-initials-avatar>
|
||||
<mat-icon svgIcon="red:arrow-down"></mat-icon>
|
||||
</button>
|
||||
<mat-menu #menu="matMenu">
|
||||
|
||||
<button [matMenuTriggerFor]="language" mat-menu-item
|
||||
translate="top-bar.navigation-items.my-account.children.language.label"></button>
|
||||
<mat-menu #language="matMenu">
|
||||
<button (click)="changeLanguage('en')" mat-menu-item
|
||||
translate="top-bar.navigation-items.my-account.children.language.english.label"></button>
|
||||
<button (click)="changeLanguage('de')" mat-menu-item
|
||||
translate="top-bar.navigation-items.my-account.children.language.german.label"></button>
|
||||
</mat-menu>
|
||||
<button (click)="logout()" mat-menu-item>
|
||||
<mat-icon svgIcon="red:logout">
|
||||
</mat-icon>
|
||||
<span translate="top-bar.navigation-items.my-account.children.logout.label">
|
||||
</span>
|
||||
</button>
|
||||
</mat-menu>
|
||||
</div>
|
||||
</div>
|
||||
<div class="divider"></div>
|
||||
<div class="divider"></div>
|
||||
</div>
|
||||
<div class="red-content">
|
||||
<router-outlet></router-outlet>
|
||||
<router-outlet></router-outlet>
|
||||
</div>
|
||||
|
||||
@ -1,25 +1,25 @@
|
||||
@import "../../../assets/styles/red-variables";
|
||||
@import "../../../assets/styles/red-mixins";
|
||||
@import '../../../assets/styles/red-variables';
|
||||
@import '../../../assets/styles/red-mixins';
|
||||
|
||||
.breadcrumbs-container {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
|
||||
.breadcrumb {
|
||||
text-decoration: none;
|
||||
color: $accent;
|
||||
font-weight: 600;
|
||||
width: fit-content;
|
||||
white-space: nowrap;
|
||||
.breadcrumb {
|
||||
text-decoration: none;
|
||||
color: $accent;
|
||||
font-weight: 600;
|
||||
width: fit-content;
|
||||
white-space: nowrap;
|
||||
|
||||
&:last-child {
|
||||
color: $primary;
|
||||
@include line-clamp(1);
|
||||
&:last-child {
|
||||
color: $primary;
|
||||
@include line-clamp(1);
|
||||
}
|
||||
|
||||
.mat-icon {
|
||||
vertical-align: middle;
|
||||
width: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.mat-icon {
|
||||
vertical-align: middle;
|
||||
width: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,191 +1,276 @@
|
||||
<section>
|
||||
<div class="page-header">
|
||||
<div class="flex-1">
|
||||
<mat-slide-toggle color="primary"
|
||||
labelPosition="after"
|
||||
[(ngModel)]="redactedView">
|
||||
{{ "file-preview.view-toggle.label" | translate }}
|
||||
</mat-slide-toggle>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 filename page-title">
|
||||
{{ appStateService.activeFile.filename }}
|
||||
</div>
|
||||
|
||||
<div class="flex-1 actions-container">
|
||||
<div class="actions-row">
|
||||
<button mat-icon-button (click)="openDeleteFileDialog($event)">
|
||||
<mat-icon svgIcon="red:trash"></mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button>
|
||||
<mat-icon svgIcon="red:report"></mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button (click)="openAssignFileOwnerDialog($event)">
|
||||
<mat-icon svgIcon="red:assign"></mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button (click)="reanalyseFile($event)">
|
||||
<mat-icon svgIcon="red:analyse"></mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button (click)="openFileDetailsDialog($event)">
|
||||
<mat-icon svgIcon="red:info"></mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button color="primary" mat-flat-button class="arrow-button"
|
||||
[matMenuTriggerFor]="downloadMenu">
|
||||
<span translate="file-preview.download.label"></span>
|
||||
<mat-icon svgIcon="red:arrow-down"></mat-icon>
|
||||
</button>
|
||||
<mat-menu #downloadMenu="matMenu" xPosition="before">
|
||||
<div mat-menu-item
|
||||
translate="file-preview.download.dropdown.original.label"
|
||||
(click)="downloadFile('ORIGINAL')"
|
||||
></div>
|
||||
<div mat-menu-item
|
||||
translate="file-preview.download.dropdown.annotated.label"
|
||||
(click)="downloadFile('ANNOTATED')"
|
||||
></div>
|
||||
<div mat-menu-item
|
||||
translate="file-preview.download.dropdown.redacted.label"
|
||||
(click)="downloadFile('REDACTED')"
|
||||
></div>
|
||||
</mat-menu>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex red-content-inner">
|
||||
<div class="left-container">
|
||||
<redaction-pdf-viewer *ngIf="annotatedFileData && redactedFileData"
|
||||
[fileData]="redactedView ? redactedFileData : annotatedFileData "
|
||||
[fileStatus]="appStateService.activeFile"
|
||||
(keyUp)="handleKeyEvent($event)"
|
||||
(pageChanged)="viewerPageChanged($event)"
|
||||
(manualAnnotationRequested)="openManualRedactionDialog($event)"
|
||||
(annotationSelected)="handleAnnotationSelected($event)"
|
||||
(annotationsAdded)="handleAnnotationsAdded($event)"
|
||||
(viewerReady)="viewerReady($event)"></redaction-pdf-viewer>
|
||||
</div>
|
||||
|
||||
<div class="right-fixed-container">
|
||||
<div class="right-title heading"
|
||||
translate="file-preview.tabs.annotations.label">
|
||||
<div>
|
||||
<button color="accent" mat-button class="arrow-button"
|
||||
[matMenuTriggerFor]="filterMenu" [ngClass]="{ overlay: hasActiveFilters }">
|
||||
<span translate="file-preview.filter-menu.label"></span>
|
||||
<mat-icon svgIcon="red:arrow-down"></mat-icon>
|
||||
</button>
|
||||
<div class="dot" *ngIf="hasActiveFilters"></div>
|
||||
<mat-menu #filterMenu="matMenu" xPosition="before">
|
||||
<div class="filter-menu-header">
|
||||
<div class="all-caps-label" translate="file-preview.filter-menu.filter-types.label"></div>
|
||||
<div class="actions">
|
||||
<div class="all-caps-label primary pointer" translate="file-preview.filter-menu.all.label"
|
||||
(click)="setAllFilters(filters, true); applyFilters(); $event.stopPropagation();"></div>
|
||||
<div class="all-caps-label primary pointer" translate="file-preview.filter-menu.none.label"
|
||||
(click)="setAllFilters(filters, false); applyFilters(); $event.stopPropagation();"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngFor="let key of filterKeys()">
|
||||
<div class="mat-menu-item flex" (click)="$event.stopPropagation()">
|
||||
<div class="arrow-wrapper" *ngIf="hasSubsections(filters[key])">
|
||||
<mat-icon *ngIf="expandedFilters[key]"
|
||||
svgIcon="red:arrow-down"
|
||||
color="accent"
|
||||
(click)="setExpanded(key, false, $event)">
|
||||
</mat-icon>
|
||||
<mat-icon *ngIf="!expandedFilters[key]"
|
||||
color="accent"
|
||||
svgIcon="red:arrow-right"
|
||||
(click)="setExpanded(key, true, $event)">
|
||||
</mat-icon>
|
||||
</div>
|
||||
<mat-checkbox [checked]="isChecked(key)"
|
||||
[indeterminate]="isIndeterminate(key)"
|
||||
(change)="setAllFilters(filters[key], $event.checked, hasSubsections(filters[key]) ? null : key)"
|
||||
color="primary">
|
||||
<redaction-annotation-icon [type]="key"></redaction-annotation-icon>
|
||||
{{"file-preview.filter-menu." + key + ".label" | translate }}
|
||||
</mat-checkbox>
|
||||
</div>
|
||||
<div *ngIf="hasSubsections(filters[key]) && expandedFilters[key]">
|
||||
<div *ngFor="let subkey of filterKeys(key)"
|
||||
class="padding-left mat-menu-item"
|
||||
(click)="$event.stopPropagation()"
|
||||
>
|
||||
<mat-checkbox [(ngModel)]="filters[key][subkey]" (change)="applyFilters()" color="primary">
|
||||
<redaction-annotation-icon [type]="key + ' ' + subkey"></redaction-annotation-icon>
|
||||
{{"file-preview.filter-menu." + key + "." + subkey + ".label" | translate }}
|
||||
</mat-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</mat-menu>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="right-content">
|
||||
<div class="pages" [class.activePanel]="pagesPanelActive"
|
||||
tabindex="0"
|
||||
(keyup)="$event.preventDefault()"
|
||||
(keydown)="$event.preventDefault();"
|
||||
#quickNavigation>
|
||||
<div class="page-number pointer"
|
||||
[ngClass]="{ active: pageNumber === activeViewerPage }"
|
||||
*ngFor="let pageNumber of displayedPages"
|
||||
[id]="'quick-nav-page-'+pageNumber" (click)="selectPage(pageNumber)">
|
||||
{{pageNumber}}
|
||||
</div>
|
||||
<div class="page-header">
|
||||
<div class="flex-1">
|
||||
<mat-slide-toggle color="primary" labelPosition="after" [(ngModel)]="redactedView">
|
||||
{{ 'file-preview.view-toggle.label' | translate }}
|
||||
</mat-slide-toggle>
|
||||
</div>
|
||||
|
||||
<div class="annotations" [class.activePanel]="!pagesPanelActive" #annotations
|
||||
tabindex="1"
|
||||
(keyup)="$event.preventDefault()"
|
||||
(keydown)="$event.preventDefault();"
|
||||
>
|
||||
<div *ngFor="let page of displayedPages">
|
||||
<div class="page-separator" attr.anotation-page-header="{{page}}">
|
||||
<span class="all-caps-label"><span translate="page"></span> {{page}}</span>
|
||||
<div class="flex-1 filename page-title">
|
||||
{{ appStateService.activeFile.filename }}
|
||||
</div>
|
||||
|
||||
<div class="flex-1 actions-container">
|
||||
<div class="actions-row">
|
||||
<button mat-icon-button (click)="openDeleteFileDialog($event)">
|
||||
<mat-icon svgIcon="red:trash"></mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button>
|
||||
<mat-icon svgIcon="red:report"></mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button (click)="openAssignFileOwnerDialog($event)">
|
||||
<mat-icon svgIcon="red:assign"></mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button (click)="reanalyseFile($event)">
|
||||
<mat-icon svgIcon="red:analyse"></mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button (click)="openFileDetailsDialog($event)">
|
||||
<mat-icon svgIcon="red:info"></mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div *ngFor="let annotation of displayedAnnotations[page].annotations"
|
||||
class="annotation" attr.annotation-id="{{annotation.Id}}"
|
||||
attr.annotation-page="{{page}}"
|
||||
[ngClass]="{ active: selectedAnnotation === annotation }"
|
||||
(click)="selectAnnotation(annotation)"
|
||||
<button
|
||||
color="primary"
|
||||
mat-flat-button
|
||||
class="arrow-button"
|
||||
[matMenuTriggerFor]="downloadMenu"
|
||||
>
|
||||
|
||||
<redaction-annotation-icon
|
||||
[type]="getType(annotation) + ' ' + getDictionary(annotation)"></redaction-annotation-icon>
|
||||
<div class="flex-1">
|
||||
<div><strong>{{getType(annotation) | translate}}</strong></div>
|
||||
<div><strong><span translate="dictionary"></span>: </strong>{{getDictionary(annotation)}}</div>
|
||||
<div *ngIf="annotation.getContents()"><strong><span translate="content"></span>:
|
||||
</strong>{{annotation.getContents()}}</div>
|
||||
<div *ngFor="let comment of annotation['comments']">
|
||||
<strong><span translate="comment"></span>:</strong> {{ comment.value }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="page-number">
|
||||
{{annotation.getPageNumber()}}
|
||||
</div>
|
||||
|
||||
<div class="annotation-actions" *ngIf="isManuallyAddedAnnotation(annotation)">
|
||||
<button mat-icon-button (click)="acceptSuggestionAnnotation($event, annotation)" class="confirm">
|
||||
<mat-icon svgIcon="red:check-alt"></mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button (click)="suggestRemoveAnnotation($event, annotation)">
|
||||
<mat-icon svgIcon="red:trash"></mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span translate="file-preview.download.label"></span>
|
||||
<mat-icon svgIcon="red:arrow-down"></mat-icon>
|
||||
</button>
|
||||
<mat-menu #downloadMenu="matMenu" xPosition="before">
|
||||
<div
|
||||
mat-menu-item
|
||||
translate="file-preview.download.dropdown.original.label"
|
||||
(click)="downloadFile('ORIGINAL')"
|
||||
></div>
|
||||
<div
|
||||
mat-menu-item
|
||||
translate="file-preview.download.dropdown.annotated.label"
|
||||
(click)="downloadFile('ANNOTATED')"
|
||||
></div>
|
||||
<div
|
||||
mat-menu-item
|
||||
translate="file-preview.download.dropdown.redacted.label"
|
||||
(click)="downloadFile('REDACTED')"
|
||||
></div>
|
||||
</mat-menu>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex red-content-inner">
|
||||
<div class="left-container">
|
||||
<redaction-pdf-viewer
|
||||
*ngIf="annotatedFileData && redactedFileData"
|
||||
[fileData]="redactedView ? redactedFileData : annotatedFileData"
|
||||
[fileStatus]="appStateService.activeFile"
|
||||
(keyUp)="handleKeyEvent($event)"
|
||||
(pageChanged)="viewerPageChanged($event)"
|
||||
(manualAnnotationRequested)="openManualRedactionDialog($event)"
|
||||
(annotationSelected)="handleAnnotationSelected($event)"
|
||||
(annotationsAdded)="handleAnnotationsAdded($event)"
|
||||
(viewerReady)="viewerReady($event)"
|
||||
></redaction-pdf-viewer>
|
||||
</div>
|
||||
|
||||
<div class="right-fixed-container">
|
||||
<div class="right-title heading" translate="file-preview.tabs.annotations.label">
|
||||
<div>
|
||||
<button
|
||||
color="accent"
|
||||
mat-button
|
||||
class="arrow-button"
|
||||
[matMenuTriggerFor]="filterMenu"
|
||||
[ngClass]="{ overlay: hasActiveFilters }"
|
||||
>
|
||||
<span translate="file-preview.filter-menu.label"></span>
|
||||
<mat-icon svgIcon="red:arrow-down"></mat-icon>
|
||||
</button>
|
||||
<div class="dot" *ngIf="hasActiveFilters"></div>
|
||||
<mat-menu #filterMenu="matMenu" xPosition="before">
|
||||
<div class="filter-menu-header">
|
||||
<div
|
||||
class="all-caps-label"
|
||||
translate="file-preview.filter-menu.filter-types.label"
|
||||
></div>
|
||||
<div class="actions">
|
||||
<div
|
||||
class="all-caps-label primary pointer"
|
||||
translate="file-preview.filter-menu.all.label"
|
||||
(click)="
|
||||
setAllFilters(filters, true);
|
||||
applyFilters();
|
||||
$event.stopPropagation()
|
||||
"
|
||||
></div>
|
||||
<div
|
||||
class="all-caps-label primary pointer"
|
||||
translate="file-preview.filter-menu.none.label"
|
||||
(click)="
|
||||
setAllFilters(filters, false);
|
||||
applyFilters();
|
||||
$event.stopPropagation()
|
||||
"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngFor="let key of filterKeys()">
|
||||
<div class="mat-menu-item flex" (click)="$event.stopPropagation()">
|
||||
<div class="arrow-wrapper" *ngIf="hasSubsections(filters[key])">
|
||||
<mat-icon
|
||||
*ngIf="expandedFilters[key]"
|
||||
svgIcon="red:arrow-down"
|
||||
color="accent"
|
||||
(click)="setExpanded(key, false, $event)"
|
||||
>
|
||||
</mat-icon>
|
||||
<mat-icon
|
||||
*ngIf="!expandedFilters[key]"
|
||||
color="accent"
|
||||
svgIcon="red:arrow-right"
|
||||
(click)="setExpanded(key, true, $event)"
|
||||
>
|
||||
</mat-icon>
|
||||
</div>
|
||||
<mat-checkbox
|
||||
[checked]="isChecked(key)"
|
||||
[indeterminate]="isIndeterminate(key)"
|
||||
(change)="
|
||||
setAllFilters(
|
||||
filters[key],
|
||||
$event.checked,
|
||||
hasSubsections(filters[key]) ? null : key
|
||||
)
|
||||
"
|
||||
color="primary"
|
||||
>
|
||||
<redaction-annotation-icon
|
||||
[type]="key"
|
||||
></redaction-annotation-icon>
|
||||
{{ 'file-preview.filter-menu.' + key + '.label' | translate }}
|
||||
</mat-checkbox>
|
||||
</div>
|
||||
<div *ngIf="hasSubsections(filters[key]) && expandedFilters[key]">
|
||||
<div
|
||||
*ngFor="let subkey of filterKeys(key)"
|
||||
class="padding-left mat-menu-item"
|
||||
(click)="$event.stopPropagation()"
|
||||
>
|
||||
<mat-checkbox
|
||||
[(ngModel)]="filters[key][subkey]"
|
||||
(change)="applyFilters()"
|
||||
color="primary"
|
||||
>
|
||||
<redaction-annotation-icon
|
||||
[type]="key + ' ' + subkey"
|
||||
></redaction-annotation-icon>
|
||||
{{
|
||||
'file-preview.filter-menu.' +
|
||||
key +
|
||||
'.' +
|
||||
subkey +
|
||||
'.label' | translate
|
||||
}}
|
||||
</mat-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</mat-menu>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="right-content">
|
||||
<div
|
||||
class="pages"
|
||||
[class.activePanel]="pagesPanelActive"
|
||||
tabindex="0"
|
||||
(keyup)="$event.preventDefault()"
|
||||
(keydown)="$event.preventDefault()"
|
||||
#quickNavigation
|
||||
>
|
||||
<div
|
||||
class="page-number pointer"
|
||||
[ngClass]="{ active: pageNumber === activeViewerPage }"
|
||||
*ngFor="let pageNumber of displayedPages"
|
||||
[id]="'quick-nav-page-' + pageNumber"
|
||||
(click)="selectPage(pageNumber)"
|
||||
>
|
||||
{{ pageNumber }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="annotations"
|
||||
[class.activePanel]="!pagesPanelActive"
|
||||
#annotations
|
||||
tabindex="1"
|
||||
(keyup)="$event.preventDefault()"
|
||||
(keydown)="$event.preventDefault()"
|
||||
>
|
||||
<div *ngFor="let page of displayedPages">
|
||||
<div class="page-separator" attr.anotation-page-header="{{ page }}">
|
||||
<span class="all-caps-label"
|
||||
><span translate="page"></span> {{ page }}</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<div
|
||||
*ngFor="let annotation of displayedAnnotations[page].annotations"
|
||||
class="annotation"
|
||||
attr.annotation-id="{{ annotation.Id }}"
|
||||
attr.annotation-page="{{ page }}"
|
||||
[ngClass]="{ active: selectedAnnotation === annotation }"
|
||||
(click)="selectAnnotation(annotation)"
|
||||
>
|
||||
<redaction-annotation-icon
|
||||
[type]="getType(annotation) + ' ' + getDictionary(annotation)"
|
||||
></redaction-annotation-icon>
|
||||
<div class="flex-1">
|
||||
<div>
|
||||
<strong>{{ getType(annotation) | translate }}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<strong><span translate="dictionary"></span>: </strong
|
||||
>{{ getDictionary(annotation) }}
|
||||
</div>
|
||||
<div *ngIf="annotation.getContents()">
|
||||
<strong><span translate="content"></span>: </strong
|
||||
>{{ annotation.getContents() }}
|
||||
</div>
|
||||
<div *ngFor="let comment of annotation['comments']">
|
||||
<strong><span translate="comment"></span>:</strong>
|
||||
{{ comment.value }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="page-number">
|
||||
{{ annotation.getPageNumber() }}
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="annotation-actions"
|
||||
*ngIf="isManuallyAddedAnnotation(annotation)"
|
||||
>
|
||||
<button
|
||||
mat-icon-button
|
||||
(click)="acceptSuggestionAnnotation($event, annotation)"
|
||||
class="confirm"
|
||||
>
|
||||
<mat-icon svgIcon="red:check-alt"></mat-icon>
|
||||
</button>
|
||||
<button
|
||||
mat-icon-button
|
||||
(click)="suggestRemoveAnnotation($event, annotation)"
|
||||
>
|
||||
<mat-icon svgIcon="red:trash"></mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!--<redaction-full-page-loading-indicator [displayed]="!viewReady"></redaction-full-page-loading-indicator>-->
|
||||
|
||||
@ -1,172 +1,179 @@
|
||||
@import "../../../../assets/styles/red-variables";
|
||||
@import "../../../../assets/styles/red-mixins";
|
||||
@import '../../../../assets/styles/red-variables';
|
||||
@import '../../../../assets/styles/red-mixins';
|
||||
|
||||
redaction-pdf-viewer {
|
||||
position: absolute;
|
||||
width: calc(100vw - #{$right-container-width});
|
||||
height: calc(100vh - 110px);
|
||||
top: 110px;
|
||||
z-index: 2;
|
||||
position: absolute;
|
||||
width: calc(100vw - #{$right-container-width});
|
||||
height: calc(100vh - 110px);
|
||||
top: 110px;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.actions-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.filename {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.right-fixed-container {
|
||||
padding: 0;
|
||||
width: $right-container-width;
|
||||
box-sizing: border-box;
|
||||
|
||||
.right-title {
|
||||
height: 70px;
|
||||
display: flex;
|
||||
border-bottom: 1px solid $separator;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 24px;
|
||||
|
||||
> div {
|
||||
position: relative;
|
||||
|
||||
.dot {
|
||||
background: $primary;
|
||||
height: 10px;
|
||||
width: 10px;
|
||||
border-radius: 50%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.close-icon {
|
||||
height: 14px;
|
||||
width: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.right-content {
|
||||
height: calc(100vh - 110px - 72px);
|
||||
padding: 0;
|
||||
width: $right-container-width;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
|
||||
.pages, .annotations {
|
||||
overflow-y: scroll;
|
||||
@include no-scroll-bar();
|
||||
outline: none;
|
||||
|
||||
&.activePanel {
|
||||
background-color: #FAFAFA;
|
||||
}
|
||||
}
|
||||
|
||||
.pages {
|
||||
border-right: 1px solid $separator;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 16px;
|
||||
min-width: 28px;
|
||||
}
|
||||
|
||||
.annotations {
|
||||
width: 100%;
|
||||
|
||||
.page-separator {
|
||||
border-bottom: 1px solid $separator;
|
||||
height: 40px;
|
||||
box-sizing: border-box;
|
||||
padding: 8px 10px;
|
||||
.right-title {
|
||||
height: 70px;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
background-color: $grey-6;
|
||||
}
|
||||
|
||||
.annotation {
|
||||
border-bottom: 1px solid $separator;
|
||||
padding: 10px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
border-left: 2px solid transparent;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 24px;
|
||||
|
||||
redaction-annotation-icon {
|
||||
margin-top: 6px;
|
||||
}
|
||||
> div {
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
background-color: #F9FAFB;
|
||||
|
||||
.annotation-actions {
|
||||
background: linear-gradient(to right, transparent 0%, #F9FAFB, #F9FAFB, #F9FAFB);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
width: 120px;
|
||||
padding-right: 16px;
|
||||
|
||||
.confirm{
|
||||
color: $green-2;
|
||||
.dot {
|
||||
background: $primary;
|
||||
height: 10px;
|
||||
width: 10px;
|
||||
border-radius: 50%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.close-icon {
|
||||
height: 14px;
|
||||
width: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.right-content {
|
||||
height: calc(100vh - 110px - 72px);
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
|
||||
.pages,
|
||||
.annotations {
|
||||
overflow-y: scroll;
|
||||
@include no-scroll-bar();
|
||||
outline: none;
|
||||
|
||||
&.activePanel {
|
||||
background-color: #fafafa;
|
||||
}
|
||||
}
|
||||
|
||||
.pages {
|
||||
border-right: 1px solid $separator;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 16px;
|
||||
min-width: 28px;
|
||||
}
|
||||
|
||||
.annotations {
|
||||
width: 100%;
|
||||
|
||||
.page-separator {
|
||||
border-bottom: 1px solid $separator;
|
||||
height: 40px;
|
||||
box-sizing: border-box;
|
||||
padding: 8px 10px;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
background-color: $grey-6;
|
||||
}
|
||||
|
||||
.annotation {
|
||||
border-bottom: 1px solid $separator;
|
||||
padding: 10px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
border-left: 2px solid transparent;
|
||||
|
||||
redaction-annotation-icon {
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: #f9fafb;
|
||||
|
||||
.annotation-actions {
|
||||
background: linear-gradient(
|
||||
to right,
|
||||
transparent 0%,
|
||||
#f9fafb,
|
||||
#f9fafb,
|
||||
#f9fafb
|
||||
);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
width: 120px;
|
||||
padding-right: 16px;
|
||||
|
||||
.confirm {
|
||||
color: $green-2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
border-left: 2px solid $primary;
|
||||
}
|
||||
|
||||
.annotation-actions {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
display: none;
|
||||
width: 40px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.page-number {
|
||||
border: 1px solid $separator;
|
||||
padding: 5px 10px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 28px;
|
||||
min-height: 28px;
|
||||
min-width: 14px;
|
||||
opacity: 0.7;
|
||||
font-size: 11px;
|
||||
line-height: 14px;
|
||||
|
||||
&.active {
|
||||
border-left: 2px solid $primary;
|
||||
border: 1px solid $primary;
|
||||
}
|
||||
|
||||
.annotation-actions {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
display: none;
|
||||
width: 40px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.page-number {
|
||||
border: 1px solid $separator;
|
||||
padding: 5px 10px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 28px;
|
||||
min-height: 28px;
|
||||
min-width: 14px;
|
||||
opacity: 0.7;
|
||||
font-size: 11px;
|
||||
line-height: 14px;
|
||||
|
||||
&.active {
|
||||
border: 1px solid $primary;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.filter-menu-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 7px 15px 15px;
|
||||
width: 350px;
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
justify-content: space-between;
|
||||
padding: 7px 15px 15px;
|
||||
width: 350px;
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,12 @@
|
||||
import { ChangeDetectorRef, Component, ElementRef, HostListener, NgZone, OnInit, ViewChild } from '@angular/core';
|
||||
import {
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
ElementRef,
|
||||
HostListener,
|
||||
NgZone,
|
||||
OnInit,
|
||||
ViewChild
|
||||
} from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { ManualRedactionEntry, ReanalysisControllerService } from '@redaction/red-ui-http';
|
||||
import { AppStateService } from '../../../state/app-state.service';
|
||||
@ -17,393 +25,440 @@ import { DialogService } from '../../../dialogs/dialog.service';
|
||||
import { MatDialogRef, MatDialogState } from '@angular/material/dialog';
|
||||
|
||||
@Component({
|
||||
selector: 'redaction-file-preview-screen',
|
||||
templateUrl: './file-preview-screen.component.html',
|
||||
styleUrls: ['./file-preview-screen.component.scss']
|
||||
selector: 'redaction-file-preview-screen',
|
||||
templateUrl: './file-preview-screen.component.html',
|
||||
styleUrls: ['./file-preview-screen.component.scss']
|
||||
})
|
||||
export class FilePreviewScreenComponent implements OnInit {
|
||||
private projectId: string;
|
||||
private _activeViewer: 'ANNOTATED' | 'REDACTED' = 'ANNOTATED';
|
||||
private instance: WebViewerInstance;
|
||||
private _dialogRef: MatDialogRef<any>;
|
||||
|
||||
private projectId: string;
|
||||
private _activeViewer: 'ANNOTATED' | 'REDACTED' = 'ANNOTATED';
|
||||
private instance: WebViewerInstance;
|
||||
private _dialogRef: MatDialogRef<any>;
|
||||
@ViewChild(PdfViewerComponent) private _viewerComponent: PdfViewerComponent;
|
||||
@ViewChild('annotations') private _annotationsElement: ElementRef;
|
||||
@ViewChild('quickNavigation') private _quickNavigationElement: ElementRef;
|
||||
|
||||
@ViewChild(PdfViewerComponent) private _viewerComponent: PdfViewerComponent;
|
||||
@ViewChild('annotations') private _annotationsElement: ElementRef;
|
||||
@ViewChild('quickNavigation') private _quickNavigationElement: ElementRef;
|
||||
public annotatedFileData: Blob;
|
||||
public redactedFileData: Blob;
|
||||
public fileId: string;
|
||||
public annotations: Annotations.Annotation[] = [];
|
||||
public displayedAnnotations: { [key: number]: { annotations: Annotations.Annotation[] } } = {};
|
||||
public selectedAnnotation: Annotations.Annotation;
|
||||
public filters: AnnotationFilters;
|
||||
public expandedFilters: AnnotationFilters = { hint: false };
|
||||
public pagesPanelActive = true;
|
||||
|
||||
public annotatedFileData: Blob;
|
||||
public redactedFileData: Blob;
|
||||
public fileId: string;
|
||||
public annotations: Annotations.Annotation[] = [];
|
||||
public displayedAnnotations: { [key: number]: { annotations: Annotations.Annotation[] } } = {};
|
||||
public selectedAnnotation: Annotations.Annotation;
|
||||
public filters: AnnotationFilters;
|
||||
public expandedFilters: AnnotationFilters = { hint: false };
|
||||
public pagesPanelActive = true;
|
||||
|
||||
constructor(
|
||||
public readonly appStateService: AppStateService,
|
||||
private readonly _changeDetectorRef: ChangeDetectorRef,
|
||||
private readonly _activatedRoute: ActivatedRoute,
|
||||
private readonly _dialogService: DialogService,
|
||||
private readonly _router: Router,
|
||||
private readonly _userService: UserService,
|
||||
private readonly _fileDownloadService: FileDownloadService,
|
||||
private readonly _reanalysisControllerService: ReanalysisControllerService,
|
||||
private readonly _filtersService: FiltersService,
|
||||
private ngZone: NgZone) {
|
||||
this._activatedRoute.params.subscribe(params => {
|
||||
this.projectId = params.projectId;
|
||||
this.fileId = params.fileId;
|
||||
this.appStateService.activateFile(this.projectId, this.fileId);
|
||||
});
|
||||
this.filters = _filtersService.filters;
|
||||
}
|
||||
|
||||
public get user() {
|
||||
return this._userService.user;
|
||||
}
|
||||
|
||||
public filterKeys(key?: string) {
|
||||
if (key) {
|
||||
return Object.keys(this.filters[key]);
|
||||
constructor(
|
||||
public readonly appStateService: AppStateService,
|
||||
private readonly _changeDetectorRef: ChangeDetectorRef,
|
||||
private readonly _activatedRoute: ActivatedRoute,
|
||||
private readonly _dialogService: DialogService,
|
||||
private readonly _router: Router,
|
||||
private readonly _userService: UserService,
|
||||
private readonly _fileDownloadService: FileDownloadService,
|
||||
private readonly _reanalysisControllerService: ReanalysisControllerService,
|
||||
private readonly _filtersService: FiltersService,
|
||||
private ngZone: NgZone
|
||||
) {
|
||||
this._activatedRoute.params.subscribe((params) => {
|
||||
this.projectId = params.projectId;
|
||||
this.fileId = params.fileId;
|
||||
this.appStateService.activateFile(this.projectId, this.fileId);
|
||||
});
|
||||
this.filters = _filtersService.filters;
|
||||
}
|
||||
|
||||
return Object.keys(this.filters);
|
||||
}
|
||||
public get user() {
|
||||
return this._userService.user;
|
||||
}
|
||||
|
||||
public get redactedView() {
|
||||
return this._activeViewer === 'REDACTED';
|
||||
}
|
||||
public filterKeys(key?: string) {
|
||||
if (key) {
|
||||
return Object.keys(this.filters[key]);
|
||||
}
|
||||
|
||||
public set redactedView(value: boolean) {
|
||||
this._activeViewer = value ? 'REDACTED' : 'ANNOTATED';
|
||||
}
|
||||
return Object.keys(this.filters);
|
||||
}
|
||||
|
||||
public ngOnInit(): void {
|
||||
// PDFTRON cache fix
|
||||
localStorage.clear();
|
||||
this._reloadFiles();
|
||||
this.appStateService.fileStatusChanged.subscribe((fileStatus) => {
|
||||
if (fileStatus.fileId === this.fileId) {
|
||||
console.log(fileStatus);
|
||||
public get redactedView() {
|
||||
return this._activeViewer === 'REDACTED';
|
||||
}
|
||||
|
||||
public set redactedView(value: boolean) {
|
||||
this._activeViewer = value ? 'REDACTED' : 'ANNOTATED';
|
||||
}
|
||||
|
||||
public ngOnInit(): void {
|
||||
// PDFTRON cache fix
|
||||
localStorage.clear();
|
||||
this._reloadFiles();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private _reloadFiles() {
|
||||
this._fileDownloadService.loadFile('ANNOTATED', this.fileId, (data) => {
|
||||
this.annotatedFileData = data;
|
||||
}).subscribe(() => {
|
||||
});
|
||||
this._fileDownloadService.loadFile('REDACTED', this.fileId, (data) => {
|
||||
this.redactedFileData = data;
|
||||
}).subscribe(() => {
|
||||
});
|
||||
}
|
||||
|
||||
public openFileDetailsDialog($event: MouseEvent) {
|
||||
this._dialogRef = this._dialogService.openFileDetailsDialog($event, this.appStateService.activeFile);
|
||||
}
|
||||
|
||||
public reanalyseFile($event: MouseEvent) {
|
||||
$event.stopPropagation();
|
||||
this._reanalysisControllerService.reanalyzeFile(this.appStateService.activeProject.project.projectId, this.fileId).subscribe(async () => {
|
||||
await this.appStateService.reloadActiveProjectFiles();
|
||||
});
|
||||
}
|
||||
|
||||
public openDeleteFileDialog($event: MouseEvent) {
|
||||
this._dialogRef = this._dialogService.openDeleteFileDialog($event, this.projectId, this.fileId, () => {
|
||||
this._router.navigate([`/ui/projects/${this.projectId}`]);
|
||||
});
|
||||
}
|
||||
|
||||
public openAssignFileOwnerDialog($event: MouseEvent) {
|
||||
const file = this.appStateService.getFileById(this.projectId, this.fileId);
|
||||
this._dialogRef = this._dialogService.openAssignFileOwnerDialog($event, file);
|
||||
}
|
||||
|
||||
public get activeViewer() {
|
||||
return this.instance;
|
||||
}
|
||||
|
||||
public applyFilters() {
|
||||
this.displayedAnnotations = AnnotationUtils.parseAnnotations(this.annotations, this.filters);
|
||||
}
|
||||
|
||||
public get displayedPages(): number[] {
|
||||
return Object.keys(this.displayedAnnotations).map(key => Number(key));
|
||||
}
|
||||
|
||||
public handleAnnotationSelected(annotation: Annotations.Annotation) {
|
||||
this.selectedAnnotation = annotation;
|
||||
this.scrollToSelectedAnnotation();
|
||||
this._changeDetectorRef.detectChanges();
|
||||
}
|
||||
|
||||
public selectAnnotation(annotation: Annotations.Annotation) {
|
||||
this._viewerComponent.selectAnnotation(annotation);
|
||||
}
|
||||
|
||||
@debounce()
|
||||
private scrollToSelectedAnnotation() {
|
||||
if (!this.selectedAnnotation) {
|
||||
return;
|
||||
this.appStateService.fileStatusChanged.subscribe((fileStatus) => {
|
||||
if (fileStatus.fileId === this.fileId) {
|
||||
console.log(fileStatus);
|
||||
this._reloadFiles();
|
||||
}
|
||||
});
|
||||
}
|
||||
const elements: any[] = this._annotationsElement.nativeElement.querySelectorAll(`div[annotation-id="${this.selectedAnnotation.Id}"].active`);
|
||||
this._scrollToFirstElement(elements);
|
||||
}
|
||||
|
||||
public selectPage(pageNumber: number) {
|
||||
this._viewerComponent.navigateToPage(pageNumber);
|
||||
}
|
||||
|
||||
public openManualRedactionDialog($event: ManualRedactionEntry) {
|
||||
this.ngZone.run(() => {
|
||||
this._dialogRef = this._dialogService.openManualRedactionDialog($event, () => {
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
get activeViewerPage() {
|
||||
return this.instance.docViewer.getCurrentPage();
|
||||
}
|
||||
|
||||
@debounce()
|
||||
private _scrollViews() {
|
||||
this._scrollQuickNavigation();
|
||||
this._scrollAnnotations();
|
||||
}
|
||||
|
||||
private _scrollQuickNavigation() {
|
||||
const elements: any[] = this._quickNavigationElement.nativeElement.querySelectorAll(`#quick-nav-page-${this.activeViewerPage}`);
|
||||
this._scrollToFirstElement(elements);
|
||||
}
|
||||
|
||||
private _scrollAnnotations() {
|
||||
if (this.selectedAnnotation?.getPageNumber() === this.activeViewerPage) {
|
||||
return;
|
||||
private _reloadFiles() {
|
||||
this._fileDownloadService
|
||||
.loadFile('ANNOTATED', this.fileId, (data) => {
|
||||
this.annotatedFileData = data;
|
||||
})
|
||||
.subscribe(() => {});
|
||||
this._fileDownloadService
|
||||
.loadFile('REDACTED', this.fileId, (data) => {
|
||||
this.redactedFileData = data;
|
||||
})
|
||||
.subscribe(() => {});
|
||||
}
|
||||
const elements: any[] = this._annotationsElement.nativeElement.querySelectorAll(`div[anotation-page-header="${this.activeViewerPage}"]`);
|
||||
this._scrollToFirstElement(elements);
|
||||
}
|
||||
|
||||
private _scrollToFirstElement(elements: HTMLElement[]) {
|
||||
if (elements.length > 0) {
|
||||
scrollIntoView(elements[0], {
|
||||
behavior: 'smooth',
|
||||
scrollMode: 'if-needed',
|
||||
block: 'start',
|
||||
inline: 'start'
|
||||
});
|
||||
public openFileDetailsDialog($event: MouseEvent) {
|
||||
this._dialogRef = this._dialogService.openFileDetailsDialog(
|
||||
$event,
|
||||
this.appStateService.activeFile
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public getType(annotation: Annotations.Annotation): string {
|
||||
return AnnotationUtils.getType(annotation);
|
||||
}
|
||||
public reanalyseFile($event: MouseEvent) {
|
||||
$event.stopPropagation();
|
||||
this._reanalysisControllerService
|
||||
.reanalyzeFile(this.appStateService.activeProject.project.projectId, this.fileId)
|
||||
.subscribe(async () => {
|
||||
await this.appStateService.reloadActiveProjectFiles();
|
||||
});
|
||||
}
|
||||
|
||||
public getDictionary(annotation: Annotations.Annotation): string {
|
||||
return AnnotationUtils.getDictionary(annotation);
|
||||
}
|
||||
public openDeleteFileDialog($event: MouseEvent) {
|
||||
this._dialogRef = this._dialogService.openDeleteFileDialog(
|
||||
$event,
|
||||
this.projectId,
|
||||
this.fileId,
|
||||
() => {
|
||||
this._router.navigate([`/ui/projects/${this.projectId}`]);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public acceptSuggestionAnnotation($event: MouseEvent, annotation: Annotations.Annotation) {
|
||||
this.ngZone.run(() => {
|
||||
this._dialogRef = this._dialogService.acceptSuggestionAnnotation($event, annotation, this.projectId, this.fileId);
|
||||
});
|
||||
}
|
||||
public openAssignFileOwnerDialog($event: MouseEvent) {
|
||||
const file = this.appStateService.getFileById(this.projectId, this.fileId);
|
||||
this._dialogRef = this._dialogService.openAssignFileOwnerDialog($event, file);
|
||||
}
|
||||
|
||||
public suggestRemoveAnnotation($event: MouseEvent, annotation: Annotations.Annotation) {
|
||||
this.ngZone.run(() => {
|
||||
this._dialogRef = this._dialogService.suggestRemoveAnnotation($event, annotation, this.projectId, this.fileId);
|
||||
});
|
||||
}
|
||||
public get activeViewer() {
|
||||
return this.instance;
|
||||
}
|
||||
|
||||
public downloadFile(type: FileType | string) {
|
||||
this._fileDownloadService.loadFile(type, this.fileId).subscribe(data => {
|
||||
saveAs(data, this.appStateService.activeFile.filename);
|
||||
});
|
||||
}
|
||||
public applyFilters() {
|
||||
this.displayedAnnotations = AnnotationUtils.parseAnnotations(
|
||||
this.annotations,
|
||||
this.filters
|
||||
);
|
||||
}
|
||||
|
||||
public setAllFilters(filter: AnnotationFilters, value: boolean, rootKey?: string) {
|
||||
if (rootKey) {
|
||||
this.filters[rootKey] = value;
|
||||
} else {
|
||||
public get displayedPages(): number[] {
|
||||
return Object.keys(this.displayedAnnotations).map((key) => Number(key));
|
||||
}
|
||||
|
||||
for (const key of Object.keys(filter)) {
|
||||
if (AnnotationUtils.hasSubsections(filter[key])) {
|
||||
this.setAllFilters(filter[key], value);
|
||||
} else {
|
||||
filter[key] = value;
|
||||
public handleAnnotationSelected(annotation: Annotations.Annotation) {
|
||||
this.selectedAnnotation = annotation;
|
||||
this.scrollToSelectedAnnotation();
|
||||
this._changeDetectorRef.detectChanges();
|
||||
}
|
||||
|
||||
public selectAnnotation(annotation: Annotations.Annotation) {
|
||||
this._viewerComponent.selectAnnotation(annotation);
|
||||
}
|
||||
|
||||
@debounce()
|
||||
private scrollToSelectedAnnotation() {
|
||||
if (!this.selectedAnnotation) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
const elements: any[] = this._annotationsElement.nativeElement.querySelectorAll(
|
||||
`div[annotation-id="${this.selectedAnnotation.Id}"].active`
|
||||
);
|
||||
this._scrollToFirstElement(elements);
|
||||
}
|
||||
|
||||
this.applyFilters();
|
||||
}
|
||||
|
||||
public isChecked(key: string): boolean {
|
||||
return AnnotationUtils.isChecked(this.filters[key]);
|
||||
}
|
||||
|
||||
public isIndeterminate(key: string): boolean {
|
||||
return AnnotationUtils.isIndeterminate(this.filters[key]);
|
||||
}
|
||||
|
||||
public get hasActiveFilters(): boolean {
|
||||
return AnnotationUtils.hasActiveFilters(this.filters);
|
||||
}
|
||||
|
||||
public hasSubsections(filter: AnnotationFilters | boolean) {
|
||||
return AnnotationUtils.hasSubsections(filter);
|
||||
}
|
||||
|
||||
public setExpanded(key: string, value: boolean, $event: MouseEvent) {
|
||||
$event.stopPropagation();
|
||||
this.expandedFilters[key] = value;
|
||||
}
|
||||
|
||||
public isManuallyAddedAnnotation(annotation: Annotations.Annotation) {
|
||||
return annotation.Id.indexOf('request:') >= 0;
|
||||
}
|
||||
|
||||
@HostListener('window:keyup', ['$event'])
|
||||
handleKeyEvent($event: KeyboardEvent) {
|
||||
const keyArray = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'];
|
||||
|
||||
if (!keyArray.includes($event.key) || this._dialogRef?.getState() === MatDialogState.OPEN) {
|
||||
return;
|
||||
public selectPage(pageNumber: number) {
|
||||
this._viewerComponent.navigateToPage(pageNumber);
|
||||
}
|
||||
|
||||
if ($event.key === 'ArrowLeft' || $event.key === 'ArrowRight') {
|
||||
this.pagesPanelActive = !this.pagesPanelActive;
|
||||
this._changeDetectorRef.detectChanges();
|
||||
return;
|
||||
public openManualRedactionDialog($event: ManualRedactionEntry) {
|
||||
this.ngZone.run(() => {
|
||||
this._dialogRef = this._dialogService.openManualRedactionDialog($event, () => {});
|
||||
});
|
||||
}
|
||||
|
||||
if (!this.pagesPanelActive) {
|
||||
this._navigateAnnotations($event);
|
||||
} else {
|
||||
this._navigatePages($event);
|
||||
get activeViewerPage() {
|
||||
return this.instance.docViewer.getCurrentPage();
|
||||
}
|
||||
}
|
||||
|
||||
private _navigateAnnotations($event: KeyboardEvent) {
|
||||
if (!this.selectedAnnotation || this.activeViewerPage !== this.selectedAnnotation.getPageNumber()) {
|
||||
const pageIdx = this.displayedPages.indexOf(this.activeViewerPage);
|
||||
if (pageIdx !== -1) { // Displayed page has annotations
|
||||
this.selectAnnotation(this.displayedAnnotations[this.activeViewerPage].annotations[0]);
|
||||
} else { // Displayed page doesn't have annotations
|
||||
if ($event.key === 'ArrowDown') {
|
||||
const nextPage = this._nextPageWithAnnotations();
|
||||
this.selectAnnotation(this.displayedAnnotations[nextPage].annotations[0]);
|
||||
} else {
|
||||
const prevPage = this._prevPageWithAnnotations();
|
||||
const prevPageAnnotations = this.displayedAnnotations[prevPage].annotations;
|
||||
this.selectAnnotation(prevPageAnnotations[prevPageAnnotations.length - 1]);
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
const page = this.selectedAnnotation.getPageNumber();
|
||||
const pageIdx = this.displayedPages.indexOf(page);
|
||||
const annotationsOnPage = this.displayedAnnotations[page].annotations;
|
||||
const idx = annotationsOnPage.indexOf(this.selectedAnnotation);
|
||||
|
||||
if ($event.key === 'ArrowDown') {
|
||||
if (idx + 1 !== annotationsOnPage.length) { // If not last item in page
|
||||
this.selectAnnotation(annotationsOnPage[idx + 1]);
|
||||
} else if (pageIdx + 1 < this.displayedPages.length) { // If not last page
|
||||
const nextPageAnnotations = this.displayedAnnotations[this.displayedPages[pageIdx + 1]].annotations;
|
||||
this.selectAnnotation(nextPageAnnotations[0]);
|
||||
}
|
||||
} else {
|
||||
if (idx !== 0) { // If not first item in page
|
||||
this.selectAnnotation(annotationsOnPage[idx - 1]);
|
||||
} else if (pageIdx) { // If not first page
|
||||
const prevPageAnnotations = this.displayedAnnotations[this.displayedPages[pageIdx - 1]].annotations;
|
||||
this.selectAnnotation(prevPageAnnotations[prevPageAnnotations.length - 1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _navigatePages($event: KeyboardEvent) {
|
||||
const pageIdx = this.displayedPages.indexOf(this.activeViewerPage);
|
||||
|
||||
if ($event.key === 'ArrowDown') {
|
||||
if (pageIdx !== -1) { // If active page has annotations
|
||||
if (pageIdx !== this.displayedPages.length - 1) {
|
||||
this.selectPage(this.displayedPages[pageIdx + 1]);
|
||||
}
|
||||
} else { // If active page doesn't have annotations
|
||||
const nextPage = this._nextPageWithAnnotations();
|
||||
if (nextPage) {
|
||||
this.selectPage(nextPage);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (pageIdx !== -1) { // If active page has annotations
|
||||
if (pageIdx !== 0) {
|
||||
this.selectPage(this.displayedPages[pageIdx - 1]);
|
||||
}
|
||||
} else { // If active page doesn't have annotations
|
||||
const prevPage = this._prevPageWithAnnotations();
|
||||
if (prevPage) {
|
||||
this.selectPage(prevPage);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _nextPageWithAnnotations() {
|
||||
let idx = 0;
|
||||
for (const page of this.displayedPages) {
|
||||
if (page > this.activeViewerPage) {
|
||||
break;
|
||||
}
|
||||
++idx;
|
||||
}
|
||||
return idx < this.displayedPages.length ? this.displayedPages[idx] : null;
|
||||
}
|
||||
|
||||
private _prevPageWithAnnotations() {
|
||||
let idx = this.displayedPages.length - 1;
|
||||
for (const page of this.displayedPages.reverse()) {
|
||||
if (page < this.activeViewerPage) {
|
||||
this.selectPage(this.displayedPages[idx]);
|
||||
@debounce()
|
||||
private _scrollViews() {
|
||||
this._scrollQuickNavigation();
|
||||
this._scrollAnnotations();
|
||||
break;
|
||||
}
|
||||
--idx;
|
||||
}
|
||||
return idx >= 0 ? this.displayedPages[idx] : null;
|
||||
}
|
||||
|
||||
viewerPageChanged($event: number) {
|
||||
this._scrollViews();
|
||||
this._changeDetectorRef.detectChanges();
|
||||
}
|
||||
private _scrollQuickNavigation() {
|
||||
const elements: any[] = this._quickNavigationElement.nativeElement.querySelectorAll(
|
||||
`#quick-nav-page-${this.activeViewerPage}`
|
||||
);
|
||||
this._scrollToFirstElement(elements);
|
||||
}
|
||||
|
||||
viewerReady($event: WebViewerInstance) {
|
||||
this.instance = $event;
|
||||
}
|
||||
private _scrollAnnotations() {
|
||||
if (this.selectedAnnotation?.getPageNumber() === this.activeViewerPage) {
|
||||
return;
|
||||
}
|
||||
const elements: any[] = this._annotationsElement.nativeElement.querySelectorAll(
|
||||
`div[anotation-page-header="${this.activeViewerPage}"]`
|
||||
);
|
||||
this._scrollToFirstElement(elements);
|
||||
}
|
||||
|
||||
handleAnnotationsAdded(annotations: Annotations.Annotation[]) {
|
||||
private _scrollToFirstElement(elements: HTMLElement[]) {
|
||||
if (elements.length > 0) {
|
||||
scrollIntoView(elements[0], {
|
||||
behavior: 'smooth',
|
||||
scrollMode: 'if-needed',
|
||||
block: 'start',
|
||||
inline: 'start'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// handle comments
|
||||
annotations.forEach(a => {
|
||||
a['comments'] = a['Mi'] ? a['Mi'].map(m => {
|
||||
return { value: m.eC };
|
||||
}) : [];
|
||||
});
|
||||
public getType(annotation: Annotations.Annotation): string {
|
||||
return AnnotationUtils.getType(annotation);
|
||||
}
|
||||
|
||||
AnnotationUtils.addAnnotations(this.annotations, annotations);
|
||||
this.applyFilters();
|
||||
this._changeDetectorRef.detectChanges();
|
||||
}
|
||||
public getDictionary(annotation: Annotations.Annotation): string {
|
||||
return AnnotationUtils.getDictionary(annotation);
|
||||
}
|
||||
|
||||
public acceptSuggestionAnnotation($event: MouseEvent, annotation: Annotations.Annotation) {
|
||||
this.ngZone.run(() => {
|
||||
this._dialogRef = this._dialogService.acceptSuggestionAnnotation(
|
||||
$event,
|
||||
annotation,
|
||||
this.projectId,
|
||||
this.fileId
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public suggestRemoveAnnotation($event: MouseEvent, annotation: Annotations.Annotation) {
|
||||
this.ngZone.run(() => {
|
||||
this._dialogRef = this._dialogService.suggestRemoveAnnotation(
|
||||
$event,
|
||||
annotation,
|
||||
this.projectId,
|
||||
this.fileId
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public downloadFile(type: FileType | string) {
|
||||
this._fileDownloadService.loadFile(type, this.fileId).subscribe((data) => {
|
||||
saveAs(data, this.appStateService.activeFile.filename);
|
||||
});
|
||||
}
|
||||
|
||||
public setAllFilters(filter: AnnotationFilters, value: boolean, rootKey?: string) {
|
||||
if (rootKey) {
|
||||
this.filters[rootKey] = value;
|
||||
} else {
|
||||
for (const key of Object.keys(filter)) {
|
||||
if (AnnotationUtils.hasSubsections(filter[key])) {
|
||||
this.setAllFilters(filter[key], value);
|
||||
} else {
|
||||
filter[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.applyFilters();
|
||||
}
|
||||
|
||||
public isChecked(key: string): boolean {
|
||||
return AnnotationUtils.isChecked(this.filters[key]);
|
||||
}
|
||||
|
||||
public isIndeterminate(key: string): boolean {
|
||||
return AnnotationUtils.isIndeterminate(this.filters[key]);
|
||||
}
|
||||
|
||||
public get hasActiveFilters(): boolean {
|
||||
return AnnotationUtils.hasActiveFilters(this.filters);
|
||||
}
|
||||
|
||||
public hasSubsections(filter: AnnotationFilters | boolean) {
|
||||
return AnnotationUtils.hasSubsections(filter);
|
||||
}
|
||||
|
||||
public setExpanded(key: string, value: boolean, $event: MouseEvent) {
|
||||
$event.stopPropagation();
|
||||
this.expandedFilters[key] = value;
|
||||
}
|
||||
|
||||
public isManuallyAddedAnnotation(annotation: Annotations.Annotation) {
|
||||
return annotation.Id.indexOf('request:') >= 0;
|
||||
}
|
||||
|
||||
@HostListener('window:keyup', ['$event'])
|
||||
handleKeyEvent($event: KeyboardEvent) {
|
||||
const keyArray = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'];
|
||||
|
||||
if (!keyArray.includes($event.key) || this._dialogRef?.getState() === MatDialogState.OPEN) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($event.key === 'ArrowLeft' || $event.key === 'ArrowRight') {
|
||||
this.pagesPanelActive = !this.pagesPanelActive;
|
||||
this._changeDetectorRef.detectChanges();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.pagesPanelActive) {
|
||||
this._navigateAnnotations($event);
|
||||
} else {
|
||||
this._navigatePages($event);
|
||||
}
|
||||
}
|
||||
|
||||
private _navigateAnnotations($event: KeyboardEvent) {
|
||||
if (
|
||||
!this.selectedAnnotation ||
|
||||
this.activeViewerPage !== this.selectedAnnotation.getPageNumber()
|
||||
) {
|
||||
const pageIdx = this.displayedPages.indexOf(this.activeViewerPage);
|
||||
if (pageIdx !== -1) {
|
||||
// Displayed page has annotations
|
||||
this.selectAnnotation(
|
||||
this.displayedAnnotations[this.activeViewerPage].annotations[0]
|
||||
);
|
||||
} else {
|
||||
// Displayed page doesn't have annotations
|
||||
if ($event.key === 'ArrowDown') {
|
||||
const nextPage = this._nextPageWithAnnotations();
|
||||
this.selectAnnotation(this.displayedAnnotations[nextPage].annotations[0]);
|
||||
} else {
|
||||
const prevPage = this._prevPageWithAnnotations();
|
||||
const prevPageAnnotations = this.displayedAnnotations[prevPage].annotations;
|
||||
this.selectAnnotation(prevPageAnnotations[prevPageAnnotations.length - 1]);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const page = this.selectedAnnotation.getPageNumber();
|
||||
const pageIdx = this.displayedPages.indexOf(page);
|
||||
const annotationsOnPage = this.displayedAnnotations[page].annotations;
|
||||
const idx = annotationsOnPage.indexOf(this.selectedAnnotation);
|
||||
|
||||
if ($event.key === 'ArrowDown') {
|
||||
if (idx + 1 !== annotationsOnPage.length) {
|
||||
// If not last item in page
|
||||
this.selectAnnotation(annotationsOnPage[idx + 1]);
|
||||
} else if (pageIdx + 1 < this.displayedPages.length) {
|
||||
// If not last page
|
||||
const nextPageAnnotations = this.displayedAnnotations[
|
||||
this.displayedPages[pageIdx + 1]
|
||||
].annotations;
|
||||
this.selectAnnotation(nextPageAnnotations[0]);
|
||||
}
|
||||
} else {
|
||||
if (idx !== 0) {
|
||||
// If not first item in page
|
||||
this.selectAnnotation(annotationsOnPage[idx - 1]);
|
||||
} else if (pageIdx) {
|
||||
// If not first page
|
||||
const prevPageAnnotations = this.displayedAnnotations[
|
||||
this.displayedPages[pageIdx - 1]
|
||||
].annotations;
|
||||
this.selectAnnotation(prevPageAnnotations[prevPageAnnotations.length - 1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _navigatePages($event: KeyboardEvent) {
|
||||
const pageIdx = this.displayedPages.indexOf(this.activeViewerPage);
|
||||
|
||||
if ($event.key === 'ArrowDown') {
|
||||
if (pageIdx !== -1) {
|
||||
// If active page has annotations
|
||||
if (pageIdx !== this.displayedPages.length - 1) {
|
||||
this.selectPage(this.displayedPages[pageIdx + 1]);
|
||||
}
|
||||
} else {
|
||||
// If active page doesn't have annotations
|
||||
const nextPage = this._nextPageWithAnnotations();
|
||||
if (nextPage) {
|
||||
this.selectPage(nextPage);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (pageIdx !== -1) {
|
||||
// If active page has annotations
|
||||
if (pageIdx !== 0) {
|
||||
this.selectPage(this.displayedPages[pageIdx - 1]);
|
||||
}
|
||||
} else {
|
||||
// If active page doesn't have annotations
|
||||
const prevPage = this._prevPageWithAnnotations();
|
||||
if (prevPage) {
|
||||
this.selectPage(prevPage);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _nextPageWithAnnotations() {
|
||||
let idx = 0;
|
||||
for (const page of this.displayedPages) {
|
||||
if (page > this.activeViewerPage) {
|
||||
break;
|
||||
}
|
||||
++idx;
|
||||
}
|
||||
return idx < this.displayedPages.length ? this.displayedPages[idx] : null;
|
||||
}
|
||||
|
||||
private _prevPageWithAnnotations() {
|
||||
let idx = this.displayedPages.length - 1;
|
||||
for (const page of this.displayedPages.reverse()) {
|
||||
if (page < this.activeViewerPage) {
|
||||
this.selectPage(this.displayedPages[idx]);
|
||||
this._scrollAnnotations();
|
||||
break;
|
||||
}
|
||||
--idx;
|
||||
}
|
||||
return idx >= 0 ? this.displayedPages[idx] : null;
|
||||
}
|
||||
|
||||
viewerPageChanged($event: number) {
|
||||
this._scrollViews();
|
||||
this._changeDetectorRef.detectChanges();
|
||||
}
|
||||
|
||||
viewerReady($event: WebViewerInstance) {
|
||||
this.instance = $event;
|
||||
}
|
||||
|
||||
handleAnnotationsAdded(annotations: Annotations.Annotation[]) {
|
||||
// handle comments
|
||||
annotations.forEach((a) => {
|
||||
a['comments'] = a['Mi']
|
||||
? a['Mi'].map((m) => {
|
||||
return { value: m.eC };
|
||||
})
|
||||
: [];
|
||||
});
|
||||
|
||||
AnnotationUtils.addAnnotations(this.annotations, annotations);
|
||||
this.applyFilters();
|
||||
this._changeDetectorRef.detectChanges();
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,252 +1,249 @@
|
||||
import {
|
||||
AfterViewInit,
|
||||
Component,
|
||||
ElementRef,
|
||||
EventEmitter,
|
||||
Input,
|
||||
OnChanges,
|
||||
OnInit,
|
||||
Output,
|
||||
SimpleChanges,
|
||||
ViewChild
|
||||
AfterViewInit,
|
||||
Component,
|
||||
ElementRef,
|
||||
EventEmitter,
|
||||
Input,
|
||||
OnChanges,
|
||||
OnInit,
|
||||
Output,
|
||||
SimpleChanges,
|
||||
ViewChild
|
||||
} from '@angular/core';
|
||||
import {AppConfigKey, AppConfigService} from '../../../app-config/app-config.service';
|
||||
import {FileStatus, ManualRedactionEntry, Rectangle} from '@redaction/red-ui-http';
|
||||
import WebViewer, {Annotations, WebViewerInstance} from '@pdftron/webviewer';
|
||||
import {TranslateService} from '@ngx-translate/core';
|
||||
import {FileDownloadService} from '../service/file-download.service';
|
||||
import { AppConfigKey, AppConfigService } from '../../../app-config/app-config.service';
|
||||
import { FileStatus, ManualRedactionEntry, Rectangle } from '@redaction/red-ui-http';
|
||||
import WebViewer, { Annotations, WebViewerInstance } from '@pdftron/webviewer';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { FileDownloadService } from '../service/file-download.service';
|
||||
|
||||
export interface ViewerState {
|
||||
|
||||
displayMode?: any;
|
||||
layoutMode?: any;
|
||||
pageNumber?: any;
|
||||
scrollTop?: any;
|
||||
scrollLeft?: any;
|
||||
zoom?: any;
|
||||
leftPanelState?: any;
|
||||
|
||||
displayMode?: any;
|
||||
layoutMode?: any;
|
||||
pageNumber?: any;
|
||||
scrollTop?: any;
|
||||
scrollLeft?: any;
|
||||
zoom?: any;
|
||||
leftPanelState?: any;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'redaction-pdf-viewer',
|
||||
templateUrl: './pdf-viewer.component.html',
|
||||
styleUrls: ['./pdf-viewer.component.scss']
|
||||
selector: 'redaction-pdf-viewer',
|
||||
templateUrl: './pdf-viewer.component.html',
|
||||
styleUrls: ['./pdf-viewer.component.scss']
|
||||
})
|
||||
export class PdfViewerComponent implements OnInit, AfterViewInit, OnChanges {
|
||||
private _viewerState: ViewerState = null; // no initial state
|
||||
|
||||
private _viewerState: ViewerState = null; // no initial state
|
||||
@Input() fileData: Blob;
|
||||
@Input() fileStatus: FileStatus;
|
||||
|
||||
@Input() fileData: Blob;
|
||||
@Input() fileStatus: FileStatus;
|
||||
@Output() fileReady = new EventEmitter();
|
||||
@Output() annotationsAdded = new EventEmitter<Annotations.Annotation[]>();
|
||||
@Output() annotationSelected = new EventEmitter<Annotations.Annotation>();
|
||||
@Output() manualAnnotationRequested = new EventEmitter<ManualRedactionEntry>();
|
||||
@Output() pageChanged = new EventEmitter<number>();
|
||||
@Output() keyUp = new EventEmitter<KeyboardEvent>();
|
||||
|
||||
@Output() viewerReady = new EventEmitter<WebViewerInstance>();
|
||||
|
||||
@Output() fileReady = new EventEmitter();
|
||||
@Output() annotationsAdded = new EventEmitter<Annotations.Annotation[]>();
|
||||
@Output() annotationSelected = new EventEmitter<Annotations.Annotation>();
|
||||
@Output() manualAnnotationRequested = new EventEmitter<ManualRedactionEntry>();
|
||||
@Output() pageChanged = new EventEmitter<number>();
|
||||
@Output() keyUp = new EventEmitter<KeyboardEvent>();
|
||||
@Input() flag = false;
|
||||
|
||||
@Output() viewerReady = new EventEmitter<WebViewerInstance>();
|
||||
@ViewChild('viewer', { static: true }) viewer: ElementRef;
|
||||
instance: WebViewerInstance;
|
||||
|
||||
@Input() flag = false;
|
||||
constructor(
|
||||
private readonly _translateService: TranslateService,
|
||||
private readonly _fileDownloadService: FileDownloadService,
|
||||
private readonly _appConfigService: AppConfigService
|
||||
) {}
|
||||
|
||||
@ViewChild('viewer', {static: true}) viewer: ElementRef;
|
||||
instance: WebViewerInstance;
|
||||
|
||||
constructor(private readonly _translateService: TranslateService,
|
||||
private readonly _fileDownloadService: FileDownloadService,
|
||||
private readonly _appConfigService: AppConfigService) {
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this._restoreViewerState = this._restoreViewerState.bind(this);
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
if (changes.fileData && !changes.fileData.firstChange) {
|
||||
this._changeDocument();
|
||||
ngOnInit() {
|
||||
this._restoreViewerState = this._restoreViewerState.bind(this);
|
||||
}
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this._loadViewer(this.fileData);
|
||||
}
|
||||
|
||||
private _loadViewer(pdfBlob: any) {
|
||||
const license = this._appConfigService.getConfig(AppConfigKey.PDFTRON_LICENSE);
|
||||
WebViewer({
|
||||
licenseKey: license,
|
||||
isReadOnly: true,
|
||||
path: '/assets/wv-resources'
|
||||
}, this.viewer.nativeElement).then(instance => {
|
||||
this.instance = instance;
|
||||
this._disableElements();
|
||||
this._configureTextPopup();
|
||||
this._configureHeader();
|
||||
instance.annotManager.on('annotationChanged', (annotations, action) => {
|
||||
if (action === 'add') {
|
||||
this.annotationsAdded.emit(annotations);
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
if (changes.fileData && !changes.fileData.firstChange) {
|
||||
this._changeDocument();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
instance.annotManager.on('annotationSelected', ((annotationList, action) => {
|
||||
if (action === 'deselected') {
|
||||
this.annotationSelected.emit(null);
|
||||
ngAfterViewInit(): void {
|
||||
this._loadViewer(this.fileData);
|
||||
}
|
||||
|
||||
private _loadViewer(pdfBlob: any) {
|
||||
const license = this._appConfigService.getConfig(AppConfigKey.PDFTRON_LICENSE);
|
||||
WebViewer(
|
||||
{
|
||||
licenseKey: license,
|
||||
isReadOnly: true,
|
||||
path: '/assets/wv-resources'
|
||||
},
|
||||
this.viewer.nativeElement
|
||||
).then((instance) => {
|
||||
this.instance = instance;
|
||||
this._disableElements();
|
||||
this._configureTextPopup();
|
||||
this._configureHeader();
|
||||
instance.annotManager.on('annotationChanged', (annotations, action) => {
|
||||
if (action === 'add') {
|
||||
this.annotationsAdded.emit(annotations);
|
||||
}
|
||||
});
|
||||
|
||||
instance.annotManager.on('annotationSelected', (annotationList, action) => {
|
||||
if (action === 'deselected') {
|
||||
this.annotationSelected.emit(null);
|
||||
} else {
|
||||
this.annotationSelected.emit(annotationList[0]);
|
||||
}
|
||||
});
|
||||
|
||||
instance.docViewer.on('pageComplete', (p) => {
|
||||
this.pageChanged.emit(p);
|
||||
});
|
||||
|
||||
instance.docViewer.on('documentLoaded', this._restoreViewerState);
|
||||
|
||||
instance.docViewer.on('keyDown', ($event) => {
|
||||
if ($event.key.startsWith('Arrow')) {
|
||||
$event.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
instance.docViewer.on('keyUp', ($event) => {
|
||||
if ($event.key.startsWith('Arrow')) {
|
||||
this.keyUp.emit($event);
|
||||
}
|
||||
});
|
||||
|
||||
instance.loadDocument(pdfBlob, {
|
||||
filename: this.fileStatus ? this.fileStatus.filename : 'document.pdf'
|
||||
});
|
||||
|
||||
this.viewerReady.emit(instance);
|
||||
});
|
||||
}
|
||||
|
||||
private _disableElements() {
|
||||
this.instance.disableElements([
|
||||
'textHighlightToolButton',
|
||||
'textUnderlineToolButton',
|
||||
'textSquigglyToolButton',
|
||||
'textStrikeoutToolButton',
|
||||
'linkButton',
|
||||
'toggleNotesButton',
|
||||
'notesPanel',
|
||||
'thumbnailControl',
|
||||
'documentControl',
|
||||
'ribbons',
|
||||
'rotateClockwiseButton',
|
||||
'rotateCounterClockwiseButton'
|
||||
]);
|
||||
}
|
||||
|
||||
private _configureTextPopup() {
|
||||
this.instance.textPopup.add(<any>{
|
||||
type: 'actionButton',
|
||||
img: '/assets/icons/general/add-redaction.svg',
|
||||
title: this._translateService.instant(
|
||||
'pdf-viewer.text-popup.actions.suggestion-redaction.label'
|
||||
),
|
||||
onClick: () => {
|
||||
const selectedQuads = this.instance.docViewer.getSelectedTextQuads();
|
||||
const text = this.instance.docViewer.getSelectedText();
|
||||
const entry: ManualRedactionEntry = { positions: [] };
|
||||
for (const key of Object.keys(selectedQuads)) {
|
||||
for (const quad of selectedQuads[key]) {
|
||||
entry.positions.push(this.toPosition(parseInt(key, 10), quad));
|
||||
}
|
||||
}
|
||||
entry.value = text;
|
||||
this.manualAnnotationRequested.emit(entry);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private toPosition(page: number, selectedQuad: any): Rectangle {
|
||||
const pageHeight = this.instance.docViewer.getPageHeight(page);
|
||||
const height = selectedQuad.y2 - selectedQuad.y4;
|
||||
return {
|
||||
page: page,
|
||||
topLeft: {
|
||||
x: selectedQuad.x4,
|
||||
y: pageHeight - (selectedQuad.y4 + height)
|
||||
},
|
||||
height: height,
|
||||
width: selectedQuad.x3 - selectedQuad.x4
|
||||
};
|
||||
}
|
||||
|
||||
private _configureHeader() {
|
||||
this.instance.setToolbarGroup('toolbarGroup-View');
|
||||
}
|
||||
|
||||
public selectAnnotation(annotation: Annotations.Annotation) {
|
||||
this.instance.annotManager.deselectAllAnnotations();
|
||||
this.instance.annotManager.selectAnnotation(annotation);
|
||||
this.navigateToPage(annotation.getPageNumber());
|
||||
}
|
||||
|
||||
public navigateToPage(pageNumber: number) {
|
||||
const activePage = this.instance.docViewer.getCurrentPage();
|
||||
if (activePage !== pageNumber) {
|
||||
this.instance.docViewer.displayPageLocation(pageNumber, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
private _changeDocument() {
|
||||
// sync layout and display mode
|
||||
|
||||
const instance = this.instance;
|
||||
const docViewer = this.instance.docViewer;
|
||||
|
||||
const lastScrolledViewerScrollElement = docViewer.getScrollViewElement();
|
||||
|
||||
const viewerState: ViewerState = {
|
||||
displayMode: docViewer.getDisplayModeManager().getDisplayMode().mode,
|
||||
layoutMode: instance.getLayoutMode(),
|
||||
pageNumber: instance.docViewer.getCurrentPage(),
|
||||
scrollLeft: lastScrolledViewerScrollElement.scrollLeft,
|
||||
scrollTop: lastScrolledViewerScrollElement.scrollTop,
|
||||
zoom: docViewer.getZoom(),
|
||||
leftPanelState: instance.isElementOpen('leftPanel')
|
||||
};
|
||||
|
||||
this.instance.loadDocument(this.fileData, {
|
||||
filename: this.fileStatus ? this.fileStatus.filename : 'document.pdf'
|
||||
});
|
||||
|
||||
this._viewerState = viewerState;
|
||||
}
|
||||
|
||||
private _restoreViewerState() {
|
||||
this._restoreState(this._viewerState, this.instance);
|
||||
}
|
||||
|
||||
private _restoreState(viewerState: ViewerState, instance: WebViewerInstance) {
|
||||
if (this._viewerState) {
|
||||
instance.docViewer.setCurrentPage(viewerState.pageNumber);
|
||||
instance.setLayoutMode(viewerState.layoutMode);
|
||||
const instanceDisplayMode = instance.docViewer.getDisplayModeManager().getDisplayMode();
|
||||
instanceDisplayMode.mode = viewerState.displayMode;
|
||||
instance.docViewer.getDisplayModeManager().setDisplayMode(instanceDisplayMode);
|
||||
// Synchronize zoom - needs to be done before scrolling
|
||||
instance.docViewer.zoomTo(viewerState.zoom);
|
||||
const viewerScrollElement = instance.docViewer.getScrollViewElement();
|
||||
viewerScrollElement.scrollTo(viewerState.scrollLeft, viewerState.scrollTop);
|
||||
|
||||
if (viewerState.leftPanelState) {
|
||||
instance.openElements(['leftPanel']);
|
||||
} else {
|
||||
instance.closeElements(['leftPanel']);
|
||||
}
|
||||
} else {
|
||||
this.annotationSelected.emit(annotationList[0]);
|
||||
// viewer init
|
||||
this.instance.setFitMode('FitPage');
|
||||
}
|
||||
}));
|
||||
|
||||
instance.docViewer.on('pageComplete', (p) => {
|
||||
this.pageChanged.emit(p);
|
||||
});
|
||||
|
||||
instance.docViewer.on('documentLoaded', this._restoreViewerState);
|
||||
|
||||
instance.docViewer.on('keyDown', ($event) => {
|
||||
if ($event.key.startsWith('Arrow')) {
|
||||
$event.preventDefault();
|
||||
}
|
||||
})
|
||||
|
||||
instance.docViewer.on('keyUp', ($event) => {
|
||||
if ($event.key.startsWith('Arrow')) {
|
||||
this.keyUp.emit($event);
|
||||
}
|
||||
})
|
||||
|
||||
instance.loadDocument(pdfBlob, {filename: this.fileStatus ? this.fileStatus.filename : 'document.pdf'});
|
||||
|
||||
this.viewerReady.emit(instance);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
private _disableElements() {
|
||||
this.instance.disableElements([
|
||||
'textHighlightToolButton',
|
||||
'textUnderlineToolButton',
|
||||
'textSquigglyToolButton',
|
||||
'textStrikeoutToolButton',
|
||||
'linkButton',
|
||||
'toggleNotesButton',
|
||||
'notesPanel',
|
||||
'thumbnailControl',
|
||||
'documentControl',
|
||||
'ribbons',
|
||||
'rotateClockwiseButton',
|
||||
'rotateCounterClockwiseButton'
|
||||
]);
|
||||
}
|
||||
|
||||
private _configureTextPopup() {
|
||||
this.instance.textPopup.add(<any>{
|
||||
type: 'actionButton',
|
||||
img: '/assets/icons/general/add-redaction.svg',
|
||||
title: this._translateService.instant('pdf-viewer.text-popup.actions.suggestion-redaction.label'),
|
||||
onClick: () => {
|
||||
const selectedQuads = this.instance.docViewer.getSelectedTextQuads();
|
||||
const text = this.instance.docViewer.getSelectedText();
|
||||
const entry: ManualRedactionEntry = {positions: []};
|
||||
for (const key of Object.keys(selectedQuads)) {
|
||||
for (const quad of selectedQuads[key]) {
|
||||
entry.positions.push(this.toPosition(parseInt(key, 10), quad));
|
||||
}
|
||||
}
|
||||
entry.value = text;
|
||||
this.manualAnnotationRequested.emit(entry);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private toPosition(page: number, selectedQuad: any): Rectangle {
|
||||
|
||||
const pageHeight = this.instance.docViewer.getPageHeight(page);
|
||||
const height = selectedQuad.y2 - selectedQuad.y4;
|
||||
return {
|
||||
page: page,
|
||||
topLeft: {
|
||||
x: selectedQuad.x4,
|
||||
y: pageHeight - (selectedQuad.y4 + height)
|
||||
},
|
||||
height: height,
|
||||
width: selectedQuad.x3 - selectedQuad.x4
|
||||
};
|
||||
}
|
||||
|
||||
private _configureHeader() {
|
||||
this.instance.setToolbarGroup('toolbarGroup-View');
|
||||
}
|
||||
|
||||
|
||||
public selectAnnotation(annotation: Annotations.Annotation) {
|
||||
this.instance.annotManager.deselectAllAnnotations();
|
||||
this.instance.annotManager.selectAnnotation(annotation);
|
||||
this.navigateToPage(annotation.getPageNumber());
|
||||
}
|
||||
|
||||
public navigateToPage(pageNumber: number) {
|
||||
const activePage = this.instance.docViewer.getCurrentPage();
|
||||
if (activePage !== pageNumber) {
|
||||
this.instance.docViewer.displayPageLocation(pageNumber, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private _changeDocument() {
|
||||
// sync layout and display mode
|
||||
|
||||
|
||||
const instance = this.instance;
|
||||
const docViewer = this.instance.docViewer;
|
||||
|
||||
const lastScrolledViewerScrollElement = docViewer.getScrollViewElement();
|
||||
|
||||
const viewerState: ViewerState = {
|
||||
displayMode: docViewer.getDisplayModeManager().getDisplayMode().mode,
|
||||
layoutMode: instance.getLayoutMode(),
|
||||
pageNumber: instance.docViewer.getCurrentPage(),
|
||||
scrollLeft: lastScrolledViewerScrollElement.scrollLeft,
|
||||
scrollTop: lastScrolledViewerScrollElement.scrollTop,
|
||||
zoom: docViewer.getZoom(),
|
||||
leftPanelState: instance.isElementOpen('leftPanel')
|
||||
}
|
||||
|
||||
this.instance.loadDocument(this.fileData, {filename: this.fileStatus ? this.fileStatus.filename : 'document.pdf'});
|
||||
|
||||
this._viewerState = viewerState;
|
||||
|
||||
}
|
||||
|
||||
private _restoreViewerState() {
|
||||
this._restoreState(this._viewerState, this.instance);
|
||||
}
|
||||
|
||||
private _restoreState(viewerState: ViewerState, instance: WebViewerInstance) {
|
||||
if (this._viewerState) {
|
||||
instance.docViewer.setCurrentPage(viewerState.pageNumber);
|
||||
instance.setLayoutMode(viewerState.layoutMode);
|
||||
const instanceDisplayMode = instance.docViewer.getDisplayModeManager().getDisplayMode();
|
||||
instanceDisplayMode.mode = viewerState.displayMode;
|
||||
instance.docViewer.getDisplayModeManager().setDisplayMode(instanceDisplayMode);
|
||||
// Synchronize zoom - needs to be done before scrolling
|
||||
instance.docViewer.zoomTo(viewerState.zoom);
|
||||
const viewerScrollElement = instance.docViewer.getScrollViewElement();
|
||||
viewerScrollElement.scrollTo(viewerState.scrollLeft, viewerState.scrollTop);
|
||||
|
||||
if (viewerState.leftPanelState) {
|
||||
instance.openElements(['leftPanel']);
|
||||
} else {
|
||||
instance.closeElements(['leftPanel']);
|
||||
}
|
||||
} else {
|
||||
// viewer init
|
||||
this.instance.setFitMode('FitPage');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
@ -1,151 +1,188 @@
|
||||
<div class="page-header">
|
||||
<div class="filters flex-row">
|
||||
<div translate="filters.filter-by.label"></div>
|
||||
<button mat-button translate="filters.status.label">
|
||||
<mat-icon svgIcon="red:status"></mat-icon>
|
||||
<div class="filters flex-row">
|
||||
<div translate="filters.filter-by.label"></div>
|
||||
<button mat-button translate="filters.status.label">
|
||||
<mat-icon svgIcon="red:status"></mat-icon>
|
||||
</button>
|
||||
<button mat-button translate="filters.people.label">
|
||||
<mat-icon svgIcon="red:user"></mat-icon>
|
||||
</button>
|
||||
<button mat-button translate="filters.due-date.label">
|
||||
<mat-icon svgIcon="red:lightning"></mat-icon>
|
||||
</button>
|
||||
<button mat-button translate="filters.created-on.label">
|
||||
<mat-icon svgIcon="red:calendar"></mat-icon>
|
||||
</button>
|
||||
<button mat-button translate="filters.project.label">
|
||||
<mat-icon svgIcon="red:folder"></mat-icon>
|
||||
</button>
|
||||
<button mat-button translate="filters.document.label">
|
||||
<mat-icon svgIcon="red:document"></mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
(click)="openAddProjectDialog()"
|
||||
color="primary"
|
||||
mat-flat-button
|
||||
class="add-project-btn"
|
||||
>
|
||||
<mat-icon svgIcon="red:plus"> </mat-icon>
|
||||
<span translate="project-listing.add-new.label"></span>
|
||||
</button>
|
||||
<button mat-button translate="filters.people.label">
|
||||
<mat-icon svgIcon="red:user"></mat-icon>
|
||||
</button>
|
||||
<button mat-button translate="filters.due-date.label">
|
||||
<mat-icon svgIcon="red:lightning"></mat-icon>
|
||||
</button>
|
||||
<button mat-button translate="filters.created-on.label">
|
||||
<mat-icon svgIcon="red:calendar"></mat-icon>
|
||||
</button>
|
||||
<button mat-button translate="filters.project.label">
|
||||
<mat-icon svgIcon="red:folder"></mat-icon>
|
||||
</button>
|
||||
<button mat-button translate="filters.document.label">
|
||||
<mat-icon svgIcon="red:document"></mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
<button (click)="openAddProjectDialog()" color="primary" mat-flat-button class="add-project-btn">
|
||||
<mat-icon svgIcon="red:plus">
|
||||
</mat-icon>
|
||||
<span translate="project-listing.add-new.label"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex red-content-inner">
|
||||
<div class="left-container">
|
||||
<div class="table-header">
|
||||
<span class="all-caps-label">
|
||||
{{'project-listing.table-header.title.label'| translate:{ length: appStateService.allProjects?.length || 0 } }}
|
||||
</span>
|
||||
<div class="actions">
|
||||
<div translate="project-listing.table-header.bulk-select.label"></div>
|
||||
<mat-form-field appearance="none" class="red-select">
|
||||
<mat-select [(ngModel)]="sortingOption" panelClass="red-select-panel">
|
||||
<mat-option *ngFor="let option of sortingOptions" [value]="option">
|
||||
{{option.label | translate }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="left-container">
|
||||
<div class="table-header">
|
||||
<span class="all-caps-label">
|
||||
{{
|
||||
'project-listing.table-header.title.label'
|
||||
| translate: { length: appStateService.allProjects?.length || 0 }
|
||||
}}
|
||||
</span>
|
||||
<div class="actions">
|
||||
<div translate="project-listing.table-header.bulk-select.label"></div>
|
||||
<mat-form-field appearance="none" class="red-select">
|
||||
<mat-select [(ngModel)]="sortingOption" panelClass="red-select-panel">
|
||||
<mat-option *ngFor="let option of sortingOptions" [value]="option">
|
||||
{{ option.label | translate }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid-container">
|
||||
<div class="table-col-name">
|
||||
<span
|
||||
class="small-label"
|
||||
translate="project-listing.table-col-names.name.label"
|
||||
></span>
|
||||
</div>
|
||||
<div class="table-col-name">
|
||||
<span
|
||||
class="small-label"
|
||||
translate="project-listing.table-col-names.owner.label"
|
||||
></span>
|
||||
</div>
|
||||
<div class="table-col-name flex-end">
|
||||
<span
|
||||
class="small-label"
|
||||
translate="project-listing.table-col-names.status.label"
|
||||
></span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="no-data"
|
||||
*ngIf="appStateService.allProjects?.length === 0"
|
||||
translate="project-listing.no-projects.label"
|
||||
></div>
|
||||
|
||||
<div
|
||||
class="table-item pointer"
|
||||
[routerLink]="'/ui/projects/' + pw.project.projectId"
|
||||
*ngFor="
|
||||
let pw of appStateService.allProjects
|
||||
| sortBy: sortingOption.order:sortingOption.column
|
||||
"
|
||||
>
|
||||
<div>
|
||||
<div class="table-item-title table-item-title--large">
|
||||
{{ pw.project.projectName }}
|
||||
</div>
|
||||
<div class="small-label stats-subtitle">
|
||||
<div>
|
||||
<mat-icon svgIcon="red:document"></mat-icon>
|
||||
{{ documentCount(pw) }}
|
||||
</div>
|
||||
<div>
|
||||
<mat-icon svgIcon="red:user"></mat-icon>
|
||||
{{ userCount(pw) }}
|
||||
</div>
|
||||
<div>
|
||||
<mat-icon svgIcon="red:calendar"></mat-icon>
|
||||
{{ pw.project.date | date: 'mediumDate' }}
|
||||
</div>
|
||||
<div *ngIf="pw.project.dueDate">
|
||||
<mat-icon svgIcon="red:lightning"></mat-icon>
|
||||
{{ pw.project.dueDate | date: 'mediumDate' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<redaction-initials-avatar
|
||||
[userId]="pw.project.ownerId"
|
||||
withName="true"
|
||||
></redaction-initials-avatar>
|
||||
</div>
|
||||
<div class="status-container">
|
||||
<redaction-status-bar
|
||||
[config]="getProjectStatusConfig(pw)"
|
||||
></redaction-status-bar>
|
||||
|
||||
<div class="action-buttons">
|
||||
<button
|
||||
mat-icon-button
|
||||
color="accent"
|
||||
(click)="openDeleteProjectDialog($event, pw.project)"
|
||||
[matTooltip]="'project-listing.delete.action.label' | translate"
|
||||
>
|
||||
<mat-icon svgIcon="red:trash"></mat-icon>
|
||||
</button>
|
||||
<button
|
||||
mat-icon-button
|
||||
color="accent"
|
||||
(click)="openProjectDetailsDialog($event, pw)"
|
||||
[matTooltip]="'project-listing.report.action.label' | translate"
|
||||
>
|
||||
<mat-icon svgIcon="red:report"></mat-icon>
|
||||
</button>
|
||||
<button
|
||||
color="accent"
|
||||
(click)="openAssignProjectOwnerDialog($event, pw.project)"
|
||||
mat-icon-button
|
||||
[matTooltip]="'project-listing.assign.action.label' | translate"
|
||||
>
|
||||
<mat-icon svgIcon="red:assign"></mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid-container">
|
||||
<div class="table-col-name">
|
||||
<span class="small-label" translate="project-listing.table-col-names.name.label"></span>
|
||||
</div>
|
||||
<div class="table-col-name">
|
||||
<span class="small-label" translate="project-listing.table-col-names.owner.label"></span>
|
||||
</div>
|
||||
<div class="table-col-name flex-end">
|
||||
<span class="small-label" translate="project-listing.table-col-names.status.label"></span>
|
||||
</div>
|
||||
|
||||
<div class="no-data" *ngIf="appStateService.allProjects?.length === 0 "
|
||||
translate="project-listing.no-projects.label"></div>
|
||||
|
||||
<div class="table-item pointer"
|
||||
[routerLink]="'/ui/projects/'+pw.project.projectId"
|
||||
*ngFor="let pw of appStateService.allProjects | sortBy:sortingOption.order:sortingOption.column"
|
||||
>
|
||||
<div class="right-fixed-container">
|
||||
<div>
|
||||
<div class="table-item-title table-item-title--large">
|
||||
{{pw.project.projectName}}
|
||||
</div>
|
||||
<div class="small-label stats-subtitle">
|
||||
<div>
|
||||
<mat-icon svgIcon="red:document"></mat-icon>
|
||||
{{documentCount(pw)}}</div>
|
||||
<div>
|
||||
<mat-icon svgIcon="red:user"></mat-icon>
|
||||
{{userCount(pw)}}</div>
|
||||
<div>
|
||||
<mat-icon svgIcon="red:calendar"></mat-icon>
|
||||
{{pw.project.date | date:'mediumDate'}}
|
||||
<redaction-simple-doughnut-chart
|
||||
[config]="projectsChartData"
|
||||
[strokeWidth]="15"
|
||||
[subtitle]="'project-listing.stats.charts.projects.label'"
|
||||
></redaction-simple-doughnut-chart>
|
||||
|
||||
<div class="project-stats-container">
|
||||
<div class="project-stats-item">
|
||||
<mat-icon svgIcon="red:document"></mat-icon>
|
||||
<div>
|
||||
<div class="heading">{{ totalPages }}</div>
|
||||
<div translate="project-listing.stats.analyzed-pages.label"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="project-stats-item">
|
||||
<mat-icon svgIcon="red:user"></mat-icon>
|
||||
<div>
|
||||
<div class="heading">{{ totalPeople }}</div>
|
||||
<div translate="project-listing.stats.total-people.label"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="pw.project.dueDate">
|
||||
<mat-icon svgIcon="red:lightning"></mat-icon>
|
||||
{{pw.project.dueDate | date:'mediumDate'}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<redaction-initials-avatar [userId]="pw.project.ownerId"
|
||||
withName="true"
|
||||
></redaction-initials-avatar>
|
||||
<redaction-simple-doughnut-chart
|
||||
[config]="documentsChartData"
|
||||
[strokeWidth]="15"
|
||||
[subtitle]="'project-listing.stats.charts.total-documents.label'"
|
||||
></redaction-simple-doughnut-chart>
|
||||
</div>
|
||||
<div class="status-container">
|
||||
<redaction-status-bar
|
||||
[config]="getProjectStatusConfig(pw)"
|
||||
></redaction-status-bar>
|
||||
|
||||
<div class="action-buttons">
|
||||
<button mat-icon-button color="accent" (click)="openDeleteProjectDialog($event,pw.project)"
|
||||
[matTooltip]="'project-listing.delete.action.label'|translate">
|
||||
<mat-icon svgIcon="red:trash"></mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button color="accent" (click)="openProjectDetailsDialog($event,pw)"
|
||||
[matTooltip]="'project-listing.report.action.label'|translate">
|
||||
<mat-icon svgIcon="red:report"></mat-icon>
|
||||
</button>
|
||||
<button color="accent" (click)="openAssignProjectOwnerDialog($event,pw.project)" mat-icon-button
|
||||
[matTooltip]="'project-listing.assign.action.label'|translate">
|
||||
<mat-icon svgIcon="red:assign"></mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="right-fixed-container">
|
||||
<div>
|
||||
<redaction-simple-doughnut-chart [config]="projectsChartData"
|
||||
[strokeWidth]="15"
|
||||
[subtitle]="'project-listing.stats.charts.projects.label'"
|
||||
></redaction-simple-doughnut-chart>
|
||||
|
||||
<div class="project-stats-container">
|
||||
<div class="project-stats-item">
|
||||
<mat-icon svgIcon="red:document"></mat-icon>
|
||||
<div>
|
||||
<div class="heading">{{totalPages}}</div>
|
||||
<div translate="project-listing.stats.analyzed-pages.label"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="project-stats-item">
|
||||
<mat-icon svgIcon="red:user"></mat-icon>
|
||||
<div>
|
||||
<div class="heading">{{totalPeople}}</div>
|
||||
<div translate="project-listing.stats.total-people.label"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<redaction-simple-doughnut-chart [config]="documentsChartData"
|
||||
[strokeWidth]="15"
|
||||
[subtitle]="'project-listing.stats.charts.total-documents.label'"
|
||||
></redaction-simple-doughnut-chart>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -1,58 +1,58 @@
|
||||
@import "../../../assets/styles/red-mixins";
|
||||
@import '../../../assets/styles/red-mixins';
|
||||
|
||||
.add-project-btn {
|
||||
mat-icon {
|
||||
width: 14px;
|
||||
margin-right: 10px;
|
||||
color: $white;
|
||||
}
|
||||
mat-icon {
|
||||
width: 14px;
|
||||
margin-right: 10px;
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
|
||||
.left-container {
|
||||
width: calc(100vw - #{$right-container-width} - 130px);
|
||||
width: calc(100vw - #{$right-container-width} - 130px);
|
||||
|
||||
.grid-container {
|
||||
grid-template-columns: 2fr 1fr auto;
|
||||
}
|
||||
.grid-container {
|
||||
grid-template-columns: 2fr 1fr auto;
|
||||
}
|
||||
|
||||
.stats-subtitle {
|
||||
margin-top: 6px;
|
||||
}
|
||||
.stats-subtitle {
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.status-container {
|
||||
width: 160px;
|
||||
}
|
||||
.status-container {
|
||||
width: 160px;
|
||||
}
|
||||
}
|
||||
|
||||
.right-fixed-container {
|
||||
display: flex;
|
||||
width: 470px;
|
||||
padding-top: 50px;
|
||||
|
||||
> div {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
width: 470px;
|
||||
padding-top: 50px;
|
||||
|
||||
.project-stats-container {
|
||||
width: fit-content;
|
||||
|
||||
.project-stats-item {
|
||||
display: flex;
|
||||
width: fit-content;
|
||||
gap: 5px;
|
||||
margin-top: 25px;
|
||||
|
||||
&:first-of-type {
|
||||
margin-top: 50px;
|
||||
}
|
||||
|
||||
mat-icon {
|
||||
height: 16px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
> div {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.project-stats-container {
|
||||
width: fit-content;
|
||||
|
||||
.project-stats-item {
|
||||
display: flex;
|
||||
width: fit-content;
|
||||
gap: 5px;
|
||||
margin-top: 25px;
|
||||
|
||||
&:first-of-type {
|
||||
margin-top: 50px;
|
||||
}
|
||||
|
||||
mat-icon {
|
||||
height: 16px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,112 +1,120 @@
|
||||
import {Component, OnInit} from '@angular/core';
|
||||
import {Project} from '@redaction/red-ui-http';
|
||||
import {AppStateService, ProjectWrapper} from '../../state/app-state.service';
|
||||
import {UserService} from '../../user/user.service';
|
||||
import {DoughnutChartConfig} from '../../components/simple-doughnut-chart/simple-doughnut-chart.component';
|
||||
import {SortingOption} from '../../utils/types';
|
||||
import {groupBy} from '../../utils/functions';
|
||||
import {DialogService} from '../../dialogs/dialog.service';
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Project } from '@redaction/red-ui-http';
|
||||
import { AppStateService, ProjectWrapper } from '../../state/app-state.service';
|
||||
import { UserService } from '../../user/user.service';
|
||||
import { DoughnutChartConfig } from '../../components/simple-doughnut-chart/simple-doughnut-chart.component';
|
||||
import { SortingOption } from '../../utils/types';
|
||||
import { groupBy } from '../../utils/functions';
|
||||
import { DialogService } from '../../dialogs/dialog.service';
|
||||
|
||||
@Component({
|
||||
selector: 'redaction-project-listing-screen',
|
||||
templateUrl: './project-listing-screen.component.html',
|
||||
styleUrls: ['./project-listing-screen.component.scss']
|
||||
selector: 'redaction-project-listing-screen',
|
||||
templateUrl: './project-listing-screen.component.html',
|
||||
styleUrls: ['./project-listing-screen.component.scss']
|
||||
})
|
||||
export class ProjectListingScreenComponent implements OnInit {
|
||||
public projectsChartData: DoughnutChartConfig [] = [];
|
||||
public documentsChartData: DoughnutChartConfig [] = [];
|
||||
public sortingOptions: SortingOption[] = [
|
||||
{label: 'project-listing.sorting.recent.label', order: 'desc', column: 'projectDate'},
|
||||
{label: 'project-listing.sorting.alphabetically.label', order: 'asc', column: 'project.projectName'}
|
||||
];
|
||||
public sortingOption: SortingOption = this.sortingOptions[0];
|
||||
|
||||
constructor(
|
||||
public readonly appStateService: AppStateService,
|
||||
private readonly _userService: UserService,
|
||||
private readonly _dialogService: DialogService
|
||||
) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.appStateService.reset();
|
||||
this._calculateData();
|
||||
this.appStateService.fileStatusChanged.subscribe(() => {
|
||||
this._calculateData();
|
||||
})
|
||||
}
|
||||
|
||||
private _calculateData() {
|
||||
this.projectsChartData = [
|
||||
{value: this.activeProjects, color: 'ACTIVE', label: 'active'},
|
||||
{value: this.inactiveProjects, color: 'DELETED', label: 'archived'}
|
||||
public projectsChartData: DoughnutChartConfig[] = [];
|
||||
public documentsChartData: DoughnutChartConfig[] = [];
|
||||
public sortingOptions: SortingOption[] = [
|
||||
{ label: 'project-listing.sorting.recent.label', order: 'desc', column: 'projectDate' },
|
||||
{
|
||||
label: 'project-listing.sorting.alphabetically.label',
|
||||
order: 'asc',
|
||||
column: 'project.projectName'
|
||||
}
|
||||
];
|
||||
const groups = groupBy(this.appStateService.aggregatedFiles, 'status');
|
||||
this.documentsChartData = [];
|
||||
for (const key of Object.keys(groups)) {
|
||||
this.documentsChartData.push({value: groups[key].length, color: key, label: key});
|
||||
public sortingOption: SortingOption = this.sortingOptions[0];
|
||||
|
||||
constructor(
|
||||
public readonly appStateService: AppStateService,
|
||||
private readonly _userService: UserService,
|
||||
private readonly _dialogService: DialogService
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.appStateService.reset();
|
||||
this._calculateData();
|
||||
this.appStateService.fileStatusChanged.subscribe(() => {
|
||||
this._calculateData();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public get user() {
|
||||
return this._userService.user;
|
||||
}
|
||||
private _calculateData() {
|
||||
this.projectsChartData = [
|
||||
{ value: this.activeProjects, color: 'ACTIVE', label: 'active' },
|
||||
{ value: this.inactiveProjects, color: 'DELETED', label: 'archived' }
|
||||
];
|
||||
const groups = groupBy(this.appStateService.aggregatedFiles, 'status');
|
||||
this.documentsChartData = [];
|
||||
for (const key of Object.keys(groups)) {
|
||||
this.documentsChartData.push({ value: groups[key].length, color: key, label: key });
|
||||
}
|
||||
}
|
||||
|
||||
public get totalPages() {
|
||||
return this.appStateService.totalAnalysedPages;
|
||||
}
|
||||
public get user() {
|
||||
return this._userService.user;
|
||||
}
|
||||
|
||||
public get totalPeople() {
|
||||
return this.appStateService.totalPeople;
|
||||
}
|
||||
public get totalPages() {
|
||||
return this.appStateService.totalAnalysedPages;
|
||||
}
|
||||
|
||||
public get activeProjects() {
|
||||
return this.appStateService.allProjects.reduce((i, p) => i + (p.project.status === Project.StatusEnum.ACTIVE ? 1 : 0), 0);
|
||||
}
|
||||
public get totalPeople() {
|
||||
return this.appStateService.totalPeople;
|
||||
}
|
||||
|
||||
public get inactiveProjects() {
|
||||
return this.appStateService.allProjects.length - this.activeProjects;
|
||||
}
|
||||
public get activeProjects() {
|
||||
return this.appStateService.allProjects.reduce(
|
||||
(i, p) => i + (p.project.status === Project.StatusEnum.ACTIVE ? 1 : 0),
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
public documentCount(project: ProjectWrapper) {
|
||||
return project.files.length;
|
||||
}
|
||||
public get inactiveProjects() {
|
||||
return this.appStateService.allProjects.length - this.activeProjects;
|
||||
}
|
||||
|
||||
public userCount(project: ProjectWrapper) {
|
||||
return 1;
|
||||
}
|
||||
public documentCount(project: ProjectWrapper) {
|
||||
return project.files.length;
|
||||
}
|
||||
|
||||
public openAddProjectDialog(): void {
|
||||
this._dialogService.openAddProjectDialog(() => {
|
||||
this._calculateData();
|
||||
});
|
||||
}
|
||||
public userCount(project: ProjectWrapper) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
public openDeleteProjectDialog($event: MouseEvent, project: Project) {
|
||||
this._dialogService.openDeleteProjectDialog($event, project, () => {
|
||||
this._calculateData();
|
||||
});
|
||||
}
|
||||
public openAddProjectDialog(): void {
|
||||
this._dialogService.openAddProjectDialog(() => {
|
||||
this._calculateData();
|
||||
});
|
||||
}
|
||||
|
||||
public openProjectDetailsDialog($event: MouseEvent, project: ProjectWrapper) {
|
||||
this._dialogService.openProjectDetailsDialog($event, project);
|
||||
}
|
||||
public openDeleteProjectDialog($event: MouseEvent, project: Project) {
|
||||
this._dialogService.openDeleteProjectDialog($event, project, () => {
|
||||
this._calculateData();
|
||||
});
|
||||
}
|
||||
|
||||
public openAssignProjectOwnerDialog($event: MouseEvent, project: Project) {
|
||||
this._dialogService.openAssignProjectMembersAndOwnerDialog($event, project);
|
||||
}
|
||||
public openProjectDetailsDialog($event: MouseEvent, project: ProjectWrapper) {
|
||||
this._dialogService.openProjectDetailsDialog($event, project);
|
||||
}
|
||||
|
||||
public getProjectStatusConfig(pw: ProjectWrapper) {
|
||||
const obj = pw.files.reduce((acc, file) => {
|
||||
const status = file.status;
|
||||
if (!acc[status]) {
|
||||
acc[status] = 1;
|
||||
} else {
|
||||
acc[status]++;
|
||||
}
|
||||
return acc;
|
||||
}, {})
|
||||
public openAssignProjectOwnerDialog($event: MouseEvent, project: Project) {
|
||||
this._dialogService.openAssignProjectMembersAndOwnerDialog($event, project);
|
||||
}
|
||||
|
||||
return Object.keys(obj).sort().map(status => ({length: obj[status], color: status}));
|
||||
}
|
||||
public getProjectStatusConfig(pw: ProjectWrapper) {
|
||||
const obj = pw.files.reduce((acc, file) => {
|
||||
const status = file.status;
|
||||
if (!acc[status]) {
|
||||
acc[status] = 1;
|
||||
} else {
|
||||
acc[status]++;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
return Object.keys(obj)
|
||||
.sort()
|
||||
.map((status) => ({ length: obj[status], color: status }));
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,231 +1,317 @@
|
||||
<div *ngIf="!appStateService.activeProject"
|
||||
[innerHTML]="'project-overview.no-project.label' | translate:{projectId: activeProject.projectId}"
|
||||
class="heading-l"></div>
|
||||
<div
|
||||
*ngIf="!appStateService.activeProject"
|
||||
[innerHTML]="
|
||||
'project-overview.no-project.label' | translate: { projectId: activeProject.projectId }
|
||||
"
|
||||
class="heading-l"
|
||||
></div>
|
||||
|
||||
<div *ngIf="appStateService.activeProject" class="page-header">
|
||||
<div class="filters flex-row">
|
||||
<div translate="filters.filter-by.label"></div>
|
||||
<button mat-button translate="filters.status.label">
|
||||
<mat-icon svgIcon="red:status"></mat-icon>
|
||||
</button>
|
||||
<button mat-button translate="filters.people.label">
|
||||
<mat-icon svgIcon="red:user"></mat-icon>
|
||||
</button>
|
||||
<button mat-button translate="filters.due-date.label">
|
||||
<mat-icon svgIcon="red:lightning"></mat-icon>
|
||||
</button>
|
||||
<button mat-button translate="filters.created-on.label">
|
||||
<mat-icon svgIcon="red:calendar"></mat-icon>
|
||||
</button>
|
||||
<button mat-button translate="filters.project.label">
|
||||
<mat-icon svgIcon="red:folder"></mat-icon>
|
||||
</button>
|
||||
<button mat-button translate="filters.document.label">
|
||||
<mat-icon svgIcon="red:document"></mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
<button (click)="fileInput.click()" color="primary" mat-flat-button
|
||||
translate="project-overview.upload-document.label"></button>
|
||||
<input #fileInput (change)="uploadFiles($event.target.files)" class="file-upload-input" multiple="true"
|
||||
type="file">
|
||||
<div class="filters flex-row">
|
||||
<div translate="filters.filter-by.label"></div>
|
||||
<button mat-button translate="filters.status.label">
|
||||
<mat-icon svgIcon="red:status"></mat-icon>
|
||||
</button>
|
||||
<button mat-button translate="filters.people.label">
|
||||
<mat-icon svgIcon="red:user"></mat-icon>
|
||||
</button>
|
||||
<button mat-button translate="filters.due-date.label">
|
||||
<mat-icon svgIcon="red:lightning"></mat-icon>
|
||||
</button>
|
||||
<button mat-button translate="filters.created-on.label">
|
||||
<mat-icon svgIcon="red:calendar"></mat-icon>
|
||||
</button>
|
||||
<button mat-button translate="filters.project.label">
|
||||
<mat-icon svgIcon="red:folder"></mat-icon>
|
||||
</button>
|
||||
<button mat-button translate="filters.document.label">
|
||||
<mat-icon svgIcon="red:document"></mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
(click)="fileInput.click()"
|
||||
color="primary"
|
||||
mat-flat-button
|
||||
translate="project-overview.upload-document.label"
|
||||
></button>
|
||||
<input
|
||||
#fileInput
|
||||
(change)="uploadFiles($event.target.files)"
|
||||
class="file-upload-input"
|
||||
multiple="true"
|
||||
type="file"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex red-content-inner">
|
||||
<div class="left-container">
|
||||
<div class="table-header">
|
||||
<div class="select-all-container">
|
||||
<div class="select-oval" [class.active]="areAllFilesSelected()" (click)="toggleSelectAll()"></div>
|
||||
<span class="all-caps-label">
|
||||
{{'project-overview.table-header.title.label'| translate:{ length: appStateService.activeProject?.files.length || 0 } }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<div translate="project-overview.table-header.bulk-select.label"></div>
|
||||
<mat-form-field appearance="none" class="red-select">
|
||||
<mat-select [(ngModel)]="sortingOption" panelClass="red-select-panel">
|
||||
<mat-option *ngFor="let option of sortingOptions" [value]="option">
|
||||
{{option.label | translate }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="left-container">
|
||||
<div class="table-header">
|
||||
<div class="select-all-container">
|
||||
<div
|
||||
class="select-oval"
|
||||
[class.active]="areAllFilesSelected()"
|
||||
(click)="toggleSelectAll()"
|
||||
></div>
|
||||
<span class="all-caps-label">
|
||||
{{
|
||||
'project-overview.table-header.title.label'
|
||||
| translate
|
||||
: { length: appStateService.activeProject?.files.length || 0 }
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<div translate="project-overview.table-header.bulk-select.label"></div>
|
||||
<mat-form-field appearance="none" class="red-select">
|
||||
<mat-select [(ngModel)]="sortingOption" panelClass="red-select-panel">
|
||||
<mat-option *ngFor="let option of sortingOptions" [value]="option">
|
||||
{{ option.label | translate }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid-container">
|
||||
<!-- Table column names-->
|
||||
<div class="table-col-name"></div>
|
||||
|
||||
<div class="table-col-name">
|
||||
<span
|
||||
class="small-label"
|
||||
translate="project-overview.table-col-names.name.label"
|
||||
></span>
|
||||
</div>
|
||||
|
||||
<div class="table-col-name pointer" (click)="toggleSortByAddedOn()">
|
||||
<span
|
||||
class="small-label"
|
||||
translate="project-overview.table-col-names.added-on.label"
|
||||
></span>
|
||||
<div class="sort-arrows-container">
|
||||
<mat-icon
|
||||
svgIcon="red:arrow-up"
|
||||
[color]="sortingOption === sortingOptions[0] ? 'primary' : 'currentColor'"
|
||||
></mat-icon>
|
||||
<mat-icon
|
||||
svgIcon="red:arrow-down"
|
||||
[color]="sortingOption === sortingOptions[1] ? 'primary' : 'currentColor'"
|
||||
></mat-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-col-name">
|
||||
<span
|
||||
class="small-label"
|
||||
translate="project-overview.table-col-names.needs-work.label"
|
||||
></span>
|
||||
</div>
|
||||
|
||||
<div class="table-col-name">
|
||||
<span
|
||||
class="small-label"
|
||||
translate="project-overview.table-col-names.assigned-to.label"
|
||||
></span>
|
||||
</div>
|
||||
|
||||
<div class="table-col-name flex-end">
|
||||
<span
|
||||
class="small-label"
|
||||
translate="project-overview.table-col-names.status.label"
|
||||
></span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="table-item"
|
||||
[class.pointer]="canOpenFile(fileStatus.status)"
|
||||
*ngFor="
|
||||
let fileStatus of appStateService.activeProject.files
|
||||
| sortBy: sortingOption.order:sortingOption.column;
|
||||
trackBy: fileId
|
||||
"
|
||||
[routerLink]="
|
||||
canOpenFile(fileStatus.status)
|
||||
? ['/ui/projects/' + activeProject.projectId + '/file/' + fileStatus.fileId]
|
||||
: []
|
||||
"
|
||||
>
|
||||
<div class="pr-0">
|
||||
<div
|
||||
class="select-oval"
|
||||
[class.active]="isFileSelected(fileStatus)"
|
||||
(click)="toggleFileSelected($event, fileStatus)"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="table-item-title"
|
||||
[matTooltip]="'[' + fileStatus.status + '] ' + fileStatus.filename"
|
||||
>
|
||||
{{ fileStatus.filename }}
|
||||
</div>
|
||||
|
||||
<div class="small-label">
|
||||
{{ fileStatus.added | date: 'd MMM. yyyy, hh:mm a' }}
|
||||
</div>
|
||||
|
||||
<div class="needs-work">
|
||||
<redaction-annotation-icon type="redaction"></redaction-annotation-icon>
|
||||
<redaction-annotation-icon type="hint"></redaction-annotation-icon>
|
||||
</div>
|
||||
|
||||
<div class="assigned-to">
|
||||
<redaction-initials-avatar
|
||||
withName="true"
|
||||
[userId]="fileStatus.currentReviewer"
|
||||
></redaction-initials-avatar>
|
||||
</div>
|
||||
|
||||
<div class="status-container">
|
||||
<redaction-status-bar
|
||||
[config]="[
|
||||
{
|
||||
color:
|
||||
fileStatus.status === 'PROCESSED'
|
||||
? 'finished'
|
||||
: fileStatus.status === 'ERROR'
|
||||
? 'under-approval'
|
||||
: 'under-review',
|
||||
length: 1
|
||||
}
|
||||
]"
|
||||
></redaction-status-bar>
|
||||
|
||||
<div class="action-buttons">
|
||||
<button
|
||||
(click)="openDeleteFileDialog($event, fileStatus)"
|
||||
color="accent"
|
||||
mat-icon-button
|
||||
[matTooltip]="'project-overview.delete.action.label' | translate"
|
||||
>
|
||||
<mat-icon svgIcon="red:trash"></mat-icon>
|
||||
</button>
|
||||
<button
|
||||
(click)="$event.stopPropagation()"
|
||||
color="accent"
|
||||
mat-icon-button
|
||||
[matTooltip]="'project-overview.report.action.label' | translate"
|
||||
>
|
||||
<mat-icon svgIcon="red:report"></mat-icon>
|
||||
</button>
|
||||
<button
|
||||
(click)="openAssignFileOwnerDialog($event, fileStatus)"
|
||||
color="accent"
|
||||
mat-icon-button
|
||||
[matTooltip]="'project-overview.assign.action.label' | translate"
|
||||
>
|
||||
<mat-icon svgIcon="red:assign"></mat-icon>
|
||||
</button>
|
||||
<button
|
||||
(click)="reanalyseFile($event, fileStatus)"
|
||||
color="accent"
|
||||
mat-icon-button
|
||||
[matTooltip]="'project-overview.bar-charts.action.label' | translate"
|
||||
>
|
||||
<mat-icon svgIcon="red:analyse"></mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid-container">
|
||||
<!-- Table column names-->
|
||||
<div class="table-col-name"></div>
|
||||
|
||||
<div class="table-col-name">
|
||||
<span class="small-label" translate="project-overview.table-col-names.name.label"></span>
|
||||
</div>
|
||||
|
||||
<div class="table-col-name pointer" (click)="toggleSortByAddedOn()">
|
||||
<span class="small-label" translate="project-overview.table-col-names.added-on.label"></span>
|
||||
<div class="sort-arrows-container">
|
||||
<mat-icon svgIcon="red:arrow-up"
|
||||
[color]="sortingOption === sortingOptions[0] ? 'primary' : 'currentColor'"></mat-icon>
|
||||
<mat-icon svgIcon="red:arrow-down"
|
||||
[color]="sortingOption === sortingOptions[1] ? 'primary' : 'currentColor'"></mat-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-col-name">
|
||||
<span class="small-label" translate="project-overview.table-col-names.needs-work.label"></span>
|
||||
</div>
|
||||
|
||||
<div class="table-col-name">
|
||||
<span class="small-label" translate="project-overview.table-col-names.assigned-to.label"></span>
|
||||
</div>
|
||||
|
||||
<div class="table-col-name flex-end">
|
||||
<span class="small-label" translate="project-overview.table-col-names.status.label"></span>
|
||||
</div>
|
||||
|
||||
<div class="table-item"
|
||||
[class.pointer]="canOpenFile(fileStatus.status)"
|
||||
*ngFor="let fileStatus of appStateService.activeProject.files | sortBy: sortingOption.order:sortingOption.column; trackBy:fileId"
|
||||
[routerLink]="canOpenFile(fileStatus.status) ? ['/ui/projects/'+activeProject.projectId+'/file/'+fileStatus.fileId] : []">
|
||||
|
||||
<div class="pr-0">
|
||||
<div class="select-oval"
|
||||
[class.active]="isFileSelected(fileStatus)"
|
||||
(click)="toggleFileSelected($event, fileStatus)"></div>
|
||||
</div>
|
||||
|
||||
<div class="table-item-title" [matTooltip]="'['+fileStatus.status+'] '+fileStatus.filename ">
|
||||
{{ fileStatus.filename }}
|
||||
</div>
|
||||
|
||||
<div class="small-label">
|
||||
{{ fileStatus.added | date:'d MMM. yyyy, hh:mm a' }}
|
||||
</div>
|
||||
|
||||
<div class="needs-work">
|
||||
<redaction-annotation-icon type="redaction"></redaction-annotation-icon>
|
||||
<redaction-annotation-icon type="hint"></redaction-annotation-icon>
|
||||
</div>
|
||||
|
||||
<div class="assigned-to">
|
||||
<redaction-initials-avatar
|
||||
withName="true"
|
||||
[userId]="fileStatus.currentReviewer"
|
||||
></redaction-initials-avatar>
|
||||
</div>
|
||||
|
||||
<div class="status-container">
|
||||
<redaction-status-bar
|
||||
[config]="[{ color: (fileStatus.status === 'PROCESSED' ? 'finished': fileStatus.status ==='ERROR'?'under-approval' : 'under-review'), length: 1 }]"
|
||||
></redaction-status-bar>
|
||||
|
||||
<div class="action-buttons">
|
||||
<button (click)="openDeleteFileDialog($event,fileStatus)" color="accent" mat-icon-button
|
||||
[matTooltip]="'project-overview.delete.action.label'|translate">
|
||||
<mat-icon svgIcon="red:trash"></mat-icon>
|
||||
<div class="project-details-container right-fixed-container">
|
||||
<div class="actions-row">
|
||||
<button mat-icon-button (click)="openDeleteProjectDialog($event)">
|
||||
<mat-icon svgIcon="red:trash"></mat-icon>
|
||||
</button>
|
||||
<button (click)="$event.stopPropagation()" color="accent" mat-icon-button
|
||||
[matTooltip]="'project-overview.report.action.label'|translate">
|
||||
<mat-icon svgIcon="red:report"></mat-icon>
|
||||
<button mat-icon-button (click)="openEditProjectDialog($event)">
|
||||
<mat-icon svgIcon="red:edit"></mat-icon>
|
||||
</button>
|
||||
<button (click)="openAssignFileOwnerDialog($event,fileStatus)" color="accent" mat-icon-button
|
||||
[matTooltip]="'project-overview.assign.action.label'|translate">
|
||||
<mat-icon svgIcon="red:assign"></mat-icon>
|
||||
<button mat-icon-button (click)="openDetailsDialog($event)">
|
||||
<mat-icon svgIcon="red:report"></mat-icon>
|
||||
</button>
|
||||
<button (click)="reanalyseFile($event,fileStatus)" color="accent" mat-icon-button
|
||||
[matTooltip]="'project-overview.bar-charts.action.label'|translate">
|
||||
<mat-icon svgIcon="red:analyse"></mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="project-details-container right-fixed-container">
|
||||
<div class="actions-row">
|
||||
<button mat-icon-button (click)="openDeleteProjectDialog($event)">
|
||||
<mat-icon svgIcon="red:trash"></mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button (click)="openEditProjectDialog($event)">
|
||||
<mat-icon svgIcon="red:edit"></mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button (click)="openDetailsDialog($event)">
|
||||
<mat-icon svgIcon="red:report"></mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="small-label stats-subtitle mt-20">
|
||||
<div>
|
||||
<mat-icon svgIcon="red:pages"></mat-icon>
|
||||
{{ appStateService.activeProject.files.length }}
|
||||
</div>
|
||||
<div>
|
||||
<mat-icon svgIcon="red:user"></mat-icon>
|
||||
{{ appStateService.activeProject.project.memberIds.length }}
|
||||
</div>
|
||||
<div>
|
||||
<mat-icon svgIcon="red:calendar"></mat-icon>
|
||||
{{ appStateService.activeProject.project.date | date:'d MMM. yyyy' }}
|
||||
</div>
|
||||
<div *ngIf="appStateService.activeProject.project.dueDate">
|
||||
<mat-icon svgIcon="red:lightning"></mat-icon>
|
||||
{{appStateService.activeProject.project.dueDate | date:'mediumDate'}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="heading-xl mt-20">
|
||||
{{ appStateService.activeProject.project.projectName }}
|
||||
</div>
|
||||
|
||||
<div class="owner flex-row mt-20">
|
||||
<redaction-initials-avatar [userId]="activeProject.ownerId"
|
||||
size="large"
|
||||
withName="true"
|
||||
></redaction-initials-avatar>
|
||||
</div>
|
||||
|
||||
<div class="mt-20">
|
||||
{{ appStateService.activeProject.project.description }}
|
||||
</div>
|
||||
|
||||
<div class="project-team mt-20">
|
||||
<div class="all-caps-label" translate="project-overview.project-details.project-team.label"></div>
|
||||
<div class="flex mt-20 members-container">
|
||||
<div *ngFor="let userId of displayMembers" class="member">
|
||||
<redaction-initials-avatar [userId]="userId" size="large"></redaction-initials-avatar>
|
||||
</div>
|
||||
<div class="member" *ngIf="overflowCount">
|
||||
<div class="oval large white-dark">+{{overflowCount}}</div>
|
||||
</div>
|
||||
<div class="member pointer" (click)="openAssignProjectMembersDialog()">
|
||||
<div class="oval red-white large">+</div>
|
||||
<div class="small-label stats-subtitle mt-20">
|
||||
<div>
|
||||
<mat-icon svgIcon="red:pages"></mat-icon>
|
||||
{{ appStateService.activeProject.files.length }}
|
||||
</div>
|
||||
<div>
|
||||
<mat-icon svgIcon="red:user"></mat-icon>
|
||||
{{ appStateService.activeProject.project.memberIds.length }}
|
||||
</div>
|
||||
<div>
|
||||
<mat-icon svgIcon="red:calendar"></mat-icon>
|
||||
{{ appStateService.activeProject.project.date | date: 'd MMM. yyyy' }}
|
||||
</div>
|
||||
<div *ngIf="appStateService.activeProject.project.dueDate">
|
||||
<mat-icon svgIcon="red:lightning"></mat-icon>
|
||||
{{ appStateService.activeProject.project.dueDate | date: 'mediumDate' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="heading-xl mt-20">
|
||||
{{ appStateService.activeProject.project.projectName }}
|
||||
</div>
|
||||
|
||||
<div class="mt-32">
|
||||
<redaction-simple-doughnut-chart [config]="documentsChartData"
|
||||
[strokeWidth]="15"
|
||||
[radius]="70"
|
||||
[subtitle]="'project-overview.project-details.charts.total-documents.label'"
|
||||
direction="row"
|
||||
></redaction-simple-doughnut-chart>
|
||||
</div>
|
||||
<div class="owner flex-row mt-20">
|
||||
<redaction-initials-avatar
|
||||
[userId]="activeProject.ownerId"
|
||||
size="large"
|
||||
withName="true"
|
||||
></redaction-initials-avatar>
|
||||
</div>
|
||||
|
||||
<div class="mt-32 legend">
|
||||
<div>
|
||||
<redaction-annotation-icon type="hint"></redaction-annotation-icon>
|
||||
{{ 'project-overview.legend.contains-hints.label' | translate }}
|
||||
</div>
|
||||
<div>
|
||||
<redaction-annotation-icon type="redaction"></redaction-annotation-icon>
|
||||
{{ 'project-overview.legend.contains-redactions.label' | translate }}
|
||||
</div>
|
||||
<div>
|
||||
<redaction-annotation-icon type="suggestion"></redaction-annotation-icon>
|
||||
{{ 'project-overview.legend.contains-suggestions.label' | translate }}
|
||||
</div>
|
||||
<div class="mt-20">
|
||||
{{ appStateService.activeProject.project.description }}
|
||||
</div>
|
||||
|
||||
<div class="project-team mt-20">
|
||||
<div
|
||||
class="all-caps-label"
|
||||
translate="project-overview.project-details.project-team.label"
|
||||
></div>
|
||||
<div class="flex mt-20 members-container">
|
||||
<div *ngFor="let userId of displayMembers" class="member">
|
||||
<redaction-initials-avatar
|
||||
[userId]="userId"
|
||||
size="large"
|
||||
></redaction-initials-avatar>
|
||||
</div>
|
||||
<div class="member" *ngIf="overflowCount">
|
||||
<div class="oval large white-dark">+{{ overflowCount }}</div>
|
||||
</div>
|
||||
<div class="member pointer" (click)="openAssignProjectMembersDialog()">
|
||||
<div class="oval red-white large">+</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-32">
|
||||
<redaction-simple-doughnut-chart
|
||||
[config]="documentsChartData"
|
||||
[strokeWidth]="15"
|
||||
[radius]="70"
|
||||
[subtitle]="'project-overview.project-details.charts.total-documents.label'"
|
||||
direction="row"
|
||||
></redaction-simple-doughnut-chart>
|
||||
</div>
|
||||
|
||||
<div class="mt-32 legend">
|
||||
<div>
|
||||
<redaction-annotation-icon type="hint"></redaction-annotation-icon>
|
||||
{{ 'project-overview.legend.contains-hints.label' | translate }}
|
||||
</div>
|
||||
<div>
|
||||
<redaction-annotation-icon type="redaction"></redaction-annotation-icon>
|
||||
{{ 'project-overview.legend.contains-redactions.label' | translate }}
|
||||
</div>
|
||||
<div>
|
||||
<redaction-annotation-icon type="suggestion"></redaction-annotation-icon>
|
||||
{{ 'project-overview.legend.contains-suggestions.label' | translate }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,76 +1,76 @@
|
||||
@import "../../../assets/styles/red-variables";
|
||||
@import '../../../assets/styles/red-variables';
|
||||
|
||||
.file-upload-input {
|
||||
display: none;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.select-all-container {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
|
||||
.select-oval {
|
||||
margin-left: 0;
|
||||
}
|
||||
.select-oval {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.pr-0 {
|
||||
padding-right: 0 !important;
|
||||
padding-right: 0 !important;
|
||||
}
|
||||
|
||||
.select-oval {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid $grey-5;
|
||||
background-color: $white;
|
||||
cursor: pointer;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid $grey-5;
|
||||
background-color: $white;
|
||||
cursor: pointer;
|
||||
|
||||
&.active {
|
||||
background-color: $primary;
|
||||
}
|
||||
&.active {
|
||||
background-color: $primary;
|
||||
}
|
||||
|
||||
&.placeholder {
|
||||
visibility: hidden;
|
||||
}
|
||||
&.placeholder {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.grid-container {
|
||||
grid-template-columns: auto 3fr 2fr 1fr 2fr auto;
|
||||
grid-template-columns: auto 3fr 2fr 1fr 2fr auto;
|
||||
|
||||
.table-item {
|
||||
.table-item-title {
|
||||
line-height: 80px;
|
||||
}
|
||||
.table-item {
|
||||
.table-item-title {
|
||||
line-height: 80px;
|
||||
}
|
||||
|
||||
.needs-work {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 4px;
|
||||
.needs-work {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.project-details-container {
|
||||
.members-container {
|
||||
gap: 5px;
|
||||
}
|
||||
.members-container {
|
||||
gap: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.legend {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.mt-32 {
|
||||
margin-top: 32px;
|
||||
margin-top: 32px;
|
||||
}
|
||||
|
||||
@ -1,6 +1,10 @@
|
||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { FileStatus, ReanalysisControllerService, StatusControllerService } from '@redaction/red-ui-http';
|
||||
import {
|
||||
FileStatus,
|
||||
ReanalysisControllerService,
|
||||
StatusControllerService
|
||||
} from '@redaction/red-ui-http';
|
||||
import { NotificationService } from '../../notification/notification.service';
|
||||
import { AppStateService } from '../../state/app-state.service';
|
||||
import { FileDropOverlayService } from '../../upload/file-drop/service/file-drop-overlay.service';
|
||||
@ -13,177 +17,210 @@ import { DoughnutChartConfig } from '../../components/simple-doughnut-chart/simp
|
||||
import { groupBy } from '../../utils/functions';
|
||||
import { DialogService } from '../../dialogs/dialog.service';
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'redaction-project-overview-screen',
|
||||
templateUrl: './project-overview-screen.component.html',
|
||||
styleUrls: ['./project-overview-screen.component.scss']
|
||||
selector: 'redaction-project-overview-screen',
|
||||
templateUrl: './project-overview-screen.component.html',
|
||||
styleUrls: ['./project-overview-screen.component.scss']
|
||||
})
|
||||
export class ProjectOverviewScreenComponent implements OnInit, OnDestroy {
|
||||
private _selectedFileIds: string[] = [];
|
||||
private _selectedFileIds: string[] = [];
|
||||
|
||||
public sortingOptions: SortingOption[] = [
|
||||
{ label: 'project-overview.sorting.recent.label', order: 'desc', column: 'lastUpdated' },
|
||||
{ label: 'project-overview.sorting.oldest.label', order: 'asc', column: 'lastUpdated' },
|
||||
{ label: 'project-overview.sorting.alphabetically.label', order: 'asc', column: 'filename' },
|
||||
{ label: 'project-overview.sorting.number-of-pages.label', order: 'asc', column: 'numberOfPages' },
|
||||
{ label: 'project-overview.sorting.number-of-analyses.label', order: 'desc', column: 'numberOfAnalyses' }
|
||||
];
|
||||
public sortingOption: SortingOption = this.sortingOptions[0];
|
||||
public documentsChartData: DoughnutChartConfig[] = [];
|
||||
public sortingOptions: SortingOption[] = [
|
||||
{ label: 'project-overview.sorting.recent.label', order: 'desc', column: 'lastUpdated' },
|
||||
{ label: 'project-overview.sorting.oldest.label', order: 'asc', column: 'lastUpdated' },
|
||||
{
|
||||
label: 'project-overview.sorting.alphabetically.label',
|
||||
order: 'asc',
|
||||
column: 'filename'
|
||||
},
|
||||
{
|
||||
label: 'project-overview.sorting.number-of-pages.label',
|
||||
order: 'asc',
|
||||
column: 'numberOfPages'
|
||||
},
|
||||
{
|
||||
label: 'project-overview.sorting.number-of-analyses.label',
|
||||
order: 'desc',
|
||||
column: 'numberOfAnalyses'
|
||||
}
|
||||
];
|
||||
public sortingOption: SortingOption = this.sortingOptions[0];
|
||||
public documentsChartData: DoughnutChartConfig[] = [];
|
||||
|
||||
constructor(public readonly appStateService: AppStateService,
|
||||
private readonly _activatedRoute: ActivatedRoute,
|
||||
private readonly _statusControllerService: StatusControllerService,
|
||||
private readonly _notificationService: NotificationService,
|
||||
private readonly _dialogService: DialogService,
|
||||
private readonly _fileUploadService: FileUploadService,
|
||||
private readonly _uploadStatusOverlayService: UploadStatusOverlayService,
|
||||
private readonly _reanalysisControllerService: ReanalysisControllerService,
|
||||
private readonly _router: Router,
|
||||
private readonly _userService: UserService,
|
||||
private readonly _fileDropOverlayService: FileDropOverlayService) {
|
||||
this._activatedRoute.params.subscribe(params => {
|
||||
this.appStateService.activateProject(params.projectId);
|
||||
});
|
||||
constructor(
|
||||
public readonly appStateService: AppStateService,
|
||||
private readonly _activatedRoute: ActivatedRoute,
|
||||
private readonly _statusControllerService: StatusControllerService,
|
||||
private readonly _notificationService: NotificationService,
|
||||
private readonly _dialogService: DialogService,
|
||||
private readonly _fileUploadService: FileUploadService,
|
||||
private readonly _uploadStatusOverlayService: UploadStatusOverlayService,
|
||||
private readonly _reanalysisControllerService: ReanalysisControllerService,
|
||||
private readonly _router: Router,
|
||||
private readonly _userService: UserService,
|
||||
private readonly _fileDropOverlayService: FileDropOverlayService
|
||||
) {
|
||||
this._activatedRoute.params.subscribe((params) => {
|
||||
this.appStateService.activateProject(params.projectId);
|
||||
});
|
||||
|
||||
this.appStateService.fileStatusChanged.subscribe(() => {
|
||||
this._calculateChartConfig();
|
||||
})
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this._fileDropOverlayService.initFileDropHandling();
|
||||
this._calculateChartConfig();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this._fileDropOverlayService.cleanupFileDropHandling();
|
||||
}
|
||||
|
||||
public get activeProject() {
|
||||
return this.appStateService.activeProject.project;
|
||||
}
|
||||
|
||||
public get user() {
|
||||
return this._userService.user;
|
||||
}
|
||||
|
||||
public get displayMembers() {
|
||||
return this.activeProject.memberIds.slice(0, 6);
|
||||
}
|
||||
|
||||
public get overflowCount() {
|
||||
return this.activeProject.memberIds.length > 6 ? this.activeProject.memberIds.length - 6 : 0;
|
||||
}
|
||||
|
||||
private _getFileStatus() {
|
||||
this.appStateService.reloadActiveProjectFiles().then(() => {
|
||||
this._calculateChartConfig();
|
||||
});
|
||||
}
|
||||
|
||||
private _calculateChartConfig() {
|
||||
const groups = groupBy(this.appStateService.activeProject.files, 'status');
|
||||
this.documentsChartData = [];
|
||||
for (const key of Object.keys(groups)) {
|
||||
this.documentsChartData.push({ value: groups[key].length, color: key, label: key });
|
||||
}
|
||||
}
|
||||
|
||||
public toggleFileSelected($event: MouseEvent, file: FileStatus) {
|
||||
$event.stopPropagation();
|
||||
const idx = this._selectedFileIds.indexOf(file.fileId);
|
||||
if (idx === -1) {
|
||||
this._selectedFileIds.push(file.fileId);
|
||||
} else {
|
||||
this._selectedFileIds.splice(idx, 1);
|
||||
}
|
||||
}
|
||||
|
||||
public toggleSelectAll() {
|
||||
if (this.areAllFilesSelected()) {
|
||||
this._selectedFileIds = [];
|
||||
} else {
|
||||
this._selectedFileIds = this.appStateService.activeProject.files.map(file => file.fileId);
|
||||
}
|
||||
}
|
||||
|
||||
public areAllFilesSelected() {
|
||||
return this.appStateService.activeProject.files.length !== 0 &&
|
||||
this._selectedFileIds.length === this.appStateService.activeProject.files.length;
|
||||
}
|
||||
|
||||
public isFileSelected(file: FileStatus) {
|
||||
return this._selectedFileIds.indexOf(file.fileId) !== -1;
|
||||
}
|
||||
|
||||
public openDeleteFileDialog($event: MouseEvent, fileStatus: FileStatus) {
|
||||
this._dialogService.openDeleteFileDialog($event, fileStatus.projectId, fileStatus.fileId, () => {
|
||||
this._calculateChartConfig();
|
||||
});
|
||||
}
|
||||
|
||||
public openDetailsDialog($event: MouseEvent) {
|
||||
this._dialogService.openProjectDetailsDialog($event, this.appStateService.activeProject);
|
||||
}
|
||||
|
||||
public openEditProjectDialog($event: MouseEvent) {
|
||||
this._dialogService.openEditProjectDialog($event, this.appStateService.activeProject.project);
|
||||
}
|
||||
|
||||
public openDeleteProjectDialog($event: MouseEvent) {
|
||||
this._dialogService.openDeleteProjectDialog($event, this.appStateService.activeProject.project, () => {
|
||||
this._router.navigate(['/ui/projects']);
|
||||
});
|
||||
}
|
||||
|
||||
public openAssignProjectMembersDialog() {
|
||||
this._dialogService.openAssignProjectMembersAndOwnerDialog(null, this.activeProject, () => {
|
||||
this._getFileStatus();
|
||||
});
|
||||
}
|
||||
|
||||
public openAssignFileOwnerDialog($event: MouseEvent, file: FileStatus) {
|
||||
this._dialogService.openAssignFileOwnerDialog($event, file, () => {
|
||||
this._getFileStatus();
|
||||
});
|
||||
}
|
||||
|
||||
public reanalyseFile($event: MouseEvent, fileStatus: FileStatus) {
|
||||
$event.stopPropagation();
|
||||
this._reanalysisControllerService.reanalyzeFile(this.appStateService.activeProject.project.projectId, fileStatus.fileId).subscribe(() => {
|
||||
this._getFileStatus();
|
||||
});
|
||||
}
|
||||
|
||||
public fileId(index, item) {
|
||||
return item.fileId;
|
||||
}
|
||||
|
||||
public uploadFiles(files: FileList | File[]) {
|
||||
const uploadFiles: FileUploadModel[] = [];
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const file = files[i];
|
||||
uploadFiles.push({
|
||||
file: file,
|
||||
progress: 0,
|
||||
completed: false,
|
||||
error: null
|
||||
});
|
||||
this.appStateService.fileStatusChanged.subscribe(() => {
|
||||
this._calculateChartConfig();
|
||||
});
|
||||
}
|
||||
|
||||
this._fileUploadService.uploadFiles(uploadFiles);
|
||||
this._uploadStatusOverlayService.openStatusOverlay();
|
||||
}
|
||||
ngOnInit(): void {
|
||||
this._fileDropOverlayService.initFileDropHandling();
|
||||
this._calculateChartConfig();
|
||||
}
|
||||
|
||||
public canOpenFile(fileStatus: FileStatus): boolean {
|
||||
// TODO check correct condition for this
|
||||
return fileStatus === 'PROCESSING' || fileStatus === 'REVIEWED' || true;
|
||||
}
|
||||
ngOnDestroy(): void {
|
||||
this._fileDropOverlayService.cleanupFileDropHandling();
|
||||
}
|
||||
|
||||
public toggleSortByAddedOn() {
|
||||
const sortedByRecent: boolean = (this.sortingOption === this.sortingOptions[0]);
|
||||
this.sortingOption = sortedByRecent ? this.sortingOptions[1] : this.sortingOptions[0];
|
||||
}
|
||||
public get activeProject() {
|
||||
return this.appStateService.activeProject.project;
|
||||
}
|
||||
|
||||
public get user() {
|
||||
return this._userService.user;
|
||||
}
|
||||
|
||||
public get displayMembers() {
|
||||
return this.activeProject.memberIds.slice(0, 6);
|
||||
}
|
||||
|
||||
public get overflowCount() {
|
||||
return this.activeProject.memberIds.length > 6
|
||||
? this.activeProject.memberIds.length - 6
|
||||
: 0;
|
||||
}
|
||||
|
||||
private _getFileStatus() {
|
||||
this.appStateService.reloadActiveProjectFiles().then(() => {
|
||||
this._calculateChartConfig();
|
||||
});
|
||||
}
|
||||
|
||||
private _calculateChartConfig() {
|
||||
const groups = groupBy(this.appStateService.activeProject.files, 'status');
|
||||
this.documentsChartData = [];
|
||||
for (const key of Object.keys(groups)) {
|
||||
this.documentsChartData.push({ value: groups[key].length, color: key, label: key });
|
||||
}
|
||||
}
|
||||
|
||||
public toggleFileSelected($event: MouseEvent, file: FileStatus) {
|
||||
$event.stopPropagation();
|
||||
const idx = this._selectedFileIds.indexOf(file.fileId);
|
||||
if (idx === -1) {
|
||||
this._selectedFileIds.push(file.fileId);
|
||||
} else {
|
||||
this._selectedFileIds.splice(idx, 1);
|
||||
}
|
||||
}
|
||||
|
||||
public toggleSelectAll() {
|
||||
if (this.areAllFilesSelected()) {
|
||||
this._selectedFileIds = [];
|
||||
} else {
|
||||
this._selectedFileIds = this.appStateService.activeProject.files.map(
|
||||
(file) => file.fileId
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public areAllFilesSelected() {
|
||||
return (
|
||||
this.appStateService.activeProject.files.length !== 0 &&
|
||||
this._selectedFileIds.length === this.appStateService.activeProject.files.length
|
||||
);
|
||||
}
|
||||
|
||||
public isFileSelected(file: FileStatus) {
|
||||
return this._selectedFileIds.indexOf(file.fileId) !== -1;
|
||||
}
|
||||
|
||||
public openDeleteFileDialog($event: MouseEvent, fileStatus: FileStatus) {
|
||||
this._dialogService.openDeleteFileDialog(
|
||||
$event,
|
||||
fileStatus.projectId,
|
||||
fileStatus.fileId,
|
||||
() => {
|
||||
this._calculateChartConfig();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public openDetailsDialog($event: MouseEvent) {
|
||||
this._dialogService.openProjectDetailsDialog($event, this.appStateService.activeProject);
|
||||
}
|
||||
|
||||
public openEditProjectDialog($event: MouseEvent) {
|
||||
this._dialogService.openEditProjectDialog(
|
||||
$event,
|
||||
this.appStateService.activeProject.project
|
||||
);
|
||||
}
|
||||
|
||||
public openDeleteProjectDialog($event: MouseEvent) {
|
||||
this._dialogService.openDeleteProjectDialog(
|
||||
$event,
|
||||
this.appStateService.activeProject.project,
|
||||
() => {
|
||||
this._router.navigate(['/ui/projects']);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public openAssignProjectMembersDialog() {
|
||||
this._dialogService.openAssignProjectMembersAndOwnerDialog(null, this.activeProject, () => {
|
||||
this._getFileStatus();
|
||||
});
|
||||
}
|
||||
|
||||
public openAssignFileOwnerDialog($event: MouseEvent, file: FileStatus) {
|
||||
this._dialogService.openAssignFileOwnerDialog($event, file, () => {
|
||||
this._getFileStatus();
|
||||
});
|
||||
}
|
||||
|
||||
public reanalyseFile($event: MouseEvent, fileStatus: FileStatus) {
|
||||
$event.stopPropagation();
|
||||
this._reanalysisControllerService
|
||||
.reanalyzeFile(this.appStateService.activeProject.project.projectId, fileStatus.fileId)
|
||||
.subscribe(() => {
|
||||
this._getFileStatus();
|
||||
});
|
||||
}
|
||||
|
||||
public fileId(index, item) {
|
||||
return item.fileId;
|
||||
}
|
||||
|
||||
public uploadFiles(files: FileList | File[]) {
|
||||
const uploadFiles: FileUploadModel[] = [];
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const file = files[i];
|
||||
uploadFiles.push({
|
||||
file: file,
|
||||
progress: 0,
|
||||
completed: false,
|
||||
error: null
|
||||
});
|
||||
}
|
||||
|
||||
this._fileUploadService.uploadFiles(uploadFiles);
|
||||
this._uploadStatusOverlayService.openStatusOverlay();
|
||||
}
|
||||
|
||||
public canOpenFile(fileStatus: FileStatus): boolean {
|
||||
// TODO check correct condition for this
|
||||
return fileStatus === 'PROCESSING' || fileStatus === 'REVIEWED' || true;
|
||||
}
|
||||
|
||||
public toggleSortByAddedOn() {
|
||||
const sortedByRecent: boolean = this.sortingOption === this.sortingOptions[0];
|
||||
this.sortingOption = sortedByRecent ? this.sortingOptions[1] : this.sortingOptions[0];
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,261 +1,298 @@
|
||||
import {EventEmitter, Injectable} from "@angular/core";
|
||||
import { EventEmitter, Injectable } from '@angular/core';
|
||||
import {
|
||||
FileStatus,
|
||||
Project,
|
||||
ProjectControllerService,
|
||||
ReanalysisControllerService,
|
||||
StatusControllerService
|
||||
} from "@redaction/red-ui-http";
|
||||
import {NotificationService, NotificationType} from "../notification/notification.service";
|
||||
import {TranslateService} from "@ngx-translate/core";
|
||||
import {Router} from "@angular/router";
|
||||
import {UserService} from "../user/user.service";
|
||||
import {interval} from "rxjs";
|
||||
import {tap} from "rxjs/operators";
|
||||
|
||||
FileStatus,
|
||||
Project,
|
||||
ProjectControllerService,
|
||||
ReanalysisControllerService,
|
||||
StatusControllerService
|
||||
} from '@redaction/red-ui-http';
|
||||
import { NotificationService, NotificationType } from '../notification/notification.service';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { UserService } from '../user/user.service';
|
||||
import { interval } from 'rxjs';
|
||||
import { tap } from 'rxjs/operators';
|
||||
|
||||
export interface AppState {
|
||||
|
||||
projects: ProjectWrapper[];
|
||||
activeProject: ProjectWrapper;
|
||||
activeFile: FileStatus;
|
||||
totalAnalysedPages?: number;
|
||||
totalDocuments?: number;
|
||||
totalPeople?: number;
|
||||
projects: ProjectWrapper[];
|
||||
activeProject: ProjectWrapper;
|
||||
activeFile: FileStatus;
|
||||
totalAnalysedPages?: number;
|
||||
totalDocuments?: number;
|
||||
totalPeople?: number;
|
||||
}
|
||||
|
||||
export class ProjectWrapper {
|
||||
constructor(public project: Project, public files: FileStatus[]) {
|
||||
constructor(public project: Project, public files: FileStatus[]) {}
|
||||
|
||||
}
|
||||
|
||||
get projectDate() {
|
||||
return this.project.date;
|
||||
}
|
||||
get projectDate() {
|
||||
return this.project.date;
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class AppStateService {
|
||||
private _appState: AppState;
|
||||
|
||||
private _appState: AppState;
|
||||
public fileStatusChanged = new EventEmitter<FileStatus>();
|
||||
|
||||
public fileStatusChanged = new EventEmitter<FileStatus>();
|
||||
constructor(
|
||||
private readonly _router: Router,
|
||||
private readonly _userService: UserService,
|
||||
private readonly _projectControllerService: ProjectControllerService,
|
||||
private readonly _notificationService: NotificationService,
|
||||
private readonly _reanalysisControllerService: ReanalysisControllerService,
|
||||
private readonly _translateService: TranslateService,
|
||||
private readonly _statusControllerService: StatusControllerService
|
||||
) {
|
||||
this._appState = {
|
||||
projects: [],
|
||||
activeProject: null,
|
||||
activeFile: null
|
||||
};
|
||||
|
||||
constructor(
|
||||
private readonly _router: Router,
|
||||
private readonly _userService: UserService,
|
||||
private readonly _projectControllerService: ProjectControllerService,
|
||||
private readonly _notificationService: NotificationService,
|
||||
private readonly _reanalysisControllerService: ReanalysisControllerService,
|
||||
private readonly _translateService: TranslateService,
|
||||
private readonly _statusControllerService: StatusControllerService) {
|
||||
this._appState = {
|
||||
projects: [],
|
||||
activeProject: null,
|
||||
activeFile: null
|
||||
interval(5000)
|
||||
.pipe(
|
||||
tap(() => {
|
||||
this.reloadActiveProjectFiles();
|
||||
})
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
interval(30000)
|
||||
.pipe(
|
||||
tap(() => {
|
||||
this.loadAllProjects();
|
||||
})
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
interval(5000).pipe(tap(() => {
|
||||
this.reloadActiveProjectFiles();
|
||||
})).subscribe();
|
||||
|
||||
interval(30000).pipe(tap(() => {
|
||||
this.loadAllProjects();
|
||||
})).subscribe();
|
||||
}
|
||||
|
||||
|
||||
get isActiveProjectOwner() {
|
||||
return this._appState.activeProject?.project?.ownerId === this._userService.userId
|
||||
}
|
||||
|
||||
get isActiveProjectMember() {
|
||||
return this._appState.activeProject?.project?.memberIds?.indexOf(this._userService.userId) >= 0;
|
||||
}
|
||||
|
||||
get isActiveFileDocumentReviewer() {
|
||||
return this._appState.activeFile?.currentReviewer === this._userService.userId;
|
||||
}
|
||||
|
||||
|
||||
get aggregatedFiles(): FileStatus[] {
|
||||
const result: FileStatus[] = [];
|
||||
this._appState.projects.forEach(p => {
|
||||
result.push(...p.files);
|
||||
})
|
||||
return result;
|
||||
}
|
||||
|
||||
get activeProjectId(): string {
|
||||
return this._appState.activeProject?.project?.projectId;
|
||||
}
|
||||
|
||||
get activeProject(): ProjectWrapper {
|
||||
return this._appState.activeProject;
|
||||
}
|
||||
|
||||
get allProjects(): ProjectWrapper[] {
|
||||
return this._appState.projects;
|
||||
}
|
||||
|
||||
get activeFile(): FileStatus {
|
||||
return this._appState.activeFile;
|
||||
}
|
||||
|
||||
get totalAnalysedPages() {
|
||||
return this._appState.totalAnalysedPages;
|
||||
}
|
||||
|
||||
get totalPeople() {
|
||||
return this._appState.totalPeople;
|
||||
}
|
||||
|
||||
get totalDocuments() {
|
||||
return this._appState.totalDocuments;
|
||||
}
|
||||
|
||||
public getProjectById(id: string) {
|
||||
return this.allProjects.find(project => project.project.projectId === id);
|
||||
}
|
||||
|
||||
public getFileById(projectId: string, fileId: string) {
|
||||
return this.getProjectById(projectId).files.find(file => file.fileId === fileId);
|
||||
}
|
||||
|
||||
async loadAllProjects() {
|
||||
const projects = await this._projectControllerService.getProjects().toPromise();
|
||||
if (projects) {
|
||||
|
||||
const mappedProjects = projects.map(p => {
|
||||
return new ProjectWrapper(p, this._getExistingFiles(p));
|
||||
});
|
||||
|
||||
for (const project of mappedProjects) {
|
||||
await this.getFiles(project);
|
||||
}
|
||||
|
||||
this._appState.projects = mappedProjects;
|
||||
this._computeStats();
|
||||
get isActiveProjectOwner() {
|
||||
return this._appState.activeProject?.project?.ownerId === this._userService.userId;
|
||||
}
|
||||
}
|
||||
|
||||
private _getExistingFiles(project: Project) {
|
||||
const found = this._appState.projects.find(p => p.project.projectId === project.projectId);
|
||||
return found ? found.files : [];
|
||||
}
|
||||
get isActiveProjectMember() {
|
||||
return (
|
||||
this._appState.activeProject?.project?.memberIds?.indexOf(this._userService.userId) >= 0
|
||||
);
|
||||
}
|
||||
|
||||
async getFiles(project?: ProjectWrapper) {
|
||||
const files = await this._statusControllerService.getProjectStatus(project.project.projectId).toPromise();
|
||||
const oldFiles = [...project.files];
|
||||
get isActiveFileDocumentReviewer() {
|
||||
return this._appState.activeFile?.currentReviewer === this._userService.userId;
|
||||
}
|
||||
|
||||
for (const file of files) {
|
||||
let found = false;
|
||||
for (const oldFile of oldFiles) {
|
||||
if (oldFile.fileId === file.fileId) {
|
||||
// emit when analysis count changed
|
||||
if (oldFile.numberOfAnalyses !== file.numberOfAnalyses) {
|
||||
this.fileStatusChanged.emit(oldFile);
|
||||
}
|
||||
found = true;
|
||||
break;
|
||||
get aggregatedFiles(): FileStatus[] {
|
||||
const result: FileStatus[] = [];
|
||||
this._appState.projects.forEach((p) => {
|
||||
result.push(...p.files);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
get activeProjectId(): string {
|
||||
return this._appState.activeProject?.project?.projectId;
|
||||
}
|
||||
|
||||
get activeProject(): ProjectWrapper {
|
||||
return this._appState.activeProject;
|
||||
}
|
||||
|
||||
get allProjects(): ProjectWrapper[] {
|
||||
return this._appState.projects;
|
||||
}
|
||||
|
||||
get activeFile(): FileStatus {
|
||||
return this._appState.activeFile;
|
||||
}
|
||||
|
||||
get totalAnalysedPages() {
|
||||
return this._appState.totalAnalysedPages;
|
||||
}
|
||||
|
||||
get totalPeople() {
|
||||
return this._appState.totalPeople;
|
||||
}
|
||||
|
||||
get totalDocuments() {
|
||||
return this._appState.totalDocuments;
|
||||
}
|
||||
|
||||
public getProjectById(id: string) {
|
||||
return this.allProjects.find((project) => project.project.projectId === id);
|
||||
}
|
||||
|
||||
public getFileById(projectId: string, fileId: string) {
|
||||
return this.getProjectById(projectId).files.find((file) => file.fileId === fileId);
|
||||
}
|
||||
|
||||
async loadAllProjects() {
|
||||
const projects = await this._projectControllerService.getProjects().toPromise();
|
||||
if (projects) {
|
||||
const mappedProjects = projects.map((p) => {
|
||||
return new ProjectWrapper(p, this._getExistingFiles(p));
|
||||
});
|
||||
|
||||
for (const project of mappedProjects) {
|
||||
await this.getFiles(project);
|
||||
}
|
||||
|
||||
this._appState.projects = mappedProjects;
|
||||
this._computeStats();
|
||||
}
|
||||
}
|
||||
// emit for new file
|
||||
if (!found) {
|
||||
this.fileStatusChanged.emit(file);
|
||||
}
|
||||
}
|
||||
|
||||
project.files = files;
|
||||
|
||||
this._computeStats();
|
||||
return files;
|
||||
}
|
||||
|
||||
|
||||
activateProject(projectId: string) {
|
||||
this._appState.activeFile = null;
|
||||
this._appState.activeProject = this._appState.projects.find(p => p.project.projectId === projectId);
|
||||
if (!this._appState.activeProject) {
|
||||
this._router.navigate(['/ui/projects']);
|
||||
private _getExistingFiles(project: Project) {
|
||||
const found = this._appState.projects.find(
|
||||
(p) => p.project.projectId === project.projectId
|
||||
);
|
||||
return found ? found.files : [];
|
||||
}
|
||||
return this._appState.activeProject;
|
||||
}
|
||||
|
||||
activateFile(projectId: string, fileId: string) {
|
||||
this._appState.activeFile = null;
|
||||
this._appState.activeProject = this._appState.projects.find(p => p.project.projectId === projectId);
|
||||
this._appState.activeFile = this._appState.activeProject.files.find(f => f.fileId === fileId);
|
||||
}
|
||||
async getFiles(project?: ProjectWrapper) {
|
||||
const files = await this._statusControllerService
|
||||
.getProjectStatus(project.project.projectId)
|
||||
.toPromise();
|
||||
const oldFiles = [...project.files];
|
||||
|
||||
reset() {
|
||||
this._appState.activeFile = null;
|
||||
this._appState.activeProject = null;
|
||||
}
|
||||
for (const file of files) {
|
||||
let found = false;
|
||||
for (const oldFile of oldFiles) {
|
||||
if (oldFile.fileId === file.fileId) {
|
||||
// emit when analysis count changed
|
||||
if (oldFile.numberOfAnalyses !== file.numberOfAnalyses) {
|
||||
this.fileStatusChanged.emit(oldFile);
|
||||
}
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// emit for new file
|
||||
if (!found) {
|
||||
this.fileStatusChanged.emit(file);
|
||||
}
|
||||
}
|
||||
|
||||
deleteProject(project: Project) {
|
||||
return this._projectControllerService.deleteProject(project.projectId).toPromise().then(() => {
|
||||
const index = this._appState.projects.findIndex(p => p.project.projectId === project.projectId);
|
||||
this._appState.projects.splice(index, 1);
|
||||
this._appState.projects = [...this._appState.projects];
|
||||
}, () => {
|
||||
this._notificationService.showToastNotification(this._translateService.instant('projects.delete.delete-failed.label', project), null, NotificationType.ERROR)
|
||||
});
|
||||
}
|
||||
project.files = files;
|
||||
|
||||
async addOrUpdateProject(project: Project) {
|
||||
try {
|
||||
const updatedProject = await this._projectControllerService.createProjectOrUpdateProject(project).toPromise();
|
||||
const foundProject = this._appState.projects.find(p => p.project.projectId === updatedProject.projectId);
|
||||
if (foundProject) {
|
||||
Object.assign(foundProject.project, updatedProject);
|
||||
} else {
|
||||
this._appState.projects.push(new ProjectWrapper(updatedProject, []));
|
||||
}
|
||||
this._appState.projects = [...this._appState.projects];
|
||||
} catch (error) {
|
||||
this._notificationService.showToastNotification(this._translateService.instant('projects.add-edit-dialog.errors.save'), null, NotificationType.ERROR);
|
||||
this._computeStats();
|
||||
return files;
|
||||
}
|
||||
}
|
||||
|
||||
private _computeStats() {
|
||||
let totalAnalysedPages = 0;
|
||||
let totalDocuments = 0;
|
||||
const totalPeople = new Set<string>();
|
||||
this._appState.projects.forEach(p => {
|
||||
totalDocuments += p.files.length;
|
||||
if (p.project.memberIds) {
|
||||
p.project.memberIds.forEach(m => totalPeople.add(m));
|
||||
}
|
||||
p.files.forEach(f => {
|
||||
totalAnalysedPages += f.numberOfPages;
|
||||
})
|
||||
})
|
||||
|
||||
this._appState.totalPeople = totalPeople.size;
|
||||
this._appState.totalAnalysedPages = totalAnalysedPages;
|
||||
this._appState.totalDocuments = totalDocuments;
|
||||
|
||||
}
|
||||
|
||||
async reloadActiveProjectFiles() {
|
||||
if (this._appState.activeProject) {
|
||||
await this.getFiles(this._appState.activeProject);
|
||||
activateProject(projectId: string) {
|
||||
this._appState.activeFile = null;
|
||||
this._appState.activeProject = this._appState.projects.find(
|
||||
(p) => p.project.projectId === projectId
|
||||
);
|
||||
if (!this._appState.activeProject) {
|
||||
this._router.navigate(['/ui/projects']);
|
||||
}
|
||||
return this._appState.activeProject;
|
||||
}
|
||||
}
|
||||
|
||||
async loadAllProjectsIfNecessary() {
|
||||
if (!this._appState.projects.length) {
|
||||
await this.loadAllProjects();
|
||||
activateFile(projectId: string, fileId: string) {
|
||||
this._appState.activeFile = null;
|
||||
this._appState.activeProject = this._appState.projects.find(
|
||||
(p) => p.project.projectId === projectId
|
||||
);
|
||||
this._appState.activeFile = this._appState.activeProject.files.find(
|
||||
(f) => f.fileId === fileId
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async reanalyseActiveFile() {
|
||||
await this._reanalysisControllerService.reanalyzeFile(this._appState.activeProject.project.projectId, this._appState.activeFile.fileId).toPromise();
|
||||
await this.reloadActiveProjectFiles();
|
||||
}
|
||||
reset() {
|
||||
this._appState.activeFile = null;
|
||||
this._appState.activeProject = null;
|
||||
}
|
||||
|
||||
deleteProject(project: Project) {
|
||||
return this._projectControllerService
|
||||
.deleteProject(project.projectId)
|
||||
.toPromise()
|
||||
.then(
|
||||
() => {
|
||||
const index = this._appState.projects.findIndex(
|
||||
(p) => p.project.projectId === project.projectId
|
||||
);
|
||||
this._appState.projects.splice(index, 1);
|
||||
this._appState.projects = [...this._appState.projects];
|
||||
},
|
||||
() => {
|
||||
this._notificationService.showToastNotification(
|
||||
this._translateService.instant(
|
||||
'projects.delete.delete-failed.label',
|
||||
project
|
||||
),
|
||||
null,
|
||||
NotificationType.ERROR
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async addOrUpdateProject(project: Project) {
|
||||
try {
|
||||
const updatedProject = await this._projectControllerService
|
||||
.createProjectOrUpdateProject(project)
|
||||
.toPromise();
|
||||
const foundProject = this._appState.projects.find(
|
||||
(p) => p.project.projectId === updatedProject.projectId
|
||||
);
|
||||
if (foundProject) {
|
||||
Object.assign(foundProject.project, updatedProject);
|
||||
} else {
|
||||
this._appState.projects.push(new ProjectWrapper(updatedProject, []));
|
||||
}
|
||||
this._appState.projects = [...this._appState.projects];
|
||||
} catch (error) {
|
||||
this._notificationService.showToastNotification(
|
||||
this._translateService.instant('projects.add-edit-dialog.errors.save'),
|
||||
null,
|
||||
NotificationType.ERROR
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private _computeStats() {
|
||||
let totalAnalysedPages = 0;
|
||||
let totalDocuments = 0;
|
||||
const totalPeople = new Set<string>();
|
||||
this._appState.projects.forEach((p) => {
|
||||
totalDocuments += p.files.length;
|
||||
if (p.project.memberIds) {
|
||||
p.project.memberIds.forEach((m) => totalPeople.add(m));
|
||||
}
|
||||
p.files.forEach((f) => {
|
||||
totalAnalysedPages += f.numberOfPages;
|
||||
});
|
||||
});
|
||||
|
||||
this._appState.totalPeople = totalPeople.size;
|
||||
this._appState.totalAnalysedPages = totalAnalysedPages;
|
||||
this._appState.totalDocuments = totalDocuments;
|
||||
}
|
||||
|
||||
async reloadActiveProjectFiles() {
|
||||
if (this._appState.activeProject) {
|
||||
await this.getFiles(this._appState.activeProject);
|
||||
}
|
||||
}
|
||||
|
||||
async loadAllProjectsIfNecessary() {
|
||||
if (!this._appState.projects.length) {
|
||||
await this.loadAllProjects();
|
||||
}
|
||||
}
|
||||
|
||||
async reanalyseActiveFile() {
|
||||
await this._reanalysisControllerService
|
||||
.reanalyzeFile(
|
||||
this._appState.activeProject.project.projectId,
|
||||
this._appState.activeFile.fileId
|
||||
)
|
||||
.toPromise();
|
||||
await this.reloadActiveProjectFiles();
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -13,17 +13,17 @@
|
||||
}
|
||||
|
||||
.mat-checkbox-layout {
|
||||
.mat-checkbox-inner-container {
|
||||
margin-left: 0;
|
||||
}
|
||||
.mat-checkbox-inner-container {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.mat-checkbox-label {
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-size: 13px;
|
||||
color: $accent;
|
||||
.mat-checkbox-label {
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-size: 13px;
|
||||
color: $accent;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
.mat-dialog-container {
|
||||
border-radius: 8px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.dialog {
|
||||
|
||||
@ -1,63 +1,65 @@
|
||||
@import "red-variables";
|
||||
@import "red-mixins";
|
||||
@import 'red-variables';
|
||||
@import 'red-mixins';
|
||||
|
||||
.red-input-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: 13px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: 13px;
|
||||
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
input, textarea, mat-select {
|
||||
box-sizing: border-box;
|
||||
width: 322px;
|
||||
padding-left: 11px;
|
||||
padding-right: 11px;
|
||||
border: 1px solid $grey-5;
|
||||
font-family: Inter, sans-serif;
|
||||
font-size: 11px;
|
||||
line-height: 14px;
|
||||
background-color: #FFFFFF;
|
||||
border-radius: 8px;
|
||||
outline: none;
|
||||
margin-top: 3px;
|
||||
min-height: 34px;
|
||||
|
||||
&:focus {
|
||||
border-color: $grey-1;
|
||||
}
|
||||
}
|
||||
|
||||
mat-select {
|
||||
width: 220px;
|
||||
|
||||
.mat-select-trigger {
|
||||
height: 32px;
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.mat-select-value {
|
||||
vertical-align: middle;
|
||||
input,
|
||||
textarea,
|
||||
mat-select {
|
||||
box-sizing: border-box;
|
||||
width: 322px;
|
||||
padding-left: 11px;
|
||||
padding-right: 11px;
|
||||
border: 1px solid $grey-5;
|
||||
font-family: Inter, sans-serif;
|
||||
font-size: 11px;
|
||||
line-height: 14px;
|
||||
background-color: #ffffff;
|
||||
border-radius: 8px;
|
||||
outline: none;
|
||||
margin-top: 3px;
|
||||
min-height: 34px;
|
||||
|
||||
&:focus {
|
||||
border-color: $grey-1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
textarea {
|
||||
resize: none;
|
||||
padding-top: 7px;
|
||||
padding-bottom: 7px;
|
||||
@include no-scroll-bar();
|
||||
}
|
||||
mat-select {
|
||||
width: 220px;
|
||||
|
||||
label {
|
||||
opacity: 0.7;
|
||||
font-size: 11px;
|
||||
letter-spacing: 0;
|
||||
line-height: 14px;
|
||||
margin-bottom: 2px;
|
||||
.mat-select-trigger {
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
&.mat-checkbox-layout {
|
||||
opacity: 1;
|
||||
.mat-select-value {
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
textarea {
|
||||
resize: none;
|
||||
padding-top: 7px;
|
||||
padding-bottom: 7px;
|
||||
@include no-scroll-bar();
|
||||
}
|
||||
|
||||
label {
|
||||
opacity: 0.7;
|
||||
font-size: 11px;
|
||||
letter-spacing: 0;
|
||||
line-height: 14px;
|
||||
margin-bottom: 2px;
|
||||
|
||||
&.mat-checkbox-layout {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,29 +1,29 @@
|
||||
@import "red-variables";
|
||||
@import 'red-variables';
|
||||
|
||||
.mat-menu-panel {
|
||||
border-radius: 8px !important;
|
||||
box-shadow: 0 2px 6px 0 rgba(40, 50, 65, 0.3);
|
||||
max-width: none !important;
|
||||
border-radius: 8px !important;
|
||||
box-shadow: 0 2px 6px 0 rgba(40, 50, 65, 0.3);
|
||||
max-width: none !important;
|
||||
|
||||
.mat-menu-item {
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-size: 13px;
|
||||
color: $accent;
|
||||
.mat-menu-item {
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-size: 13px;
|
||||
color: $accent;
|
||||
|
||||
.arrow-wrapper {
|
||||
width: 16px;
|
||||
margin-right: 8px;
|
||||
text-align: center;
|
||||
.arrow-wrapper {
|
||||
width: 16px;
|
||||
margin-right: 8px;
|
||||
text-align: center;
|
||||
|
||||
mat-icon {
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
margin: 0;
|
||||
}
|
||||
mat-icon {
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.padding-left {
|
||||
padding-left: 64px;
|
||||
}
|
||||
}
|
||||
|
||||
&.padding-left {
|
||||
padding-left: 64px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,10 +10,10 @@
|
||||
}
|
||||
|
||||
@mixin no-scroll-bar {
|
||||
scrollbar-width: none; /* Firefox */
|
||||
-ms-overflow-style: none; /* IE 10+ */
|
||||
&::-webkit-scrollbar {
|
||||
width: 0;
|
||||
background: transparent; /* Chrome/Safari/Webkit */
|
||||
}
|
||||
scrollbar-width: none; /* Firefox */
|
||||
-ms-overflow-style: none; /* IE 10+ */
|
||||
&::-webkit-scrollbar {
|
||||
width: 0;
|
||||
background: transparent; /* Chrome/Safari/Webkit */
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,181 +1,182 @@
|
||||
@import "red-variables";
|
||||
@import 'red-variables';
|
||||
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100vh;
|
||||
font-family: Inter, sans-serif;
|
||||
color: $accent;
|
||||
font-size: 13px;
|
||||
line-height: 16px;
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100vh;
|
||||
font-family: Inter, sans-serif;
|
||||
color: $accent;
|
||||
font-size: 13px;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 24px;
|
||||
height: 50px;
|
||||
box-shadow: 0 2px 4px 0 $grey-4;
|
||||
position: fixed;
|
||||
top: 61px;
|
||||
width: 100vw;
|
||||
box-sizing: border-box;
|
||||
background-color: $white;
|
||||
z-index: 3;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 24px;
|
||||
height: 50px;
|
||||
box-shadow: 0 2px 4px 0 $grey-4;
|
||||
position: fixed;
|
||||
top: 61px;
|
||||
width: 100vw;
|
||||
box-sizing: border-box;
|
||||
background-color: $white;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.red-content-inner {
|
||||
margin-top: 50px;
|
||||
margin-top: 50px;
|
||||
}
|
||||
|
||||
.right-fixed-container {
|
||||
border-left: 1px solid $grey-4;
|
||||
height: 100%;
|
||||
width: $right-container-inside-width;
|
||||
padding: $right-container-padding;
|
||||
position: fixed;
|
||||
right: 0;
|
||||
border-left: 1px solid $grey-4;
|
||||
height: 100%;
|
||||
width: $right-container-inside-width;
|
||||
padding: $right-container-padding;
|
||||
position: fixed;
|
||||
right: 0;
|
||||
|
||||
.actions-row {
|
||||
display: flex;
|
||||
height: 40px;
|
||||
.actions-row {
|
||||
display: flex;
|
||||
height: 40px;
|
||||
|
||||
> div {
|
||||
padding: 10px;
|
||||
> div {
|
||||
padding: 10px;
|
||||
|
||||
mat-icon {
|
||||
cursor: pointer;
|
||||
width: 14px;
|
||||
mat-icon {
|
||||
cursor: pointer;
|
||||
width: 14px;
|
||||
|
||||
&:hover {
|
||||
color: $primary;
|
||||
&:hover {
|
||||
color: $primary;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.filters {
|
||||
div {
|
||||
margin-right: 6px;
|
||||
}
|
||||
div {
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
button {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
button {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
}
|
||||
|
||||
.flex-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex !important;
|
||||
display: flex !important;
|
||||
}
|
||||
|
||||
.flex-end {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.flex-1 {
|
||||
flex: 1;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.flex-2 {
|
||||
flex: 2;
|
||||
flex: 2;
|
||||
}
|
||||
|
||||
.mt-5 {
|
||||
margin-top: 5px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.mt-20 {
|
||||
margin-top: 20px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.left-container {
|
||||
height: calc(100vh - 61px - 50px);
|
||||
width: calc(100vw - #{$right-container-width});
|
||||
height: calc(100vh - 61px - 50px);
|
||||
width: calc(100vw - #{$right-container-width});
|
||||
}
|
||||
|
||||
.break-20 {
|
||||
height: 20px;
|
||||
background: transparent;
|
||||
display: block;
|
||||
height: 20px;
|
||||
background: transparent;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.detail-row {
|
||||
opacity: 1;
|
||||
font-family: Inconsolata, monospace, monospace;
|
||||
font-size: 14px;
|
||||
letter-spacing: 0;
|
||||
line-height: 14px;
|
||||
padding: 4px;
|
||||
opacity: 1;
|
||||
font-family: Inconsolata, monospace, monospace;
|
||||
font-size: 14px;
|
||||
letter-spacing: 0;
|
||||
line-height: 14px;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.red-top-bar {
|
||||
height: 61px;
|
||||
width: 100%;
|
||||
max-height: 61px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.top-bar-row {
|
||||
height: 60px;
|
||||
height: 61px;
|
||||
width: 100%;
|
||||
max-height: 61px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0 24px;
|
||||
flex-direction: column;
|
||||
|
||||
.center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
.top-bar-row {
|
||||
height: 60px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0 24px;
|
||||
|
||||
.center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.app-name {
|
||||
margin-left: 16px;
|
||||
height: 20px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.menu {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
|
||||
&.right {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.app-name {
|
||||
margin-left: 16px;
|
||||
height: 20px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0;
|
||||
line-height: 20px;
|
||||
.divider {
|
||||
height: 1px;
|
||||
opacity: 0.15;
|
||||
background-color: $grey-1;
|
||||
}
|
||||
|
||||
.menu {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
|
||||
&.right {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.divider {
|
||||
height: 1px;
|
||||
opacity: 0.15;
|
||||
background-color: $grey-1;
|
||||
}
|
||||
}
|
||||
|
||||
.red-content {
|
||||
width: 100vw;
|
||||
height: calc(100vh - 61px);
|
||||
overflow: auto;
|
||||
width: 100vw;
|
||||
height: calc(100vh - 61px);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none !important;
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.not-visible {
|
||||
visibility: hidden;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.pointer {
|
||||
cursor: pointer;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@ -1,98 +1,97 @@
|
||||
@import "red-variables";
|
||||
@import "red-mixins";
|
||||
@import 'red-variables';
|
||||
@import 'red-mixins';
|
||||
|
||||
.table-header {
|
||||
background-color: rgba(226, 228, 233, 0.9);
|
||||
height: 50px;
|
||||
padding: 0 16px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.actions {
|
||||
background-color: rgba(226, 228, 233, 0.9);
|
||||
height: 50px;
|
||||
padding: 0 16px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 25px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 25px;
|
||||
}
|
||||
}
|
||||
|
||||
.grid-container {
|
||||
display: grid;
|
||||
position: relative;
|
||||
display: grid;
|
||||
position: relative;
|
||||
|
||||
.no-data {
|
||||
grid-column: 1/-1;
|
||||
}
|
||||
|
||||
.table-col-name {
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding: 8px 16px;
|
||||
border-bottom: 1px solid rgba(226, 228, 233, 0.9);
|
||||
align-items: center;
|
||||
text-transform: uppercase;
|
||||
|
||||
.sort-arrows-container {
|
||||
mat-icon {
|
||||
display: block;
|
||||
width: 6px;
|
||||
height: 11px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.table-item {
|
||||
display: contents;
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
.no-data {
|
||||
grid-column: 1/-1;
|
||||
}
|
||||
|
||||
.table-item-title {
|
||||
font-weight: 600;
|
||||
@include line-clamp(1);
|
||||
}
|
||||
|
||||
> div {
|
||||
height: 80px;
|
||||
border-bottom: 1px solid rgba(226, 228, 233, 0.9);
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
|
||||
.action-buttons {
|
||||
position: absolute;
|
||||
display: none;
|
||||
right: 0;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
width: fit-content;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding-left: 100px;
|
||||
padding-right: 8px;
|
||||
background: linear-gradient(to right, rgba(244, 245, 247, 0) 0%, #F4F5F7 35%);
|
||||
|
||||
mat-icon {
|
||||
width: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
> div {
|
||||
background-color: #F9FAFB;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
.table-col-name {
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
}
|
||||
gap: 8px;
|
||||
padding: 8px 16px;
|
||||
border-bottom: 1px solid rgba(226, 228, 233, 0.9);
|
||||
align-items: center;
|
||||
text-transform: uppercase;
|
||||
|
||||
.sort-arrows-container {
|
||||
mat-icon {
|
||||
display: block;
|
||||
width: 6px;
|
||||
height: 11px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.table-item {
|
||||
display: contents;
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.table-item-title {
|
||||
font-weight: 600;
|
||||
@include line-clamp(1);
|
||||
}
|
||||
|
||||
> div {
|
||||
height: 80px;
|
||||
border-bottom: 1px solid rgba(226, 228, 233, 0.9);
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
position: absolute;
|
||||
display: none;
|
||||
right: 0;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
width: fit-content;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding-left: 100px;
|
||||
padding-right: 8px;
|
||||
background: linear-gradient(to right, rgba(244, 245, 247, 0) 0%, #f4f5f7 35%);
|
||||
|
||||
mat-icon {
|
||||
width: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
> div {
|
||||
background-color: #f9fafb;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user