refactor permissions
This commit is contained in:
parent
d7f8fe6508
commit
1c5f8fd006
@ -1,6 +1,6 @@
|
||||
{
|
||||
"useTabs": false,
|
||||
"printWidth": 100,
|
||||
"printWidth": 160,
|
||||
"tabWidth": 4,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
|
||||
@ -58,11 +58,9 @@ import { AssignOwnerDialogComponent } from './dialogs/assign-owner-dialog/assign
|
||||
import { MatDatepickerModule } from '@angular/material/datepicker';
|
||||
import { MatNativeDateModule } from '@angular/material/core';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { ProjectMemberGuard } from './auth/project-member-guard.service';
|
||||
import { HumanizePipe } from './utils/humanize.pipe';
|
||||
import { CommentsComponent } from './components/comments/comments.component';
|
||||
import { ManualAnnotationDialogComponent } from './dialogs/manual-redaction-dialog/manual-annotation-dialog.component';
|
||||
import { FileNotAvailableOverlayComponent } from './screens/file/file-not-available-overlay/file-not-available-overlay.component';
|
||||
import { ToastComponent } from './components/toast/toast.component';
|
||||
import { FilterComponent } from './common/filter/filter.component';
|
||||
import { AppInfoComponent } from './screens/app-info/app-info.component';
|
||||
@ -75,6 +73,7 @@ import { ProjectOverviewEmptyComponent } from './screens/empty-states/project-ov
|
||||
import { ProjectListingEmptyComponent } from './screens/empty-states/project-listing-empty/project-listing-empty.component';
|
||||
import { AnnotationActionsComponent } from './screens/file/annotation-actions/annotation-actions.component';
|
||||
import { ProjectListingDetailsComponent } from './screens/project-listing-screen/project-listing-details/project-listing-details.component';
|
||||
import { FileActionsComponent } from './common/file-actions/file-actions.component';
|
||||
|
||||
export function HttpLoaderFactory(httpClient: HttpClient) {
|
||||
return new TranslateHttpLoader(httpClient, '/assets/i18n/', '.json');
|
||||
@ -104,7 +103,6 @@ export function HttpLoaderFactory(httpClient: HttpClient) {
|
||||
CommentsComponent,
|
||||
HumanizePipe,
|
||||
ToastComponent,
|
||||
FileNotAvailableOverlayComponent,
|
||||
FilterComponent,
|
||||
AppInfoComponent,
|
||||
SortingComponent,
|
||||
@ -116,7 +114,8 @@ export function HttpLoaderFactory(httpClient: HttpClient) {
|
||||
ProjectListingEmptyComponent,
|
||||
AnnotationActionsComponent,
|
||||
ProjectListingEmptyComponent,
|
||||
ProjectListingDetailsComponent
|
||||
ProjectListingDetailsComponent,
|
||||
FileActionsComponent
|
||||
],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
@ -226,11 +225,7 @@ export function HttpLoaderFactory(httpClient: HttpClient) {
|
||||
export class AppModule {
|
||||
constructor(private router: Router, private route: ActivatedRoute) {
|
||||
route.queryParamMap.subscribe((queryParams) => {
|
||||
if (
|
||||
queryParams.has('code') ||
|
||||
queryParams.has('state') ||
|
||||
queryParams.has('session_state')
|
||||
) {
|
||||
if (queryParams.has('code') || queryParams.has('state') || queryParams.has('session_state')) {
|
||||
this.router.navigate([], {
|
||||
queryParams: {
|
||||
state: null,
|
||||
|
||||
@ -0,0 +1,80 @@
|
||||
<button
|
||||
(click)="openDeleteFileDialog($event, fileStatus)"
|
||||
*ngIf="permissionsService.canDeleteFile(fileStatus)"
|
||||
[matTooltip]="'project-overview.delete.action' | translate"
|
||||
matTooltipPosition="above"
|
||||
color="accent"
|
||||
mat-icon-button
|
||||
>
|
||||
<mat-icon svgIcon="red:trash"></mat-icon>
|
||||
</button>
|
||||
|
||||
<div
|
||||
[matTooltip]="(fileStatus.isApproved ? 'report.action' : 'report.unavailable-single') | translate"
|
||||
*ngIf="permissionsService.canShowRedactionReportDownloadBtn()"
|
||||
matTooltipPosition="above"
|
||||
>
|
||||
<button (click)="downloadFileRedactionReport($event, fileStatus)" [disabled]="!fileStatus.isApproved" color="accent" mat-icon-button>
|
||||
<mat-icon svgIcon="red:report"></mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
(click)="assignReviewer($event, fileStatus)"
|
||||
*ngIf="permissionsService.canAssignReviewer(fileStatus)"
|
||||
[matTooltip]="'project-overview.assign.action' | translate"
|
||||
matTooltipPosition="above"
|
||||
color="accent"
|
||||
mat-icon-button
|
||||
>
|
||||
<mat-icon svgIcon="red:assign"></mat-icon>
|
||||
</button>
|
||||
<button
|
||||
(click)="reanalyseFile($event, fileStatus)"
|
||||
*ngIf="permissionsService.canReanalyseFile(fileStatus)"
|
||||
[matTooltip]="'project-overview.reanalyse.action' | translate"
|
||||
matTooltipPosition="above"
|
||||
color="accent"
|
||||
mat-icon-button
|
||||
>
|
||||
<mat-icon svgIcon="red:refresh"></mat-icon>
|
||||
</button>
|
||||
<button
|
||||
(click)="setFileApproved($event, fileStatus)"
|
||||
*ngIf="permissionsService.canApprove(fileStatus)"
|
||||
[matTooltip]="'project-overview.approve' | translate"
|
||||
matTooltipPosition="above"
|
||||
color="accent"
|
||||
mat-icon-button
|
||||
>
|
||||
<mat-icon svgIcon="red:check-alt"></mat-icon>
|
||||
</button>
|
||||
<button
|
||||
(click)="setFileUnderApproval($event, fileStatus)"
|
||||
*ngIf="permissionsService.canSetUnderApproval(fileStatus)"
|
||||
[matTooltip]="'project-overview.under-approval' | translate"
|
||||
matTooltipPosition="above"
|
||||
color="accent"
|
||||
mat-icon-button
|
||||
>
|
||||
<mat-icon svgIcon="red:check-alt"></mat-icon>
|
||||
</button>
|
||||
<button
|
||||
(click)="setFileUnderApproval($event, fileStatus)"
|
||||
*ngIf="permissionsService.canUndoApproval(fileStatus)"
|
||||
[matTooltip]="'project-overview.under-approval' | translate"
|
||||
matTooltipPosition="above"
|
||||
color="accent"
|
||||
mat-icon-button
|
||||
>
|
||||
<mat-icon svgIcon="red:undo"></mat-icon>
|
||||
</button>
|
||||
<button
|
||||
(click)="setFileUnderReview($event, fileStatus)"
|
||||
*ngIf="permissionsService.canUndoUnderApproval(fileStatus)"
|
||||
[matTooltip]="'project-overview.under-review' | translate"
|
||||
matTooltipPosition="above"
|
||||
color="accent"
|
||||
mat-icon-button
|
||||
>
|
||||
<mat-icon svgIcon="red:undo"></mat-icon>
|
||||
</button>
|
||||
@ -0,0 +1,75 @@
|
||||
import { Component, Input, OnInit, Output, EventEmitter } from '@angular/core';
|
||||
import { PermissionsService } from '../service/permissions.service';
|
||||
import { FileStatusWrapper } from '../../screens/file/model/file-status.wrapper';
|
||||
import { DialogService } from '../../dialogs/dialog.service';
|
||||
import { AppStateService } from '../../state/app-state.service';
|
||||
import { FileActionService } from '../../screens/file/service/file-action.service';
|
||||
|
||||
@Component({
|
||||
selector: 'redaction-file-actions',
|
||||
templateUrl: './file-actions.component.html',
|
||||
styleUrls: ['./file-actions.component.scss']
|
||||
})
|
||||
export class FileActionsComponent implements OnInit {
|
||||
@Input() fileStatus: FileStatusWrapper;
|
||||
@Output() actionPerformed = new EventEmitter<string>();
|
||||
|
||||
constructor(
|
||||
public readonly permissionsService: PermissionsService,
|
||||
private readonly _dialogService: DialogService,
|
||||
private readonly _appStateService: AppStateService,
|
||||
private readonly _fileActionService: FileActionService
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {}
|
||||
|
||||
openDeleteFileDialog($event: MouseEvent, fileStatusWrapper: FileStatusWrapper) {
|
||||
this._dialogService.openDeleteFileDialog($event, fileStatusWrapper.projectId, fileStatusWrapper.fileId, () => {
|
||||
this.actionPerformed.emit('delete');
|
||||
});
|
||||
}
|
||||
|
||||
downloadFileRedactionReport($event: MouseEvent, file: FileStatusWrapper) {
|
||||
$event.stopPropagation();
|
||||
this._appStateService.downloadFileRedactionReport(file);
|
||||
}
|
||||
|
||||
assignReviewer($event: MouseEvent, file: FileStatusWrapper) {
|
||||
$event.stopPropagation();
|
||||
this._fileActionService.assignProjectReviewer(file, () => this.actionPerformed.emit('assign-reviewer'));
|
||||
}
|
||||
|
||||
reanalyseFile($event: MouseEvent, fileStatusWrapper: FileStatusWrapper) {
|
||||
$event.stopPropagation();
|
||||
this._fileActionService.reanalyseFile(fileStatusWrapper).subscribe(() => {
|
||||
this.reloadProjects('reanalyse');
|
||||
});
|
||||
}
|
||||
|
||||
setFileUnderApproval($event: MouseEvent, fileStatus: FileStatusWrapper) {
|
||||
$event.stopPropagation();
|
||||
this._fileActionService.setFileUnderApproval(fileStatus).subscribe(() => {
|
||||
this.reloadProjects('set-under-approval');
|
||||
});
|
||||
}
|
||||
|
||||
setFileApproved($event: MouseEvent, fileStatus: FileStatusWrapper) {
|
||||
$event.stopPropagation();
|
||||
this._fileActionService.setFileApproved(fileStatus).subscribe(() => {
|
||||
this.reloadProjects('set-approved');
|
||||
});
|
||||
}
|
||||
|
||||
setFileUnderReview($event: MouseEvent, fileStatus: FileStatusWrapper) {
|
||||
$event.stopPropagation();
|
||||
this._fileActionService.setFileUnderReview(fileStatus).subscribe(() => {
|
||||
this.reloadProjects('set-review');
|
||||
});
|
||||
}
|
||||
|
||||
public reloadProjects(action: string) {
|
||||
this._appStateService.getFiles().then(() => {
|
||||
this.actionPerformed.emit(action);
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -1,16 +1,8 @@
|
||||
import {
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
EventEmitter,
|
||||
Input,
|
||||
OnChanges,
|
||||
Output,
|
||||
SimpleChanges,
|
||||
TemplateRef
|
||||
} from '@angular/core';
|
||||
import { ChangeDetectorRef, Component, EventEmitter, Input, OnChanges, Output, SimpleChanges, TemplateRef } from '@angular/core';
|
||||
import { AppStateService } from '../../state/app-state.service';
|
||||
import { FilterModel } from './model/filter.model';
|
||||
import { handleCheckedValue } from './utils/filter-utils';
|
||||
import { PermissionsService } from '../service/permissions.service';
|
||||
|
||||
@Component({
|
||||
selector: 'redaction-filter',
|
||||
@ -25,10 +17,7 @@ export class FilterComponent implements OnChanges {
|
||||
@Input() hasArrow = true;
|
||||
@Input() icon: string;
|
||||
|
||||
constructor(
|
||||
public readonly appStateService: AppStateService,
|
||||
private readonly _changeDetectorRef: ChangeDetectorRef
|
||||
) {}
|
||||
constructor(public readonly appStateService: AppStateService, private readonly _changeDetectorRef: ChangeDetectorRef) {}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
if (changes.filters) {
|
||||
|
||||
@ -3,9 +3,9 @@ import { FileStatusWrapper } from '../../../screens/file/model/file-status.wrapp
|
||||
import { ProjectWrapper } from '../../../state/app-state.service';
|
||||
|
||||
export const RedactionFilterSorter = {
|
||||
hints: 1,
|
||||
redactions: 2,
|
||||
requests: 3,
|
||||
hint: 1,
|
||||
redaction: 2,
|
||||
suggestion: 3,
|
||||
none: 4
|
||||
};
|
||||
|
||||
@ -22,12 +22,7 @@ export function handleCheckedValue(filter: FilterModel) {
|
||||
}
|
||||
}
|
||||
|
||||
export function checkFilter(
|
||||
entity: any,
|
||||
filters: FilterModel[],
|
||||
validate: Function,
|
||||
matchAll: boolean = false
|
||||
) {
|
||||
export function checkFilter(entity: any, filters: FilterModel[], validate: Function, matchAll: boolean = false) {
|
||||
const hasChecked = filters.find((f) => f.checked);
|
||||
|
||||
if (!hasChecked) {
|
||||
@ -48,8 +43,7 @@ export function checkFilter(
|
||||
return filterMatched;
|
||||
}
|
||||
|
||||
export const keyChecker = (key: string) => (entity: any, filter: FilterModel) =>
|
||||
entity[key] === filter.key;
|
||||
export const keyChecker = (key: string) => (entity: any, filter: FilterModel) => entity[key] === filter.key;
|
||||
|
||||
export const annotationFilterChecker = (f: FileStatusWrapper, filter: FilterModel) => {
|
||||
switch (filter.key) {
|
||||
@ -60,7 +54,7 @@ export const annotationFilterChecker = (f: FileStatusWrapper, filter: FilterMode
|
||||
return f.hasRedactions;
|
||||
}
|
||||
case 'hint': {
|
||||
return f.hasHints;
|
||||
return f.hintsOnly;
|
||||
}
|
||||
case 'none': {
|
||||
return f.hasNone;
|
||||
@ -68,23 +62,17 @@ export const annotationFilterChecker = (f: FileStatusWrapper, filter: FilterMode
|
||||
}
|
||||
};
|
||||
|
||||
export const projectStatusChecker = (pw: ProjectWrapper, filter: FilterModel) =>
|
||||
pw.hasStatus(filter.key);
|
||||
export const projectStatusChecker = (pw: ProjectWrapper, filter: FilterModel) => pw.hasStatus(filter.key);
|
||||
|
||||
export const projectMemberChecker = (pw: ProjectWrapper, filter: FilterModel) => {
|
||||
return pw.hasMember(filter.key);
|
||||
};
|
||||
|
||||
export const dueDateChecker = (pw: ProjectWrapper, filter: FilterModel) =>
|
||||
pw.dueDateMatches(filter.key);
|
||||
export const dueDateChecker = (pw: ProjectWrapper, filter: FilterModel) => pw.dueDateMatches(filter.key);
|
||||
|
||||
export const addedDateChecker = (pw: ProjectWrapper, filter: FilterModel) =>
|
||||
pw.addedDateMatches(filter.key);
|
||||
export const addedDateChecker = (pw: ProjectWrapper, filter: FilterModel) => pw.addedDateMatches(filter.key);
|
||||
|
||||
export function getFilteredEntities(
|
||||
entities: any[],
|
||||
filters: { values: FilterModel[]; checker: Function; matchAll?: boolean }[]
|
||||
) {
|
||||
export function getFilteredEntities(entities: any[], filters: { values: FilterModel[]; checker: Function; matchAll?: boolean }[]) {
|
||||
const filteredEntities = [];
|
||||
for (const entity of entities) {
|
||||
let add = true;
|
||||
|
||||
120
apps/red-ui/src/app/common/service/permissions.service.ts
Normal file
120
apps/red-ui/src/app/common/service/permissions.service.ts
Normal file
@ -0,0 +1,120 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { AppStateService } from '../../state/app-state.service';
|
||||
import { UserService, UserWrapper } from '../../user/user.service';
|
||||
import { FileStatusWrapper } from '../../screens/file/model/file-status.wrapper';
|
||||
import { Project, User } from '@redaction/red-ui-http';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class PermissionsService {
|
||||
constructor(private readonly _appStateService: AppStateService, private _userService: UserService) {}
|
||||
|
||||
get currentUser() {
|
||||
return this._userService.user;
|
||||
}
|
||||
|
||||
public isManager(user?: User) {
|
||||
return this._userService.isManager();
|
||||
}
|
||||
|
||||
isReviewerOrOwner(fileStatus?: FileStatusWrapper, user?: User) {
|
||||
return this.isActiveFileDocumentReviewer() || this.isManagerAndOwner();
|
||||
}
|
||||
|
||||
canReanalyseFile(fileStatus?: FileStatusWrapper) {
|
||||
if (!fileStatus) {
|
||||
fileStatus = this._appStateService.activeFile;
|
||||
}
|
||||
// can reanalyse file if error, has requests not up to date with dictionary and is owner or reviewer
|
||||
return (
|
||||
((!fileStatus.isApproved && this._appStateService.fileNotUpToDateWithDictionary(fileStatus)) || fileStatus.isError || fileStatus.hasRequests) &&
|
||||
(this.isManagerAndOwner() || this.isActiveFileDocumentReviewer())
|
||||
);
|
||||
}
|
||||
|
||||
isActiveFileDocumentReviewer() {
|
||||
return this._appStateService.activeFile?.currentReviewer === this._userService.userId;
|
||||
}
|
||||
|
||||
canDeleteFile(fileStatus?: FileStatusWrapper) {
|
||||
return this.isManagerAndOwner() || fileStatus.isUnassigned;
|
||||
}
|
||||
|
||||
isApprovedOrUnderApproval(fileStatus?: FileStatusWrapper) {
|
||||
if (!fileStatus) {
|
||||
fileStatus = this._appStateService.activeFile;
|
||||
}
|
||||
return fileStatus.status === 'APPROVED' || fileStatus.status === 'UNDER_APPROVAL';
|
||||
}
|
||||
|
||||
isApproved(fileStatus?: FileStatusWrapper) {
|
||||
if (!fileStatus) {
|
||||
fileStatus = this._appStateService.activeFile;
|
||||
}
|
||||
return fileStatus.status === 'APPROVED';
|
||||
}
|
||||
|
||||
canApprove(fileStatus?: FileStatusWrapper) {
|
||||
if (!fileStatus) {
|
||||
fileStatus = this._appStateService.activeFile;
|
||||
}
|
||||
return fileStatus.status === 'UNDER_APPROVAL';
|
||||
}
|
||||
|
||||
canSetUnderApproval(fileStatus?: FileStatusWrapper) {
|
||||
if (!fileStatus) {
|
||||
fileStatus = this._appStateService.activeFile;
|
||||
}
|
||||
return fileStatus.status === 'UNDER_REVIEW';
|
||||
}
|
||||
|
||||
isManagerAndOwner(project?: Project, user?: UserWrapper) {
|
||||
if (!user) {
|
||||
user = this._userService.user;
|
||||
}
|
||||
if (!project) {
|
||||
project = this._appStateService.activeProject;
|
||||
}
|
||||
return user.isManager && project.ownerId === user.id;
|
||||
}
|
||||
|
||||
isProjectMember(project?: Project, user?: UserWrapper) {
|
||||
if (!user) {
|
||||
user = this._userService.user;
|
||||
}
|
||||
if (!project) {
|
||||
project = this._appStateService.activeProject;
|
||||
}
|
||||
return project.memberIds?.includes(user.id);
|
||||
}
|
||||
|
||||
canPerformAnnotationActions(fileStatus?: FileStatusWrapper) {
|
||||
if (!fileStatus) {
|
||||
fileStatus = this._appStateService.activeFile;
|
||||
}
|
||||
return (fileStatus.status === 'UNDER_APPROVAL' || fileStatus.status === 'UNDER_REVIEW') && this._userService.userId === fileStatus.currentReviewer;
|
||||
}
|
||||
|
||||
public canOpenFile(fileStatus: FileStatusWrapper) {
|
||||
if (!fileStatus) {
|
||||
fileStatus = this._appStateService.activeFile;
|
||||
}
|
||||
return !fileStatus.isError && !fileStatus.isProcessing;
|
||||
}
|
||||
|
||||
canShowRedactionReportDownloadBtn(fileStatus?: FileStatusWrapper) {
|
||||
return this.isManagerAndOwner() && !fileStatus.isError;
|
||||
}
|
||||
|
||||
canAssignReviewer(fileStatus?: FileStatusWrapper) {
|
||||
if (!fileStatus) {
|
||||
fileStatus = this._appStateService.activeFile;
|
||||
}
|
||||
return this.isProjectMember() && !fileStatus.isError && !fileStatus.isApprovedOrUnderApproval;
|
||||
}
|
||||
|
||||
canUndoApproval(fileStatus: any) {}
|
||||
|
||||
canUndoUnderApproval(fileStatus: any) {}
|
||||
}
|
||||
@ -3,10 +3,7 @@
|
||||
*ngIf="needsWorkInput.hasRedactions"
|
||||
[typeValue]="appStateService.getDictionaryTypeValue('redaction')"
|
||||
></redaction-annotation-icon>
|
||||
<redaction-annotation-icon
|
||||
*ngIf="needsWorkInput.hasHints"
|
||||
[typeValue]="appStateService.getDictionaryTypeValue('hint')"
|
||||
></redaction-annotation-icon>
|
||||
<redaction-annotation-icon *ngIf="needsWorkInput.hintsOnly" [typeValue]="appStateService.getDictionaryTypeValue('hint')"></redaction-annotation-icon>
|
||||
<redaction-annotation-icon
|
||||
*ngIf="needsWorkInput.hasRequests"
|
||||
[typeValue]="appStateService.getDictionaryTypeValue('suggestion')"
|
||||
|
||||
@ -2,7 +2,7 @@ import { Component, Input, OnInit } from '@angular/core';
|
||||
import { AppStateService } from '../../../state/app-state.service';
|
||||
|
||||
export interface NeedsWorkInput {
|
||||
hasHints?: boolean;
|
||||
hintsOnly?: boolean;
|
||||
hasRedactions?: boolean;
|
||||
hasRequests?: boolean;
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@ import { AppStateService } from '../../../state/app-state.service';
|
||||
import { TypeValue } from '@redaction/red-ui-http';
|
||||
import { ManualAnnotationService } from '../service/manual-annotation.service';
|
||||
import { Observable } from 'rxjs';
|
||||
import { PermissionsService } from '../../../common/service/permissions.service';
|
||||
|
||||
@Component({
|
||||
selector: 'redaction-annotation-actions',
|
||||
@ -21,6 +22,7 @@ export class AnnotationActionsComponent implements OnInit {
|
||||
|
||||
constructor(
|
||||
public appStateService: AppStateService,
|
||||
public permissionsService: PermissionsService,
|
||||
private readonly _manualAnnotationService: ManualAnnotationService
|
||||
) {}
|
||||
|
||||
@ -29,18 +31,12 @@ export class AnnotationActionsComponent implements OnInit {
|
||||
}
|
||||
|
||||
get canAcceptSuggestion() {
|
||||
return (
|
||||
this.appStateService.isActiveProjectOwnerAndManager &&
|
||||
(this.annotation.superType === 'suggestion' ||
|
||||
this.annotation.superType === 'suggestion-remove')
|
||||
);
|
||||
return this.permissionsService.isManagerAndOwner() && (this.annotation.superType === 'suggestion' || this.annotation.superType === 'suggestion-remove');
|
||||
}
|
||||
|
||||
acceptSuggestion($event: MouseEvent, annotation: AnnotationWrapper, addToDictionary: boolean) {
|
||||
$event.stopPropagation();
|
||||
this._processObsAndEmit(
|
||||
this._manualAnnotationService.approveRequest(annotation.id, addToDictionary)
|
||||
);
|
||||
this._processObsAndEmit(this._manualAnnotationService.approveRequest(annotation.id, addToDictionary));
|
||||
}
|
||||
|
||||
rejectSuggestion($event: MouseEvent, annotation: AnnotationWrapper) {
|
||||
@ -50,9 +46,7 @@ export class AnnotationActionsComponent implements OnInit {
|
||||
|
||||
suggestRemoveAnnotation($event: MouseEvent, annotation: AnnotationWrapper) {
|
||||
$event.stopPropagation();
|
||||
this._processObsAndEmit(
|
||||
this._manualAnnotationService.removeOrSuggestRemoveAnnotation(annotation)
|
||||
);
|
||||
this._processObsAndEmit(this._manualAnnotationService.removeOrSuggestRemoveAnnotation(annotation));
|
||||
}
|
||||
|
||||
undoDirectAction($event: MouseEvent, annotation: AnnotationWrapper) {
|
||||
|
||||
@ -1 +0,0 @@
|
||||
<p>file-not-available-overlay works!</p>
|
||||
@ -1,12 +0,0 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'redaction-file-not-available-overlay',
|
||||
templateUrl: './file-not-available-overlay.component.html',
|
||||
styleUrls: ['./file-not-available-overlay.component.scss']
|
||||
})
|
||||
export class FileNotAvailableOverlayComponent implements OnInit {
|
||||
constructor() {}
|
||||
|
||||
ngOnInit(): void {}
|
||||
}
|
||||
@ -3,131 +3,24 @@
|
||||
<div class="flex-1">
|
||||
<div
|
||||
class="fit-content"
|
||||
[matTooltip]="
|
||||
(canNotSwitchToRedactedView
|
||||
? 'file-preview.cannot-show-redacted-view'
|
||||
: 'file-preview.show-redacted-view'
|
||||
) | translate
|
||||
"
|
||||
[matTooltip]="(canNotSwitchToRedactedView ? 'file-preview.cannot-show-redacted-view' : 'file-preview.show-redacted-view') | translate"
|
||||
>
|
||||
<mat-slide-toggle
|
||||
[(ngModel)]="redactedView"
|
||||
color="primary"
|
||||
labelPosition="after"
|
||||
[disabled]="canNotSwitchToRedactedView"
|
||||
>
|
||||
<mat-slide-toggle [(ngModel)]="redactedView" color="primary" labelPosition="after" [disabled]="canNotSwitchToRedactedView">
|
||||
{{ 'file-preview.view-toggle' | translate }}
|
||||
</mat-slide-toggle>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 filename page-title">
|
||||
<span
|
||||
*ngIf="appStateService.fileNotUpToDateWithDictionary()"
|
||||
class="pill"
|
||||
translate="project-overview.new-rule.label"
|
||||
></span>
|
||||
<span
|
||||
*ngIf="!appStateService.canPerformAnnotationActionsOnCurrentFile()"
|
||||
class="pill"
|
||||
translate="readonly-pill"
|
||||
></span
|
||||
> <span>{{ appStateService.activeFile.filename }}</span>
|
||||
<span *ngIf="appStateService.fileNotUpToDateWithDictionary()" class="pill" translate="project-overview.new-rule.label"></span>
|
||||
<span *ngIf="!permissionsService.canPerformAnnotationActions()" class="pill" translate="readonly-pill"></span> <span>{{
|
||||
appStateService.activeFile.filename
|
||||
}}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 actions-container">
|
||||
<button
|
||||
(click)="openDeleteFileDialog($event)"
|
||||
*ngIf="userService.isManager(user)"
|
||||
mat-icon-button
|
||||
>
|
||||
<mat-icon svgIcon="red:trash"></mat-icon>
|
||||
</button>
|
||||
<div
|
||||
[matTooltip]="
|
||||
(appStateService.activeFile.isApproved
|
||||
? 'report.action'
|
||||
: 'report.unavailable-single'
|
||||
) | translate
|
||||
"
|
||||
matTooltipPosition="above"
|
||||
>
|
||||
<button
|
||||
mat-icon-button
|
||||
(click)="appStateService.downloadFileRedactionReport()"
|
||||
*ngIf="appStateService.isActiveProjectOwnerAndManager"
|
||||
[disabled]="!appStateService.activeFile.isApproved"
|
||||
color="accent"
|
||||
>
|
||||
<mat-icon svgIcon="red:report"></mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
<button (click)="assignReviewer()" *ngIf="!isApprovedOrUnderApproval()" mat-icon-button>
|
||||
<mat-icon svgIcon="red:assign"></mat-icon>
|
||||
</button>
|
||||
<button
|
||||
(click)="reanalyseFile($event)"
|
||||
[class.warn]="appStateService.fileNotUpToDateWithDictionary()"
|
||||
*ngIf="appStateService.canReanalyseFile()"
|
||||
mat-icon-button
|
||||
#reanalyseTooltip="matTooltip"
|
||||
[matTooltip]="
|
||||
appStateService.fileNotUpToDateWithDictionary()
|
||||
? ('file-preview.reanalyse-notification' | translate)
|
||||
: null
|
||||
"
|
||||
matTooltipClass="warn"
|
||||
>
|
||||
<mat-icon svgIcon="red:refresh"></mat-icon>
|
||||
</button>
|
||||
<button
|
||||
*ngIf="canApprove() && appStateService.isActiveProjectOwnerAndManager"
|
||||
(click)="requestApprovalOrApproveFile($event)"
|
||||
color="accent"
|
||||
mat-icon-button
|
||||
[matTooltip]="
|
||||
(appStateService.activeFile.status === 'UNDER_APPROVAL'
|
||||
? 'project-overview.approve'
|
||||
: 'project-overview.under-approval'
|
||||
) | translate
|
||||
"
|
||||
matTooltipPosition="above"
|
||||
>
|
||||
<mat-icon svgIcon="red:check-alt"></mat-icon>
|
||||
</button>
|
||||
<button
|
||||
(click)="undoApproveOrUnderApproval($event)"
|
||||
*ngIf="
|
||||
isApprovedOrUnderApproval() && appStateService.isActiveProjectOwnerAndManager
|
||||
"
|
||||
[matTooltip]="
|
||||
(appStateService.activeFile.status === 'APPROVED'
|
||||
? 'project-overview.under-approval'
|
||||
: 'project-overview.under-review'
|
||||
) | translate
|
||||
"
|
||||
matTooltipPosition="above"
|
||||
color="accent"
|
||||
mat-icon-button
|
||||
>
|
||||
<mat-icon svgIcon="red:undo"></mat-icon>
|
||||
</button>
|
||||
<button (click)="openFileDetailsDialog($event)" mat-icon-button>
|
||||
<mat-icon svgIcon="red:info"></mat-icon>
|
||||
</button>
|
||||
|
||||
<button
|
||||
(click)="downloadFile('REDACTED')"
|
||||
color="primary"
|
||||
class="custom-mini-fab"
|
||||
mat-mini-fab
|
||||
>
|
||||
<mat-icon svgIcon="red:download"></mat-icon>
|
||||
</button>
|
||||
<button
|
||||
[routerLink]="['/ui/projects/' + appStateService.activeProjectId]"
|
||||
mat-icon-button
|
||||
>
|
||||
<redaction-file-actions [fileStatus]="fileData.fileStatus" (actionPerformed)="fileActionPerformed()"></redaction-file-actions>
|
||||
<button [routerLink]="['/ui/projects/' + appStateService.activeProjectId]" mat-icon-button>
|
||||
<mat-icon svgIcon="red:close"></mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
@ -150,11 +43,7 @@
|
||||
|
||||
<div class="right-fixed-container">
|
||||
<div class="right-title heading" translate="file-preview.tabs.annotations.label">
|
||||
<redaction-filter
|
||||
(filtersChanged)="filtersChanged($event)"
|
||||
[filterTemplate]="annotationFilterTemplate"
|
||||
[filters]="filters"
|
||||
></redaction-filter>
|
||||
<redaction-filter (filtersChanged)="filtersChanged($event)" [filterTemplate]="annotationFilterTemplate" [filters]="filters"></redaction-filter>
|
||||
</div>
|
||||
|
||||
<div class="right-content">
|
||||
@ -186,9 +75,7 @@
|
||||
>
|
||||
<div *ngFor="let page of displayedPages">
|
||||
<div attr.anotation-page-header="{{ page }}" class="page-separator">
|
||||
<span class="all-caps-label"
|
||||
><span translate="page"></span> {{ page }}</span
|
||||
>
|
||||
<span class="all-caps-label"><span translate="page"></span> {{ page }}</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
@ -201,23 +88,17 @@
|
||||
>
|
||||
<div class="details">
|
||||
<redaction-annotation-icon
|
||||
[typeValue]="
|
||||
appStateService.getDictionaryTypeValueForAnnotation(
|
||||
annotation
|
||||
)
|
||||
"
|
||||
[typeValue]="appStateService.getDictionaryTypeValueForAnnotation(annotation)"
|
||||
></redaction-annotation-icon>
|
||||
<div class="flex-1">
|
||||
<div>
|
||||
<strong>{{ annotation.superType | humanize }}</strong>
|
||||
</div>
|
||||
<div *ngIf="annotation.dictionary">
|
||||
<strong><span translate="dictionary"></span>: </strong
|
||||
>{{ annotation.dictionary | humanize }}
|
||||
<strong><span translate="dictionary"></span>: </strong>{{ annotation.dictionary | humanize }}
|
||||
</div>
|
||||
<div *ngIf="annotation.content">
|
||||
<strong><span translate="content"></span>: </strong
|
||||
>{{ annotation.content }}
|
||||
<strong><span translate="content"></span>: </strong>{{ annotation.content }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -235,16 +116,11 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<redaction-full-page-loading-indicator
|
||||
[message]="loadingMessage"
|
||||
[displayed]="!viewReady"
|
||||
></redaction-full-page-loading-indicator>
|
||||
<redaction-full-page-loading-indicator [message]="loadingMessage" [displayed]="!viewReady"></redaction-full-page-loading-indicator>
|
||||
|
||||
<ng-template #annotationFilterTemplate let-filter="filter">
|
||||
<ng-container>
|
||||
<redaction-annotation-icon
|
||||
[typeValue]="appStateService.getDictionaryTypeValue(filter.key)"
|
||||
></redaction-annotation-icon>
|
||||
<redaction-annotation-icon [typeValue]="appStateService.getDictionaryTypeValue(filter.key)"></redaction-annotation-icon>
|
||||
{{ filter.label ? (filter.label | translate) : (filter.key | humanize) }}
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
|
||||
@ -1,23 +1,11 @@
|
||||
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 { ReanalysisControllerService } from '@redaction/red-ui-http';
|
||||
import { AppStateService } from '../../../state/app-state.service';
|
||||
import { WebViewerInstance } from '@pdftron/webviewer';
|
||||
import { PdfViewerComponent } from '../pdf-viewer/pdf-viewer.component';
|
||||
import { UserService } from '../../../user/user.service';
|
||||
import { debounce } from '../../../utils/debounce';
|
||||
import scrollIntoView from 'scroll-into-view-if-needed';
|
||||
import { FileDownloadService } from '../service/file-download.service';
|
||||
import { saveAs } from 'file-saver';
|
||||
import { FileType } from '../model/file-type';
|
||||
import { DialogService } from '../../../dialogs/dialog.service';
|
||||
import { MatDialogRef, MatDialogState } from '@angular/material/dialog';
|
||||
import { ManualRedactionEntryWrapper } from '../model/manual-redaction-entry.wrapper';
|
||||
@ -34,6 +22,7 @@ import { NotificationService } from '../../../notification/notification.service'
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { FileStatusWrapper } from '../model/file-status.wrapper';
|
||||
import { MatTooltip } from '@angular/material/tooltip';
|
||||
import { PermissionsService } from '../../../common/service/permissions.service';
|
||||
|
||||
const KEY_ARRAY = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'];
|
||||
|
||||
@ -53,21 +42,21 @@ export class FilePreviewScreenComponent implements OnInit {
|
||||
@ViewChild('quickNavigation') private _quickNavigationElement: ElementRef;
|
||||
@ViewChild('reanalyseTooltip') private _reanalyseTooltip: MatTooltip;
|
||||
|
||||
public fileData: FileDataModel;
|
||||
public fileId: string;
|
||||
public annotations: AnnotationWrapper[] = [];
|
||||
public displayedAnnotations: { [key: number]: { annotations: AnnotationWrapper[] } } = {};
|
||||
public selectedAnnotation: AnnotationWrapper;
|
||||
public pagesPanelActive = true;
|
||||
public viewReady = false;
|
||||
public filters: FilterModel[];
|
||||
fileData: FileDataModel;
|
||||
fileId: string;
|
||||
annotations: AnnotationWrapper[] = [];
|
||||
displayedAnnotations: { [key: number]: { annotations: AnnotationWrapper[] } } = {};
|
||||
selectedAnnotation: AnnotationWrapper;
|
||||
pagesPanelActive = true;
|
||||
viewReady = false;
|
||||
filters: FilterModel[];
|
||||
|
||||
loadingMessage: string;
|
||||
canPerformAnnotationActions: boolean;
|
||||
|
||||
constructor(
|
||||
public readonly appStateService: AppStateService,
|
||||
public readonly userService: UserService,
|
||||
public readonly permissionsService: PermissionsService,
|
||||
private readonly _changeDetectorRef: ChangeDetectorRef,
|
||||
private readonly _activatedRoute: ActivatedRoute,
|
||||
private readonly _dialogService: DialogService,
|
||||
@ -79,7 +68,6 @@ export class FilePreviewScreenComponent implements OnInit {
|
||||
private readonly _fileActionService: FileActionService,
|
||||
private readonly _manualAnnotationService: ManualAnnotationService,
|
||||
private readonly _fileDownloadService: FileDownloadService,
|
||||
private readonly _reanalysisControllerService: ReanalysisControllerService,
|
||||
private ngZone: NgZone
|
||||
) {
|
||||
this._activatedRoute.params.subscribe((params) => {
|
||||
@ -89,23 +77,19 @@ export class FilePreviewScreenComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
public get user() {
|
||||
return this.userService.user;
|
||||
}
|
||||
|
||||
public get redactedView() {
|
||||
get redactedView() {
|
||||
return this._activeViewer === 'REDACTED';
|
||||
}
|
||||
|
||||
public set redactedView(value: boolean) {
|
||||
set redactedView(value: boolean) {
|
||||
this._activeViewer = value ? 'REDACTED' : 'ANNOTATED';
|
||||
}
|
||||
|
||||
public get activeViewer() {
|
||||
get activeViewer() {
|
||||
return this.instance;
|
||||
}
|
||||
|
||||
public get displayedPages(): number[] {
|
||||
get displayedPages(): number[] {
|
||||
return Object.keys(this.displayedAnnotations).map((key) => Number(key));
|
||||
}
|
||||
|
||||
@ -114,14 +98,11 @@ export class FilePreviewScreenComponent implements OnInit {
|
||||
}
|
||||
|
||||
get canNotSwitchToRedactedView() {
|
||||
return (
|
||||
this.appStateService.fileNotUpToDateWithDictionary() ||
|
||||
this.fileData?.entriesToAdd?.length > 0
|
||||
);
|
||||
return this.appStateService.fileNotUpToDateWithDictionary() || this.fileData?.entriesToAdd?.length > 0;
|
||||
}
|
||||
|
||||
public ngOnInit(): void {
|
||||
this.canPerformAnnotationActions = this.appStateService.canPerformAnnotationActionsOnCurrentFile();
|
||||
ngOnInit(): void {
|
||||
this.canPerformAnnotationActions = this.permissionsService.canPerformAnnotationActions();
|
||||
this._loadFileData().subscribe(() => {});
|
||||
this.appStateService.fileReanalysed.subscribe((fileStatus: FileStatusWrapper) => {
|
||||
if (fileStatus.fileId === this.fileId) {
|
||||
@ -145,18 +126,12 @@ export class FilePreviewScreenComponent implements OnInit {
|
||||
|
||||
private _rebuildFilters() {
|
||||
const manualRedactionAnnotations = this.fileData.entriesToAdd.map((mr) =>
|
||||
AnnotationWrapper.fromManualRedaction(
|
||||
mr,
|
||||
this.fileData.manualRedactions,
|
||||
this.appStateService.dictionaryData,
|
||||
this.user
|
||||
)
|
||||
AnnotationWrapper.fromManualRedaction(mr, this.fileData.manualRedactions, this.appStateService.dictionaryData, this.permissionsService.currentUser)
|
||||
);
|
||||
const redactionLogAnnotations = this.fileData.redactionLog.redactionLogEntry.map((rde) =>
|
||||
AnnotationWrapper.fromRedactionLog(rde, this.fileData.manualRedactions, this.user)
|
||||
AnnotationWrapper.fromRedactionLog(rde, this.fileData.manualRedactions, this.permissionsService.currentUser)
|
||||
);
|
||||
|
||||
//this.annotations.splice(0, this.annotations.length);
|
||||
this.annotations = [];
|
||||
this.annotations.push(...manualRedactionAnnotations);
|
||||
this.annotations.push(...redactionLogAnnotations);
|
||||
@ -164,48 +139,13 @@ export class FilePreviewScreenComponent implements OnInit {
|
||||
this.filtersChanged(this.filters);
|
||||
}
|
||||
|
||||
public openFileDetailsDialog($event: MouseEvent) {
|
||||
this._dialogRef = this._dialogService.openFileDetailsDialog(
|
||||
$event,
|
||||
this.appStateService.activeFile
|
||||
);
|
||||
}
|
||||
|
||||
public reanalyseFile($event?: MouseEvent) {
|
||||
$event?.stopPropagation();
|
||||
this.viewReady = false;
|
||||
this.loadingMessage = 'file-preview.reanalyse-file';
|
||||
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 assignReviewer() {
|
||||
this._fileActionService.assignProjectReviewer(null, () => {
|
||||
this.canPerformAnnotationActions = this.appStateService.canPerformAnnotationActionsOnCurrentFile();
|
||||
});
|
||||
}
|
||||
|
||||
public handleAnnotationSelected(annotationId: string) {
|
||||
handleAnnotationSelected(annotationId: string) {
|
||||
this.selectedAnnotation = this.annotations.find((a) => a.id === annotationId);
|
||||
this.scrollToSelectedAnnotation();
|
||||
this._changeDetectorRef.detectChanges();
|
||||
}
|
||||
|
||||
public selectAnnotation(annotation: AnnotationWrapper) {
|
||||
selectAnnotation(annotation: AnnotationWrapper) {
|
||||
this._viewerComponent.selectAnnotation(annotation);
|
||||
}
|
||||
|
||||
@ -214,25 +154,20 @@ export class FilePreviewScreenComponent implements OnInit {
|
||||
if (!this.selectedAnnotation) {
|
||||
return;
|
||||
}
|
||||
const elements: any[] = this._annotationsElement.nativeElement.querySelectorAll(
|
||||
`div[annotation-id="${this.selectedAnnotation.id}"].active`
|
||||
);
|
||||
const elements: any[] = this._annotationsElement.nativeElement.querySelectorAll(`div[annotation-id="${this.selectedAnnotation.id}"].active`);
|
||||
this._scrollToFirstElement(elements);
|
||||
}
|
||||
|
||||
public selectPage(pageNumber: number) {
|
||||
selectPage(pageNumber: number) {
|
||||
this._viewerComponent.navigateToPage(pageNumber);
|
||||
this._scrollAnnotationsToPage(pageNumber, 'always');
|
||||
}
|
||||
|
||||
public openManualRedactionDialog($event: ManualRedactionEntryWrapper) {
|
||||
openManualRedactionDialog($event: ManualRedactionEntryWrapper) {
|
||||
this.ngZone.run(() => {
|
||||
this._dialogRef = this._dialogService.openManualRedactionDialog(
|
||||
$event,
|
||||
(response: ManualAnnotationResponse) => {
|
||||
this._cleanupAndRedrawManualAnnotations();
|
||||
}
|
||||
);
|
||||
this._dialogRef = this._dialogService.openManualRedactionDialog($event, (response: ManualAnnotationResponse) => {
|
||||
this._cleanupAndRedrawManualAnnotations();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@ -243,9 +178,7 @@ export class FilePreviewScreenComponent implements OnInit {
|
||||
}
|
||||
|
||||
private _scrollQuickNavigation() {
|
||||
const elements: any[] = this._quickNavigationElement.nativeElement.querySelectorAll(
|
||||
`#quick-nav-page-${this.activeViewerPage}`
|
||||
);
|
||||
const elements: any[] = this._quickNavigationElement.nativeElement.querySelectorAll(`#quick-nav-page-${this.activeViewerPage}`);
|
||||
this._scrollToFirstElement(elements);
|
||||
}
|
||||
|
||||
@ -257,16 +190,11 @@ export class FilePreviewScreenComponent implements OnInit {
|
||||
}
|
||||
|
||||
private _scrollAnnotationsToPage(page: number, mode: 'always' | 'if-needed' = 'if-needed') {
|
||||
const elements: any[] = this._annotationsElement.nativeElement.querySelectorAll(
|
||||
`div[anotation-page-header="${page}"]`
|
||||
);
|
||||
const elements: any[] = this._annotationsElement.nativeElement.querySelectorAll(`div[anotation-page-header="${page}"]`);
|
||||
this._scrollToFirstElement(elements, mode);
|
||||
}
|
||||
|
||||
private _scrollToFirstElement(
|
||||
elements: HTMLElement[],
|
||||
mode: 'always' | 'if-needed' = 'if-needed'
|
||||
) {
|
||||
private _scrollToFirstElement(elements: HTMLElement[], mode: 'always' | 'if-needed' = 'if-needed') {
|
||||
if (elements.length > 0) {
|
||||
scrollIntoView(elements[0], {
|
||||
behavior: 'smooth',
|
||||
@ -277,18 +205,9 @@ export class FilePreviewScreenComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
public downloadFile(type: FileType | string) {
|
||||
this._fileDownloadService.loadFile(type, this.fileId).subscribe((data) => {
|
||||
saveAs(data, this.appStateService.activeFile.filename);
|
||||
});
|
||||
}
|
||||
|
||||
@HostListener('window:keyup', ['$event'])
|
||||
public handleKeyEvent($event: KeyboardEvent) {
|
||||
if (
|
||||
!KEY_ARRAY.includes($event.key) ||
|
||||
this._dialogRef?.getState() === MatDialogState.OPEN
|
||||
) {
|
||||
handleKeyEvent($event: KeyboardEvent) {
|
||||
if (!KEY_ARRAY.includes($event.key) || this._dialogRef?.getState() === MatDialogState.OPEN) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -318,8 +237,7 @@ export class FilePreviewScreenComponent implements OnInit {
|
||||
|
||||
private _selectFirstAnnotationOnCurrentPageIfNecessary() {
|
||||
if (
|
||||
(!this.selectedAnnotation ||
|
||||
this.activeViewerPage !== this.selectedAnnotation.pageNumber) &&
|
||||
(!this.selectedAnnotation || this.activeViewerPage !== this.selectedAnnotation.pageNumber) &&
|
||||
this.displayedPages.indexOf(this.activeViewerPage) >= 0
|
||||
) {
|
||||
this.selectAnnotation(this.displayedAnnotations[this.activeViewerPage].annotations[0]);
|
||||
@ -327,16 +245,11 @@ export class FilePreviewScreenComponent implements OnInit {
|
||||
}
|
||||
|
||||
private _navigateAnnotations($event: KeyboardEvent) {
|
||||
if (
|
||||
!this.selectedAnnotation ||
|
||||
this.activeViewerPage !== this.selectedAnnotation.pageNumber
|
||||
) {
|
||||
if (!this.selectedAnnotation || this.activeViewerPage !== this.selectedAnnotation.pageNumber) {
|
||||
const pageIdx = this.displayedPages.indexOf(this.activeViewerPage);
|
||||
if (pageIdx !== -1) {
|
||||
// Displayed page has annotations
|
||||
this.selectAnnotation(
|
||||
this.displayedAnnotations[this.activeViewerPage].annotations[0]
|
||||
);
|
||||
this.selectAnnotation(this.displayedAnnotations[this.activeViewerPage].annotations[0]);
|
||||
} else {
|
||||
// Displayed page doesn't have annotations
|
||||
if ($event.key === 'ArrowDown') {
|
||||
@ -360,9 +273,7 @@ export class FilePreviewScreenComponent implements OnInit {
|
||||
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;
|
||||
const nextPageAnnotations = this.displayedAnnotations[this.displayedPages[pageIdx + 1]].annotations;
|
||||
this.selectAnnotation(nextPageAnnotations[0]);
|
||||
}
|
||||
} else {
|
||||
@ -371,9 +282,7 @@ export class FilePreviewScreenComponent implements OnInit {
|
||||
this.selectAnnotation(annotationsOnPage[idx - 1]);
|
||||
} else if (pageIdx) {
|
||||
// If not first page
|
||||
const prevPageAnnotations = this.displayedAnnotations[
|
||||
this.displayedPages[pageIdx - 1]
|
||||
].annotations;
|
||||
const prevPageAnnotations = this.displayedAnnotations[this.displayedPages[pageIdx - 1]].annotations;
|
||||
this.selectAnnotation(prevPageAnnotations[prevPageAnnotations.length - 1]);
|
||||
}
|
||||
}
|
||||
@ -451,10 +360,7 @@ export class FilePreviewScreenComponent implements OnInit {
|
||||
}
|
||||
|
||||
filtersChanged(filters: FilterModel[]) {
|
||||
this.displayedAnnotations = this._annotationProcessingService.filterAndGroupAnnotations(
|
||||
this.annotations,
|
||||
filters
|
||||
);
|
||||
this.displayedAnnotations = this._annotationProcessingService.filterAndGroupAnnotations(this.annotations, filters);
|
||||
this._changeDetectorRef.markForCheck();
|
||||
}
|
||||
|
||||
@ -465,62 +371,41 @@ export class FilePreviewScreenComponent implements OnInit {
|
||||
}
|
||||
|
||||
private _cleanupAndRedrawManualAnnotations() {
|
||||
this._fileDownloadService
|
||||
.loadActiveFileManualAnnotations()
|
||||
.subscribe((manualRedactions) => {
|
||||
const annotationsToRemove = [];
|
||||
this.fileData.entriesToAdd.forEach((manuallyAddedEntry) => {
|
||||
const annotation = this.activeViewer.annotManager.getAnnotationById(
|
||||
manuallyAddedEntry.id
|
||||
);
|
||||
if (annotation) {
|
||||
annotationsToRemove.push(annotation);
|
||||
}
|
||||
});
|
||||
this.activeViewer.annotManager.deleteAnnotations(annotationsToRemove, false, true);
|
||||
|
||||
this.fileData.manualRedactions = manualRedactions;
|
||||
this._annotationDrawService.drawAnnotations(
|
||||
this.instance,
|
||||
this.fileData.entriesToAdd
|
||||
);
|
||||
this._rebuildFilters();
|
||||
this._fileDownloadService.loadActiveFileManualAnnotations().subscribe((manualRedactions) => {
|
||||
const annotationsToRemove = [];
|
||||
this.fileData.entriesToAdd.forEach((manuallyAddedEntry) => {
|
||||
const annotation = this.activeViewer.annotManager.getAnnotationById(manuallyAddedEntry.id);
|
||||
if (annotation) {
|
||||
annotationsToRemove.push(annotation);
|
||||
}
|
||||
});
|
||||
}
|
||||
this.activeViewer.annotManager.deleteAnnotations(annotationsToRemove, false, true);
|
||||
|
||||
get fileReadyForDownload() {
|
||||
return this.appStateService.activeFile.status === 'APPROVED';
|
||||
}
|
||||
|
||||
isApprovedOrUnderApproval() {
|
||||
return (
|
||||
this.appStateService.activeFile.status === 'APPROVED' ||
|
||||
this.appStateService.activeFile.status === 'UNDER_APPROVAL'
|
||||
);
|
||||
}
|
||||
|
||||
isApproved() {
|
||||
return this.appStateService.activeFile.status === 'APPROVED';
|
||||
}
|
||||
|
||||
canApprove() {
|
||||
return (
|
||||
this.appStateService.activeFile.status === 'UNDER_REVIEW' ||
|
||||
this.appStateService.activeFile.status === 'UNDER_APPROVAL'
|
||||
);
|
||||
}
|
||||
|
||||
requestApprovalOrApproveFile($event: MouseEvent) {
|
||||
$event.stopPropagation();
|
||||
this._fileActionService.requestApprovalOrApproveFile().subscribe(() => {});
|
||||
}
|
||||
|
||||
undoApproveOrUnderApproval($event: MouseEvent) {
|
||||
$event.stopPropagation();
|
||||
this._fileActionService.undoApproveOrUnderApproval().subscribe(() => {});
|
||||
this.fileData.manualRedactions = manualRedactions;
|
||||
this._annotationDrawService.drawAnnotations(this.instance, this.fileData.entriesToAdd);
|
||||
this._rebuildFilters();
|
||||
});
|
||||
}
|
||||
|
||||
annotationsChangedByReviewAction() {
|
||||
this._cleanupAndRedrawManualAnnotations();
|
||||
}
|
||||
|
||||
async fileActionPerformed(action: string) {
|
||||
switch (action) {
|
||||
case 'delete':
|
||||
await this._router.navigate([`/ui/projects/${this.projectId}`]);
|
||||
break;
|
||||
|
||||
case 'reanalyse':
|
||||
this.viewReady = false;
|
||||
this.loadingMessage = 'file-preview.reanalyse-file';
|
||||
break;
|
||||
}
|
||||
|
||||
this.canPerformAnnotationActions = this.permissionsService.canPerformAnnotationActions();
|
||||
await this.appStateService.reloadActiveProjectFiles();
|
||||
}
|
||||
|
||||
// allManualRedactionsApplied
|
||||
}
|
||||
|
||||
@ -11,6 +11,7 @@ export class FileStatusWrapper {
|
||||
return this.fileStatus.added;
|
||||
}
|
||||
|
||||
// TODO use this for suggestions
|
||||
get allManualRedactionsApplied() {
|
||||
return this.fileStatus.allManualRedactionsApplied;
|
||||
}
|
||||
@ -39,6 +40,10 @@ export class FileStatusWrapper {
|
||||
return this.fileStatus.hasHints;
|
||||
}
|
||||
|
||||
get hintsOnly() {
|
||||
return this.fileStatus.hasHints && !this.fileStatus.hasRedactions;
|
||||
}
|
||||
|
||||
get hasRedactions() {
|
||||
return this.fileStatus.hasRedactions;
|
||||
}
|
||||
@ -79,6 +84,18 @@ export class FileStatusWrapper {
|
||||
return this.fileStatus.uploader;
|
||||
}
|
||||
|
||||
get isPending() {
|
||||
return this.status === FileStatus.StatusEnum.UNPROCESSED;
|
||||
}
|
||||
|
||||
get isProcessing() {
|
||||
return [FileStatus.StatusEnum.REPROCESS, FileStatus.StatusEnum.PROCESSING].includes(this.status);
|
||||
}
|
||||
|
||||
get isWorkable() {
|
||||
return !this.isProcessing && !this.isPending && !this.isError;
|
||||
}
|
||||
|
||||
get isApproved() {
|
||||
return this.fileStatus.status === 'APPROVED';
|
||||
}
|
||||
@ -99,7 +116,7 @@ export class FileStatusWrapper {
|
||||
}
|
||||
|
||||
get isUnassigned() {
|
||||
return this.status === 'UNASSIGNED';
|
||||
return !this.currentReviewer;
|
||||
}
|
||||
|
||||
get canApprove() {
|
||||
|
||||
@ -2,9 +2,10 @@ import { Injectable } from '@angular/core';
|
||||
import { DialogService } from '../../../dialogs/dialog.service';
|
||||
import { AppStateService } from '../../../state/app-state.service';
|
||||
import { UserService } from '../../../user/user.service';
|
||||
import { StatusControllerService } from '@redaction/red-ui-http';
|
||||
import { ReanalysisControllerService, StatusControllerService } from '@redaction/red-ui-http';
|
||||
import { FileStatus } from '@redaction/red-ui-http';
|
||||
import { FileStatusWrapper } from '../model/file-status.wrapper';
|
||||
import { PermissionsService } from '../../../common/service/permissions.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
@ -12,29 +13,31 @@ import { FileStatusWrapper } from '../model/file-status.wrapper';
|
||||
export class FileActionService {
|
||||
constructor(
|
||||
private readonly _dialogService: DialogService,
|
||||
private readonly _permissionsService: PermissionsService,
|
||||
private readonly _userService: UserService,
|
||||
private readonly _statusControllerService: StatusControllerService,
|
||||
private _appStateService: AppStateService
|
||||
private readonly _reanalysisControllerService: ReanalysisControllerService,
|
||||
private readonly _appStateService: AppStateService
|
||||
) {}
|
||||
|
||||
public reanalyseFile(fileStatusWrapper?: FileStatusWrapper) {
|
||||
if (!fileStatusWrapper) {
|
||||
fileStatusWrapper = this._appStateService.activeFile;
|
||||
}
|
||||
return this._reanalysisControllerService.reanalyzeFile(this._appStateService.activeProject.project.projectId, fileStatusWrapper.fileId);
|
||||
}
|
||||
|
||||
public assignProjectReviewer(file?: FileStatus, callback?: Function) {
|
||||
if (this._appStateService.isActiveProjectOwnerAndManager) {
|
||||
this._dialogService.openAssignFileReviewerDialog(
|
||||
file ? file : this._appStateService.activeFile,
|
||||
async () => {
|
||||
await this._appStateService.reloadActiveProjectFiles();
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
if (this._permissionsService.isManagerAndOwner()) {
|
||||
this._dialogService.openAssignFileReviewerDialog(file ? file : this._appStateService.activeFile, async () => {
|
||||
await this._appStateService.reloadActiveProjectFiles();
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
);
|
||||
});
|
||||
} else {
|
||||
this._statusControllerService
|
||||
.assignProjectOwner(
|
||||
this._appStateService.activeProjectId,
|
||||
file ? file.fileId : this._appStateService.activeFileId,
|
||||
this._userService.userId
|
||||
)
|
||||
.assignProjectOwner(this._appStateService.activeProjectId, file ? file.fileId : this._appStateService.activeFileId, this._userService.userId)
|
||||
.subscribe(async () => {
|
||||
await this._appStateService.reloadActiveProjectFiles();
|
||||
if (callback) {
|
||||
@ -44,46 +47,15 @@ export class FileActionService {
|
||||
}
|
||||
}
|
||||
|
||||
setUnderApproval(fileStatus: FileStatusWrapper) {
|
||||
return this._statusControllerService.setStatusUnderApproval(
|
||||
this._appStateService.activeProjectId,
|
||||
fileStatus.fileId
|
||||
);
|
||||
setFileUnderApproval(fileStatus: FileStatusWrapper) {
|
||||
return this._statusControllerService.setStatusUnderApproval(this._appStateService.activeProjectId, fileStatus.fileId);
|
||||
}
|
||||
|
||||
setApproved(fileStatus: FileStatusWrapper) {
|
||||
return this._statusControllerService.setStatusApproved(
|
||||
this._appStateService.activeProjectId,
|
||||
fileStatus.fileId
|
||||
);
|
||||
setFileApproved(fileStatus: FileStatusWrapper) {
|
||||
return this._statusControllerService.setStatusApproved(this._appStateService.activeProjectId, fileStatus.fileId);
|
||||
}
|
||||
|
||||
setReview(fileStatus: FileStatusWrapper) {
|
||||
return this._statusControllerService.setStatusUnderReview(
|
||||
this._appStateService.activeProjectId,
|
||||
fileStatus.fileId
|
||||
);
|
||||
}
|
||||
|
||||
requestApprovalOrApproveFile(fileStatusWrapper?: FileStatusWrapper) {
|
||||
if (!fileStatusWrapper) {
|
||||
fileStatusWrapper = this._appStateService.activeFile;
|
||||
}
|
||||
if (fileStatusWrapper.status === 'UNDER_REVIEW') {
|
||||
return this.setUnderApproval(fileStatusWrapper);
|
||||
} else {
|
||||
return this.setApproved(fileStatusWrapper);
|
||||
}
|
||||
}
|
||||
|
||||
undoApproveOrUnderApproval(fileStatusWrapper?: FileStatusWrapper) {
|
||||
if (!fileStatusWrapper) {
|
||||
fileStatusWrapper = this._appStateService.activeFile;
|
||||
}
|
||||
if (fileStatusWrapper.status === 'APPROVED') {
|
||||
return this.setUnderApproval(fileStatusWrapper);
|
||||
} else {
|
||||
return this.setReview(fileStatusWrapper);
|
||||
}
|
||||
setFileUnderReview(fileStatus: FileStatusWrapper) {
|
||||
return this._statusControllerService.setStatusUnderReview(this._appStateService.activeProjectId, fileStatus.fileId);
|
||||
}
|
||||
}
|
||||
|
||||
@ -32,13 +32,7 @@
|
||||
[icon]="'red:needs-work'"
|
||||
></redaction-filter>
|
||||
</div>
|
||||
<button
|
||||
(click)="openAddProjectDialog()"
|
||||
*ngIf="userService.isManager(user)"
|
||||
class="add-project-btn"
|
||||
color="primary"
|
||||
mat-flat-button
|
||||
>
|
||||
<button (click)="openAddProjectDialog()" *ngIf="userService.isManager(user)" class="add-project-btn" color="primary" mat-flat-button>
|
||||
<mat-icon svgIcon="red:plus"></mat-icon>
|
||||
<span translate="project-listing.add-new"></span>
|
||||
</button>
|
||||
@ -49,16 +43,9 @@
|
||||
<div class="grid-container bulk-select">
|
||||
<div class="header-item span-5">
|
||||
<div class="select-all-container">
|
||||
<div
|
||||
class="select-oval always-visible"
|
||||
[class.active]="areAllProjectsSelected()"
|
||||
(click)="toggleSelectAll()"
|
||||
></div>
|
||||
<div class="select-oval always-visible" [class.active]="areAllProjectsSelected()" (click)="toggleSelectAll()"></div>
|
||||
<span class="all-caps-label">
|
||||
{{
|
||||
'project-listing.table-header.title'
|
||||
| translate: { length: displayedProjects.length || 0 }
|
||||
}}
|
||||
{{ 'project-listing.table-header.title' | translate: { length: displayedProjects.length || 0 } }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@ -82,42 +69,22 @@
|
||||
(toggleSort)="sortingComponent.toggleSort($event)"
|
||||
></redaction-table-col-name>
|
||||
|
||||
<redaction-table-col-name
|
||||
label="project-listing.table-col-names.needs-work"
|
||||
></redaction-table-col-name>
|
||||
<redaction-table-col-name label="project-listing.table-col-names.needs-work"></redaction-table-col-name>
|
||||
|
||||
<redaction-table-col-name
|
||||
label="project-listing.table-col-names.owner"
|
||||
></redaction-table-col-name>
|
||||
<redaction-table-col-name label="project-listing.table-col-names.owner"></redaction-table-col-name>
|
||||
|
||||
<redaction-table-col-name
|
||||
label="project-listing.table-col-names.status"
|
||||
class="flex-end"
|
||||
></redaction-table-col-name>
|
||||
<redaction-table-col-name label="project-listing.table-col-names.status" class="flex-end"></redaction-table-col-name>
|
||||
|
||||
<div *ngIf="displayedProjects?.length === 0" class="no-data heading-l" translate="project-listing.no-projects-match"></div>
|
||||
|
||||
<div
|
||||
*ngIf="displayedProjects?.length === 0"
|
||||
class="no-data heading-l"
|
||||
translate="project-listing.no-projects-match"
|
||||
></div>
|
||||
|
||||
<div
|
||||
*ngFor="
|
||||
let pw of displayedProjects
|
||||
| sortBy: sortingOption.order:sortingOption.column
|
||||
"
|
||||
[routerLink]="[
|
||||
canOpenProject(pw) ? '/ui/projects/' + pw.project.projectId : []
|
||||
]"
|
||||
*ngFor="let pw of displayedProjects | sortBy: sortingOption.order:sortingOption.column"
|
||||
[routerLink]="[canOpenProject(pw) ? '/ui/projects/' + pw.project.projectId : []]"
|
||||
class="table-item"
|
||||
[class.pointer]="canOpenProject(pw)"
|
||||
>
|
||||
<div class="pr-0">
|
||||
<div
|
||||
class="select-oval"
|
||||
[class.active]="isProjectSelected(pw)"
|
||||
(click)="toggleProjectSelected($event, pw)"
|
||||
></div>
|
||||
<div class="select-oval" [class.active]="isProjectSelected(pw)" (click)="toggleProjectSelected($event, pw)"></div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@ -148,27 +115,19 @@
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<redaction-needs-work-badge
|
||||
[needsWorkInput]="pw"
|
||||
></redaction-needs-work-badge>
|
||||
<redaction-needs-work-badge [needsWorkInput]="pw"></redaction-needs-work-badge>
|
||||
</div>
|
||||
<div>
|
||||
<redaction-initials-avatar
|
||||
[userId]="pw.project.ownerId"
|
||||
withName="true"
|
||||
></redaction-initials-avatar>
|
||||
<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>
|
||||
<redaction-status-bar [config]="getProjectStatusConfig(pw)"></redaction-status-bar>
|
||||
|
||||
<div class="action-buttons" *ngIf="userService.isManager(user)">
|
||||
<div class="action-buttons" *ngIf="permissionsService.isManager()">
|
||||
<button
|
||||
(click)="openDeleteProjectDialog($event, pw.project)"
|
||||
[matTooltip]="'project-listing.delete.action' | translate"
|
||||
matTooltipPosition="above"
|
||||
*ngIf="userService.isManager(user)"
|
||||
color="accent"
|
||||
mat-icon-button
|
||||
>
|
||||
@ -179,7 +138,6 @@
|
||||
(click)="openEditProjectDialog($event, pw.project)"
|
||||
[matTooltip]="'project-listing.edit.action' | translate"
|
||||
matTooltipPosition="above"
|
||||
*ngIf="userService.isManager(user)"
|
||||
color="accent"
|
||||
mat-icon-button
|
||||
>
|
||||
@ -187,19 +145,11 @@
|
||||
</button>
|
||||
|
||||
<div
|
||||
[matTooltip]="
|
||||
(pw.allFilesApproved ? 'report.action' : 'report.unavailable')
|
||||
| translate
|
||||
"
|
||||
[matTooltip]="(pw.allFilesApproved ? 'report.action' : 'report.unavailable') | translate"
|
||||
matTooltipPosition="above"
|
||||
*ngIf="appStateService.isManagerAndOwner(pw.project) && pw.hasFiles"
|
||||
*ngIf="permissionsService.isManagerAndOwner(pw.project) && pw.hasFiles"
|
||||
>
|
||||
<button
|
||||
mat-icon-button
|
||||
(click)="downloadRedactionReport($event, pw.project)"
|
||||
[disabled]="!pw.allFilesApproved"
|
||||
color="accent"
|
||||
>
|
||||
<button mat-icon-button (click)="downloadRedactionReport($event, pw.project)" [disabled]="!pw.allFilesApproved" color="accent">
|
||||
<mat-icon svgIcon="red:report"></mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
@ -207,7 +157,6 @@
|
||||
(click)="openAssignProjectOwnerDialog($event, pw.project)"
|
||||
[matTooltip]="'project-listing.assign.action' | translate"
|
||||
matTooltipPosition="above"
|
||||
*ngIf="userService.isManager(user)"
|
||||
color="accent"
|
||||
mat-icon-button
|
||||
>
|
||||
@ -215,7 +164,7 @@
|
||||
</button>
|
||||
<button
|
||||
color="accent"
|
||||
*ngIf="appStateService.isManagerAndOwner(pw.project) && pw.hasFiles"
|
||||
*ngIf="permissionsService.isManagerAndOwner(pw.project) && pw.hasFiles"
|
||||
(click)="reanalyseProject($event, pw.project)"
|
||||
mat-icon-button
|
||||
[matTooltip]="'project-listing.reanalyse.action' | translate"
|
||||
@ -240,17 +189,11 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<redaction-project-listing-empty
|
||||
*ngIf="!appStateService.hasProjects"
|
||||
(addProjectRequest)="openAddProjectDialog()"
|
||||
></redaction-project-listing-empty>
|
||||
<redaction-project-listing-empty *ngIf="!appStateService.hasProjects" (addProjectRequest)="openAddProjectDialog()"></redaction-project-listing-empty>
|
||||
|
||||
<ng-template #needsWorkTemplate let-filter="filter">
|
||||
<ng-container>
|
||||
<redaction-annotation-icon
|
||||
*ngIf="filter.key !== 'none'"
|
||||
[typeValue]="appStateService.getDictionaryTypeValue(filter.key)"
|
||||
></redaction-annotation-icon>
|
||||
<redaction-annotation-icon *ngIf="filter.key !== 'none'" [typeValue]="appStateService.getDictionaryTypeValue(filter.key)"></redaction-annotation-icon>
|
||||
{{ filter.label | translate }}
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
|
||||
@ -1,15 +1,14 @@
|
||||
import { ChangeDetectorRef, Component, OnInit, ViewChild } from '@angular/core';
|
||||
import { Project } from '@redaction/red-ui-http';
|
||||
import { AppStateService, ProjectWrapper } from '../../state/app-state.service';
|
||||
import { AppStateService } from '../../state/app-state.service';
|
||||
import { UserService } from '../../user/user.service';
|
||||
import { DoughnutChartConfig } from '../../components/simple-doughnut-chart/simple-doughnut-chart.component';
|
||||
import { groupBy, humanize } from '../../utils/functions';
|
||||
import { DialogService } from '../../dialogs/dialog.service';
|
||||
import { FilterModel } from '../../common/filter/model/filter.model';
|
||||
import * as moment from 'moment';
|
||||
import { SortingComponent, SortingOption } from '../../components/sorting/sorting.component';
|
||||
import { SortingOption } from '../../components/sorting/sorting.component';
|
||||
import {
|
||||
addedDateChecker,
|
||||
annotationFilterChecker,
|
||||
dueDateChecker,
|
||||
getFilteredEntities,
|
||||
@ -18,6 +17,8 @@ import {
|
||||
RedactionFilterSorter
|
||||
} from '../../common/filter/utils/filter-utils';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { PermissionsService } from '../../common/service/permissions.service';
|
||||
import { ProjectWrapper } from '../../state/model/project.wrapper';
|
||||
|
||||
@Component({
|
||||
selector: 'redaction-project-listing-screen',
|
||||
@ -45,6 +46,7 @@ export class ProjectListingScreenComponent implements OnInit {
|
||||
constructor(
|
||||
public readonly appStateService: AppStateService,
|
||||
public readonly userService: UserService,
|
||||
public readonly permissionsService: PermissionsService,
|
||||
private readonly _changeDetectorRef: ChangeDetectorRef,
|
||||
private readonly _dialogService: DialogService,
|
||||
private readonly _translateService: TranslateService
|
||||
@ -77,10 +79,7 @@ export class ProjectListingScreenComponent implements OnInit {
|
||||
}
|
||||
|
||||
public get activeProjects() {
|
||||
return this.appStateService.allProjects.reduce(
|
||||
(i, p) => i + (p.project.status === Project.StatusEnum.ACTIVE ? 1 : 0),
|
||||
0
|
||||
);
|
||||
return this.appStateService.allProjects.reduce((i, p) => i + (p.project.status === Project.StatusEnum.ACTIVE ? 1 : 0), 0);
|
||||
}
|
||||
|
||||
public get inactiveProjects() {
|
||||
@ -92,7 +91,7 @@ export class ProjectListingScreenComponent implements OnInit {
|
||||
}
|
||||
|
||||
public userCount(project: ProjectWrapper) {
|
||||
return 1;
|
||||
return project.numberOfMembers;
|
||||
}
|
||||
|
||||
public canOpenProject(pw: ProjectWrapper): boolean {
|
||||
@ -168,7 +167,7 @@ export class ProjectListingScreenComponent implements OnInit {
|
||||
|
||||
// Needs work
|
||||
entry.files.forEach((file) => {
|
||||
if (file.hasHints) allDistinctNeedsWork.add('hint');
|
||||
if (file.hintsOnly) allDistinctNeedsWork.add('hint');
|
||||
if (file.hasRedactions) allDistinctNeedsWork.add('redaction');
|
||||
if (file.hasRequests) allDistinctNeedsWork.add('suggestion');
|
||||
if (file.hasNone) allDistinctNeedsWork.add('none');
|
||||
@ -206,9 +205,8 @@ export class ProjectListingScreenComponent implements OnInit {
|
||||
label: this._translateService.instant('filter.' + type)
|
||||
});
|
||||
});
|
||||
needsWorkFilters.sort(
|
||||
(a, b) => RedactionFilterSorter[a.key] - RedactionFilterSorter[b.key]
|
||||
);
|
||||
|
||||
needsWorkFilters.sort((a, b) => RedactionFilterSorter[a.key] - RedactionFilterSorter[b.key]);
|
||||
this.needsWorkFilters = needsWorkFilters;
|
||||
}
|
||||
|
||||
@ -251,17 +249,12 @@ export class ProjectListingScreenComponent implements OnInit {
|
||||
if (this.areAllProjectsSelected()) {
|
||||
this._selectedProjectIds = [];
|
||||
} else {
|
||||
this._selectedProjectIds = this.appStateService.allProjects.map(
|
||||
(pw) => pw.project.projectId
|
||||
);
|
||||
this._selectedProjectIds = this.appStateService.allProjects.map((pw) => pw.project.projectId);
|
||||
}
|
||||
}
|
||||
|
||||
public areAllProjectsSelected(): boolean {
|
||||
return (
|
||||
this.appStateService.allProjects.length !== 0 &&
|
||||
this._selectedProjectIds.length === this.appStateService.allProjects.length
|
||||
);
|
||||
return this.appStateService.allProjects.length !== 0 && this._selectedProjectIds.length === this.appStateService.allProjects.length;
|
||||
}
|
||||
|
||||
public isProjectSelected(pw: ProjectWrapper): boolean {
|
||||
|
||||
@ -1,29 +1,16 @@
|
||||
<div class="actions-row" *ngIf="userService.isManager()">
|
||||
<button
|
||||
(click)="openDeleteProjectDialog($event)"
|
||||
*ngIf="userService.isManager()"
|
||||
mat-icon-button
|
||||
>
|
||||
<div class="actions-row" *ngIf="permissionsService.isManager()">
|
||||
<button (click)="openDeleteProjectDialog($event)" mat-icon-button>
|
||||
<mat-icon svgIcon="red:trash"></mat-icon>
|
||||
</button>
|
||||
<button (click)="openEditProjectDialog($event)" *ngIf="userService.isManager()" mat-icon-button>
|
||||
<button (click)="openEditProjectDialog($event)" mat-icon-button>
|
||||
<mat-icon svgIcon="red:edit"></mat-icon>
|
||||
</button>
|
||||
<div
|
||||
[matTooltip]="
|
||||
(appStateService.activeProject.allFilesApproved
|
||||
? 'report.action'
|
||||
: 'report.unavailable'
|
||||
) | translate
|
||||
"
|
||||
*ngIf="appStateService.isActiveProjectOwnerAndManager"
|
||||
[matTooltip]="(appStateService.activeProject.allFilesApproved ? 'report.action' : 'report.unavailable') | translate"
|
||||
*ngIf="permissionsService.isManagerAndOwner()"
|
||||
matTooltipPosition="above"
|
||||
>
|
||||
<button
|
||||
(click)="downloadRedactionReport($event)"
|
||||
mat-icon-button
|
||||
[disabled]="!appStateService.activeProject.allFilesApproved"
|
||||
>
|
||||
<button (click)="downloadRedactionReport($event)" mat-icon-button [disabled]="!appStateService.activeProject.allFilesApproved">
|
||||
<mat-icon svgIcon="red:report"></mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
@ -34,11 +21,7 @@
|
||||
</div>
|
||||
|
||||
<div class="owner flex-row mt-16">
|
||||
<redaction-initials-avatar
|
||||
[userId]="appStateService.activeProject.project.ownerId"
|
||||
size="large"
|
||||
withName="true"
|
||||
></redaction-initials-avatar>
|
||||
<redaction-initials-avatar [userId]="appStateService.activeProject.project.ownerId" size="large" withName="true"></redaction-initials-avatar>
|
||||
</div>
|
||||
|
||||
<div class="project-team mt-16">
|
||||
@ -49,11 +32,7 @@
|
||||
<div *ngIf="overflowCount" class="member">
|
||||
<div class="oval large white-dark">+{{ overflowCount }}</div>
|
||||
</div>
|
||||
<div
|
||||
(click)="openAssignProjectMembersDialog()"
|
||||
*ngIf="userService.isManager()"
|
||||
class="member pointer"
|
||||
>
|
||||
<div (click)="openAssignProjectMembersDialog()" *ngIf="permissionsService.isManager()" class="member pointer">
|
||||
<div class="oval red-white large">+</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -72,14 +51,8 @@
|
||||
</div>
|
||||
|
||||
<div class="mt-24 legend" *ngIf="hasFiles">
|
||||
<div
|
||||
*ngFor="let filter of filters.needsWorkFilters"
|
||||
[class.active]="filter.checked"
|
||||
(click)="toggleFilter('needsWorkFilters', filter.key)"
|
||||
>
|
||||
<redaction-annotation-icon
|
||||
[typeValue]="appStateService.getDictionaryTypeValue(filter.key)"
|
||||
></redaction-annotation-icon>
|
||||
<div *ngFor="let filter of filters.needsWorkFilters" [class.active]="filter.checked" (click)="toggleFilter('needsWorkFilters', filter.key)">
|
||||
<redaction-annotation-icon *ngIf="filter.key !== 'none'" [typeValue]="appStateService.getDictionaryTypeValue(filter.key)"></redaction-annotation-icon>
|
||||
{{ 'project-overview.legend.' + filter.key | translate }}
|
||||
</div>
|
||||
</div>
|
||||
@ -87,41 +60,26 @@
|
||||
<div class="mt-32 small-label stats-subtitle">
|
||||
<div>
|
||||
<mat-icon svgIcon="red:document"></mat-icon>
|
||||
<span>{{
|
||||
'project-overview.project-details.stats.documents'
|
||||
| translate: { count: appStateService.activeProject.files.length }
|
||||
}}</span>
|
||||
<span>{{ 'project-overview.project-details.stats.documents' | translate: { count: appStateService.activeProject.files.length } }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<mat-icon svgIcon="red:user"></mat-icon>
|
||||
<span>{{
|
||||
'project-overview.project-details.stats.people'
|
||||
| translate: { count: appStateService.activeProject.project.memberIds.length }
|
||||
}}</span>
|
||||
<span>{{ 'project-overview.project-details.stats.people' | translate: { count: appStateService.activeProject.project.memberIds.length } }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<mat-icon svgIcon="red:pages"></mat-icon>
|
||||
<span>{{
|
||||
'project-overview.project-details.stats.analysed-pages'
|
||||
| translate: { count: appStateService.activeProject.totalNumberOfPages }
|
||||
}}</span>
|
||||
<span>{{ 'project-overview.project-details.stats.analysed-pages' | translate: { count: appStateService.activeProject.totalNumberOfPages } }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<mat-icon svgIcon="red:calendar"></mat-icon>
|
||||
<span
|
||||
>{{
|
||||
'project-overview.project-details.stats.created-on'
|
||||
| translate
|
||||
: { date: appStateService.activeProject.project.date | date: 'd MMM. yyyy' }
|
||||
}}
|
||||
>{{ 'project-overview.project-details.stats.created-on' | translate: { date: appStateService.activeProject.project.date | date: 'd MMM. yyyy' } }}
|
||||
</span>
|
||||
</div>
|
||||
<div *ngIf="appStateService.activeProject.project.dueDate">
|
||||
<mat-icon svgIcon="red:lightning"></mat-icon>
|
||||
<span>{{
|
||||
'project-overview.project-details.stats.due-date'
|
||||
| translate
|
||||
: { date: appStateService.activeProject.project.dueDate | date: 'd MMM. yyyy' }
|
||||
'project-overview.project-details.stats.due-date' | translate: { date: appStateService.activeProject.project.dueDate | date: 'd MMM. yyyy' }
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -6,6 +6,7 @@ import { DoughnutChartConfig } from '../../../components/simple-doughnut-chart/s
|
||||
import { DialogService } from '../../../dialogs/dialog.service';
|
||||
import { Router } from '@angular/router';
|
||||
import { FilterModel } from '../../../common/filter/model/filter.model';
|
||||
import { PermissionsService } from '../../../common/service/permissions.service';
|
||||
|
||||
@Component({
|
||||
selector: 'redaction-project-details',
|
||||
@ -20,7 +21,7 @@ export class ProjectDetailsComponent implements OnInit {
|
||||
|
||||
constructor(
|
||||
public readonly appStateService: AppStateService,
|
||||
public readonly userService: UserService,
|
||||
public readonly permissionsService: PermissionsService,
|
||||
private readonly _changeDetectorRef: ChangeDetectorRef,
|
||||
private readonly _dialogService: DialogService,
|
||||
private readonly _router: Router
|
||||
@ -33,45 +34,28 @@ export class ProjectDetailsComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
public get user() {
|
||||
return this.userService.user;
|
||||
}
|
||||
|
||||
public get displayMembers() {
|
||||
return this.appStateService.activeProject.project.memberIds.slice(0, 6);
|
||||
}
|
||||
|
||||
public get overflowCount() {
|
||||
return this.appStateService.activeProject.project.memberIds.length > 6
|
||||
? this.appStateService.activeProject.project.memberIds.length - 6
|
||||
: 0;
|
||||
return this.appStateService.activeProject.project.memberIds.length > 6 ? this.appStateService.activeProject.project.memberIds.length - 6 : 0;
|
||||
}
|
||||
|
||||
public openEditProjectDialog($event: MouseEvent) {
|
||||
this._dialogService.openEditProjectDialog(
|
||||
$event,
|
||||
this.appStateService.activeProject.project
|
||||
);
|
||||
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']);
|
||||
}
|
||||
);
|
||||
this._dialogService.openDeleteProjectDialog($event, this.appStateService.activeProject.project, () => {
|
||||
this._router.navigate(['/ui/projects']);
|
||||
});
|
||||
}
|
||||
|
||||
public openAssignProjectMembersDialog(): void {
|
||||
this._dialogService.openAssignProjectMembersAndOwnerDialog(
|
||||
null,
|
||||
this.appStateService.activeProject.project,
|
||||
() => {
|
||||
this.reloadProjects.emit();
|
||||
}
|
||||
);
|
||||
this._dialogService.openAssignProjectMembersAndOwnerDialog(null, this.appStateService.activeProject.project, () => {
|
||||
this.reloadProjects.emit();
|
||||
});
|
||||
}
|
||||
|
||||
public downloadRedactionReport($event: MouseEvent): void {
|
||||
|
||||
@ -1,7 +1,4 @@
|
||||
<redaction-project-overview-empty
|
||||
(uploadFiles)="uploadFiles($event)"
|
||||
*ngIf="!appStateService.activeProject?.hasFiles"
|
||||
></redaction-project-overview-empty>
|
||||
<redaction-project-overview-empty (uploadFiles)="uploadFiles($event)" *ngIf="!appStateService.activeProject?.hasFiles"></redaction-project-overview-empty>
|
||||
<section *ngIf="appStateService.activeProject?.hasFiles">
|
||||
<div *ngIf="appStateService.activeProject" class="page-header">
|
||||
<div class="filters flex-row">
|
||||
@ -31,25 +28,14 @@
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button
|
||||
(click)="fileInput.click()"
|
||||
color="primary"
|
||||
class="custom-mini-fab"
|
||||
mat-mini-fab
|
||||
>
|
||||
<button (click)="fileInput.click()" color="primary" class="custom-mini-fab" mat-mini-fab>
|
||||
<mat-icon svgIcon="red:upload"></mat-icon>
|
||||
</button>
|
||||
<button [routerLink]="['/ui/projects/']" mat-icon-button>
|
||||
<mat-icon svgIcon="red:close"></mat-icon>
|
||||
</button>
|
||||
|
||||
<input
|
||||
#fileInput
|
||||
(change)="uploadFiles($event.target.files)"
|
||||
class="file-upload-input"
|
||||
multiple="true"
|
||||
type="file"
|
||||
/>
|
||||
<input #fileInput (change)="uploadFiles($event.target['files'])" class="file-upload-input" multiple="true" type="file" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -58,16 +44,9 @@
|
||||
<div class="grid-container bulk-select">
|
||||
<div class="header-item span-7">
|
||||
<div class="select-all-container">
|
||||
<div
|
||||
(click)="toggleSelectAll()"
|
||||
[class.active]="areAllFilesSelected()"
|
||||
class="select-oval always-visible"
|
||||
></div>
|
||||
<div (click)="toggleSelectAll()" [class.active]="areAllFilesSelected()" class="select-oval always-visible"></div>
|
||||
<span class="all-caps-label">
|
||||
{{
|
||||
'project-overview.table-header.title'
|
||||
| translate: { length: displayedFiles.length || 0 }
|
||||
}}
|
||||
{{ 'project-overview.table-header.title' | translate: { length: displayedFiles.length || 0 } }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="actions">
|
||||
@ -99,9 +78,7 @@
|
||||
label="project-overview.table-col-names.added-on"
|
||||
></redaction-table-col-name>
|
||||
|
||||
<redaction-table-col-name
|
||||
label="project-overview.table-col-names.needs-work"
|
||||
></redaction-table-col-name>
|
||||
<redaction-table-col-name label="project-overview.table-col-names.needs-work"></redaction-table-col-name>
|
||||
|
||||
<redaction-table-col-name
|
||||
(toggleSort)="sortingComponent.toggleSort($event)"
|
||||
@ -128,48 +105,21 @@
|
||||
label="project-overview.table-col-names.status"
|
||||
></redaction-table-col-name>
|
||||
|
||||
<div
|
||||
*ngIf="displayedFiles?.length === 0"
|
||||
class="no-data heading-l"
|
||||
translate="project-overview.no-files-match"
|
||||
></div>
|
||||
<div *ngIf="displayedFiles?.length === 0" class="no-data heading-l" translate="project-overview.no-files-match"></div>
|
||||
|
||||
<div
|
||||
*ngFor="
|
||||
let fileStatus of displayedFiles
|
||||
| sortBy: sortingOption.order:sortingOption.column
|
||||
"
|
||||
[class.pointer]="canOpenFile(fileStatus)"
|
||||
[routerLink]="
|
||||
canOpenFile(fileStatus)
|
||||
? [
|
||||
'/ui/projects/' +
|
||||
appStateService.activeProject.project.projectId +
|
||||
'/file/' +
|
||||
fileStatus.fileId
|
||||
]
|
||||
: []
|
||||
"
|
||||
*ngFor="let fileStatus of displayedFiles | sortBy: sortingOption.order:sortingOption.column"
|
||||
[class.pointer]="permissionsService.canOpenFile(fileStatus)"
|
||||
[routerLink]="fileLink(fileStatus)"
|
||||
class="table-item"
|
||||
>
|
||||
<div class="pr-0">
|
||||
<div
|
||||
(click)="toggleFileSelected($event, fileStatus)"
|
||||
[class.active]="isFileSelected(fileStatus)"
|
||||
class="select-oval"
|
||||
></div>
|
||||
<div (click)="toggleFileSelected($event, fileStatus)" [class.active]="isFileSelected(fileStatus)" class="select-oval"></div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
matTooltipPosition="above"
|
||||
[matTooltip]="'[' + fileStatus.status + '] ' + fileStatus.filename"
|
||||
>
|
||||
<div matTooltipPosition="above" [matTooltip]="'[' + fileStatus.status + '] ' + fileStatus.filename">
|
||||
<div class="filename-wrapper">
|
||||
<div
|
||||
[class.disabled]="isPending(fileStatus) || isProcessing(fileStatus)"
|
||||
[class.error]="isError(fileStatus)"
|
||||
class="table-item-title"
|
||||
>
|
||||
<div [class.disabled]="fileStatus.isPending || fileStatus.isProcessing" [class.error]="fileStatus.isError" class="table-item-title">
|
||||
{{ fileStatus.filename }}
|
||||
</div>
|
||||
<span
|
||||
@ -181,50 +131,29 @@
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div [class.error]="isError(fileStatus)" class="small-label">
|
||||
<div [class.error]="fileStatus.isError" class="small-label">
|
||||
{{ fileStatus.added | date: 'd MMM. yyyy, hh:mm a' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="!isError(fileStatus)">
|
||||
<redaction-needs-work-badge
|
||||
[needsWorkInput]="fileStatus"
|
||||
></redaction-needs-work-badge>
|
||||
<div *ngIf="!fileStatus.isError">
|
||||
<redaction-needs-work-badge [needsWorkInput]="fileStatus"></redaction-needs-work-badge>
|
||||
</div>
|
||||
<div *ngIf="!isError(fileStatus)" class="assigned-to">
|
||||
<redaction-initials-avatar
|
||||
[userId]="fileStatus.currentReviewer"
|
||||
withName="true"
|
||||
></redaction-initials-avatar>
|
||||
<div *ngIf="!fileStatus.isError" class="assigned-to">
|
||||
<redaction-initials-avatar [userId]="fileStatus.currentReviewer" withName="true"></redaction-initials-avatar>
|
||||
</div>
|
||||
|
||||
<div *ngIf="!isError(fileStatus)" class="pages">
|
||||
<div *ngIf="!fileStatus.isError" class="pages">
|
||||
<mat-icon svgIcon="red:pages"></mat-icon>
|
||||
{{ fileStatus.numberOfPages }}
|
||||
</div>
|
||||
|
||||
<div [class.extend-cols]="isError(fileStatus)" class="status-container">
|
||||
<div
|
||||
*ngIf="isError(fileStatus)"
|
||||
class="small-label error"
|
||||
translate="project-overview.file-listing.file-entry.file-error"
|
||||
></div>
|
||||
<div
|
||||
*ngIf="isPending(fileStatus)"
|
||||
class="small-label"
|
||||
translate="project-overview.file-listing.file-entry.file-pending"
|
||||
></div>
|
||||
<div
|
||||
*ngIf="isProcessing(fileStatus)"
|
||||
class="small-label"
|
||||
translate="processing"
|
||||
></div>
|
||||
<div [class.extend-cols]="fileStatus.isError" class="status-container">
|
||||
<div *ngIf="fileStatus.isError" class="small-label error" translate="project-overview.file-listing.file-entry.file-error"></div>
|
||||
<div *ngIf="fileStatus.isPending" class="small-label" translate="project-overview.file-listing.file-entry.file-pending"></div>
|
||||
<div *ngIf="fileStatus.isProcessing" class="small-label" translate="processing"></div>
|
||||
<redaction-status-bar
|
||||
*ngIf="
|
||||
!isPending(fileStatus) &&
|
||||
!isProcessing(fileStatus) &&
|
||||
!isError(fileStatus)
|
||||
"
|
||||
*ngIf="fileStatus.isWorkable"
|
||||
[config]="[
|
||||
{
|
||||
color: fileStatus.status,
|
||||
@ -234,111 +163,10 @@
|
||||
></redaction-status-bar>
|
||||
|
||||
<div class="action-buttons">
|
||||
<button
|
||||
(click)="openDeleteFileDialog($event, fileStatus)"
|
||||
*ngIf="
|
||||
userService.isManager(user) ||
|
||||
appStateService.isActiveProjectOwnerAndManager ||
|
||||
fileStatus.isUnassigned ||
|
||||
fileStatus.isError
|
||||
"
|
||||
[matTooltip]="'project-overview.delete.action' | translate"
|
||||
matTooltipPosition="above"
|
||||
color="accent"
|
||||
mat-icon-button
|
||||
>
|
||||
<mat-icon svgIcon="red:trash"></mat-icon>
|
||||
</button>
|
||||
|
||||
<div
|
||||
[matTooltip]="
|
||||
(fileStatus.isApproved
|
||||
? 'report.action'
|
||||
: 'report.unavailable-single'
|
||||
) | translate
|
||||
"
|
||||
*ngIf="
|
||||
appStateService.isActiveProjectOwnerAndManager &&
|
||||
!isError(fileStatus)
|
||||
"
|
||||
matTooltipPosition="above"
|
||||
>
|
||||
<button
|
||||
(click)="downloadFileRedactionReport($event, fileStatus)"
|
||||
[disabled]="!fileStatus.isApproved"
|
||||
color="accent"
|
||||
mat-icon-button
|
||||
>
|
||||
<mat-icon svgIcon="red:report"></mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
(click)="assignReviewer($event, fileStatus)"
|
||||
*ngIf="
|
||||
appStateService.isActiveProjectMember &&
|
||||
!isError(fileStatus) &&
|
||||
!fileStatus.isApprovedOrUnderApproval
|
||||
"
|
||||
[matTooltip]="'project-overview.assign.action' | translate"
|
||||
matTooltipPosition="above"
|
||||
color="accent"
|
||||
mat-icon-button
|
||||
>
|
||||
<mat-icon svgIcon="red:assign"></mat-icon>
|
||||
</button>
|
||||
<button
|
||||
(click)="reanalyseFile($event, fileStatus)"
|
||||
*ngIf="appStateService.canReanalyseFile(fileStatus)"
|
||||
[matTooltip]="'project-overview.reanalyse.action' | translate"
|
||||
matTooltipPosition="above"
|
||||
color="accent"
|
||||
mat-icon-button
|
||||
>
|
||||
<mat-icon svgIcon="red:refresh"></mat-icon>
|
||||
</button>
|
||||
<button
|
||||
(click)="requestApprovalOrApproveFile($event, fileStatus)"
|
||||
*ngIf="
|
||||
fileStatus.canApprove &&
|
||||
appStateService.isActiveProjectOwnerAndManager
|
||||
"
|
||||
[matTooltip]="
|
||||
(fileStatus.status === 'UNDER_APPROVAL'
|
||||
? 'project-overview.approve'
|
||||
: 'project-overview.under-approval'
|
||||
) | translate
|
||||
"
|
||||
matTooltipPosition="above"
|
||||
color="accent"
|
||||
mat-icon-button
|
||||
>
|
||||
<mat-icon svgIcon="red:check-alt"></mat-icon>
|
||||
</button>
|
||||
<button
|
||||
(click)="undoApproveOrUnderApproval($event, fileStatus)"
|
||||
*ngIf="
|
||||
fileStatus.isApprovedOrUnderApproval &&
|
||||
appStateService.isActiveProjectOwnerAndManager
|
||||
"
|
||||
[matTooltip]="
|
||||
(fileStatus.status === 'APPROVED'
|
||||
? 'project-overview.under-approval'
|
||||
: 'project-overview.under-review'
|
||||
) | translate
|
||||
"
|
||||
matTooltipPosition="above"
|
||||
color="accent"
|
||||
mat-icon-button
|
||||
>
|
||||
<mat-icon svgIcon="red:undo"></mat-icon>
|
||||
</button>
|
||||
<redaction-file-actions [fileStatus]="fileStatus" (actionPerformed)="calculateData()"></redaction-file-actions>
|
||||
<redaction-status-bar
|
||||
class="mr-8"
|
||||
*ngIf="
|
||||
!isPending(fileStatus) &&
|
||||
!isProcessing(fileStatus) &&
|
||||
!isError(fileStatus)
|
||||
"
|
||||
*ngIf="fileStatus.isWorkable"
|
||||
[config]="[
|
||||
{
|
||||
color: fileStatus.status,
|
||||
@ -365,10 +193,7 @@
|
||||
|
||||
<ng-template #needsWorkTemplate let-filter="filter">
|
||||
<ng-container>
|
||||
<redaction-annotation-icon
|
||||
*ngIf="filter.key !== 'none'"
|
||||
[typeValue]="appStateService.getDictionaryTypeValue(filter.key)"
|
||||
></redaction-annotation-icon>
|
||||
<redaction-annotation-icon *ngIf="filter.key !== 'none'" [typeValue]="appStateService.getDictionaryTypeValue(filter.key)"></redaction-annotation-icon>
|
||||
{{ filter.label | translate }}
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
|
||||
@ -1,18 +1,12 @@
|
||||
import { ChangeDetectorRef, Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import {
|
||||
FileStatus,
|
||||
FileUploadControllerService,
|
||||
ReanalysisControllerService,
|
||||
StatusControllerService
|
||||
} from '@redaction/red-ui-http';
|
||||
import { FileUploadControllerService, ReanalysisControllerService, StatusControllerService } from '@redaction/red-ui-http';
|
||||
import { NotificationService, NotificationType } from '../../notification/notification.service';
|
||||
import { AppStateService } from '../../state/app-state.service';
|
||||
import { FileDropOverlayService } from '../../upload/file-drop/service/file-drop-overlay.service';
|
||||
import { FileUploadModel } from '../../upload/model/file-upload.model';
|
||||
import { FileUploadService } from '../../upload/file-upload.service';
|
||||
import { UploadStatusOverlayService } from '../../upload/upload-status-dialog/service/upload-status-overlay.service';
|
||||
import { UserService } from '../../user/user.service';
|
||||
import { humanize } from '../../utils/functions';
|
||||
import { DialogService } from '../../dialogs/dialog.service';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
@ -22,12 +16,9 @@ import * as moment from 'moment';
|
||||
import { SortingOption } from '../../components/sorting/sorting.component';
|
||||
import { ProjectDetailsComponent } from './project-details/project-details.component';
|
||||
import { FileStatusWrapper } from '../file/model/file-status.wrapper';
|
||||
import {
|
||||
annotationFilterChecker,
|
||||
getFilteredEntities,
|
||||
keyChecker,
|
||||
RedactionFilterSorter
|
||||
} from '../../common/filter/utils/filter-utils';
|
||||
import { annotationFilterChecker, getFilteredEntities, keyChecker, RedactionFilterSorter } from '../../common/filter/utils/filter-utils';
|
||||
import { PermissionsService } from '../../common/service/permissions.service';
|
||||
import { UserService } from '../../user/user.service';
|
||||
|
||||
@Component({
|
||||
selector: 'redaction-project-overview-screen',
|
||||
@ -37,13 +28,13 @@ import {
|
||||
export class ProjectOverviewScreenComponent implements OnInit, OnDestroy {
|
||||
private _selectedFileIds: string[] = [];
|
||||
|
||||
public statusFilters: FilterModel[];
|
||||
public peopleFilters: FilterModel[];
|
||||
public needsWorkFilters: FilterModel[];
|
||||
statusFilters: FilterModel[];
|
||||
peopleFilters: FilterModel[];
|
||||
needsWorkFilters: FilterModel[];
|
||||
|
||||
public displayedFiles: FileStatusWrapper[] = [];
|
||||
displayedFiles: FileStatusWrapper[] = [];
|
||||
|
||||
public detailsContainerFilters: {
|
||||
detailsContainerFilters: {
|
||||
needsWorkFilters: FilterModel[];
|
||||
statusFilters: FilterModel[];
|
||||
};
|
||||
@ -51,20 +42,18 @@ export class ProjectOverviewScreenComponent implements OnInit, OnDestroy {
|
||||
@ViewChild('projectDetailsComponent', { static: false })
|
||||
private _projectDetailsComponent: ProjectDetailsComponent;
|
||||
|
||||
public sortingOption: SortingOption = { column: 'added', order: 'desc' };
|
||||
sortingOption: SortingOption = { column: 'added', order: 'desc' };
|
||||
|
||||
constructor(
|
||||
public readonly appStateService: AppStateService,
|
||||
public readonly userService: UserService,
|
||||
public readonly permissionsService: PermissionsService,
|
||||
private readonly _userService: UserService,
|
||||
private readonly _activatedRoute: ActivatedRoute,
|
||||
private readonly _fileUploadControllerService: FileUploadControllerService,
|
||||
private readonly _statusControllerService: StatusControllerService,
|
||||
private readonly _notificationService: NotificationService,
|
||||
private readonly _dialogService: DialogService,
|
||||
private readonly _fileActionService: FileActionService,
|
||||
private readonly _fileUploadService: FileUploadService,
|
||||
private readonly _uploadStatusOverlayService: UploadStatusOverlayService,
|
||||
private readonly _reanalysisControllerService: ReanalysisControllerService,
|
||||
private readonly _router: Router,
|
||||
private readonly _changeDetectorRef: ChangeDetectorRef,
|
||||
private readonly _translateService: TranslateService,
|
||||
@ -75,13 +64,13 @@ export class ProjectOverviewScreenComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
|
||||
this.appStateService.fileStatusChanged.subscribe(() => {
|
||||
this._calculateData();
|
||||
this.calculateData();
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this._fileDropOverlayService.initFileDropHandling();
|
||||
this._calculateData();
|
||||
this.calculateData();
|
||||
this._displayNewRuleToast();
|
||||
}
|
||||
|
||||
@ -89,38 +78,14 @@ export class ProjectOverviewScreenComponent implements OnInit, OnDestroy {
|
||||
this._fileDropOverlayService.cleanupFileDropHandling();
|
||||
}
|
||||
|
||||
public get user() {
|
||||
return this.userService.user;
|
||||
}
|
||||
|
||||
public isPending(fileStatusWrapper: FileStatusWrapper) {
|
||||
return fileStatusWrapper.status === FileStatus.StatusEnum.UNPROCESSED;
|
||||
}
|
||||
|
||||
public isError(fileStatusWrapper: FileStatusWrapper) {
|
||||
return fileStatusWrapper.status === FileStatus.StatusEnum.ERROR;
|
||||
}
|
||||
|
||||
public isProcessing(fileStatusWrapper: FileStatusWrapper) {
|
||||
return [FileStatus.StatusEnum.REPROCESS, FileStatus.StatusEnum.PROCESSING].includes(
|
||||
fileStatusWrapper.status
|
||||
);
|
||||
}
|
||||
|
||||
private _displayNewRuleToast() {
|
||||
// @ts-ignore
|
||||
if (
|
||||
!this.appStateService.activeProject.files.filter((file) =>
|
||||
this.appStateService.fileNotUpToDateWithDictionary(file)
|
||||
).length
|
||||
) {
|
||||
if (!this.appStateService.activeProject.files.filter((file) => this.appStateService.fileNotUpToDateWithDictionary(file)).length) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._notificationService.showToastNotification(
|
||||
`${this._translateService.instant(
|
||||
'project-overview.new-rule.toast.message-project'
|
||||
)} <span class="pill">${this._translateService.instant(
|
||||
`${this._translateService.instant('project-overview.new-rule.toast.message-project')} <span class="pill">${this._translateService.instant(
|
||||
'project-overview.new-rule.label'
|
||||
)}</span>`,
|
||||
null,
|
||||
@ -130,41 +95,35 @@ export class ProjectOverviewScreenComponent implements OnInit, OnDestroy {
|
||||
positionClass: 'toast-top-left',
|
||||
actions: [
|
||||
{
|
||||
title: this._translateService.instant(
|
||||
'project-overview.new-rule.toast.actions.reanalyse-all'
|
||||
),
|
||||
title: this._translateService.instant('project-overview.new-rule.toast.actions.reanalyse-all'),
|
||||
action: () =>
|
||||
this._reanalysisControllerService
|
||||
.reanalyzeProject(
|
||||
this.appStateService.activeProject.project.projectId
|
||||
)
|
||||
this.appStateService
|
||||
.reanalyzeProject()
|
||||
.toPromise()
|
||||
.then(() => this.reloadProjects())
|
||||
},
|
||||
{
|
||||
title: this._translateService.instant(
|
||||
'project-overview.new-rule.toast.actions.later'
|
||||
)
|
||||
title: this._translateService.instant('project-overview.new-rule.toast.actions.later')
|
||||
}
|
||||
]
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public reloadProjects() {
|
||||
reloadProjects() {
|
||||
this.appStateService.getFiles().then(() => {
|
||||
this._calculateData();
|
||||
this.calculateData();
|
||||
});
|
||||
}
|
||||
|
||||
private _calculateData(): void {
|
||||
calculateData(): void {
|
||||
this._computeAllFilters();
|
||||
this._filterFiles();
|
||||
this._projectDetailsComponent?.calculateChartConfig();
|
||||
this._changeDetectorRef.detectChanges();
|
||||
}
|
||||
|
||||
public toggleFileSelected($event: MouseEvent, file: FileStatusWrapper) {
|
||||
toggleFileSelected($event: MouseEvent, file: FileStatusWrapper) {
|
||||
$event.stopPropagation();
|
||||
const idx = this._selectedFileIds.indexOf(file.fileId);
|
||||
if (idx === -1) {
|
||||
@ -174,65 +133,27 @@ export class ProjectOverviewScreenComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
public toggleSelectAll() {
|
||||
toggleSelectAll() {
|
||||
if (this.areAllFilesSelected()) {
|
||||
this._selectedFileIds = [];
|
||||
} else {
|
||||
this._selectedFileIds = this.appStateService.activeProject.files.map(
|
||||
(file) => file.fileId
|
||||
);
|
||||
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
|
||||
);
|
||||
areAllFilesSelected() {
|
||||
return this.appStateService.activeProject.files.length !== 0 && this._selectedFileIds.length === this.appStateService.activeProject.files.length;
|
||||
}
|
||||
|
||||
public isFileSelected(file: FileStatusWrapper) {
|
||||
isFileSelected(file: FileStatusWrapper) {
|
||||
return this._selectedFileIds.indexOf(file.fileId) !== -1;
|
||||
}
|
||||
|
||||
public openDeleteFileDialog($event: MouseEvent, fileStatusWrapper: FileStatusWrapper) {
|
||||
this._dialogService.openDeleteFileDialog(
|
||||
$event,
|
||||
fileStatusWrapper.projectId,
|
||||
fileStatusWrapper.fileId,
|
||||
() => {
|
||||
this._calculateData();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
downloadFileRedactionReport($event: MouseEvent, file: FileStatusWrapper) {
|
||||
$event.stopPropagation();
|
||||
this.appStateService.downloadFileRedactionReport(file);
|
||||
}
|
||||
|
||||
public assignReviewer($event: MouseEvent, file: FileStatusWrapper) {
|
||||
$event.stopPropagation();
|
||||
this._fileActionService.assignProjectReviewer(file, () => this._calculateData());
|
||||
}
|
||||
|
||||
public reanalyseFile($event: MouseEvent, fileStatusWrapper: FileStatusWrapper) {
|
||||
$event.stopPropagation();
|
||||
this._reanalysisControllerService
|
||||
.reanalyzeFile(
|
||||
this.appStateService.activeProject.project.projectId,
|
||||
fileStatusWrapper.fileId
|
||||
)
|
||||
.subscribe(() => {
|
||||
this.reloadProjects();
|
||||
});
|
||||
}
|
||||
|
||||
public fileId(index, item) {
|
||||
fileId(index, item) {
|
||||
return item.fileId;
|
||||
}
|
||||
|
||||
public uploadFiles(files: FileList | File[]) {
|
||||
uploadFiles(files: FileList | File[]) {
|
||||
const uploadFiles: FileUploadModel[] = [];
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const file = files[i];
|
||||
@ -248,15 +169,7 @@ export class ProjectOverviewScreenComponent implements OnInit, OnDestroy {
|
||||
this._uploadStatusOverlayService.openStatusOverlay();
|
||||
}
|
||||
|
||||
public canOpenFile(fileStatusWrapper: FileStatusWrapper): boolean {
|
||||
return (
|
||||
!this.isError(fileStatusWrapper) &&
|
||||
!this.isProcessing(fileStatusWrapper) &&
|
||||
this.appStateService.isActiveProjectMember
|
||||
);
|
||||
}
|
||||
|
||||
public sortingOptionChanged(option: SortingOption) {
|
||||
sortingOptionChanged(option: SortingOption) {
|
||||
this.sortingOption = option;
|
||||
}
|
||||
|
||||
@ -267,23 +180,17 @@ export class ProjectOverviewScreenComponent implements OnInit, OnDestroy {
|
||||
const allDistinctNeedsWork = new Set<string>();
|
||||
|
||||
// All people
|
||||
this.appStateService.activeProject.project.memberIds.forEach((memberId) =>
|
||||
allDistinctPeople.add(memberId)
|
||||
);
|
||||
this.appStateService.activeProject.project.memberIds.forEach((memberId) => allDistinctPeople.add(memberId));
|
||||
|
||||
// File statuses
|
||||
this.appStateService.activeProject.files.forEach((file) =>
|
||||
allDistinctFileStatusWrapper.add(file.status)
|
||||
);
|
||||
this.appStateService.activeProject.files.forEach((file) => allDistinctFileStatusWrapper.add(file.status));
|
||||
|
||||
// Added dates
|
||||
this.appStateService.activeProject.files.forEach((file) =>
|
||||
allDistinctAddedDates.add(moment(file.added).format('DD/MM/YYYY'))
|
||||
);
|
||||
this.appStateService.activeProject.files.forEach((file) => allDistinctAddedDates.add(moment(file.added).format('DD/MM/YYYY')));
|
||||
|
||||
// Needs work
|
||||
this.appStateService.activeProject.files.forEach((file) => {
|
||||
if (file.hasHints) allDistinctNeedsWork.add('hint');
|
||||
if (file.hintsOnly) allDistinctNeedsWork.add('hint');
|
||||
if (file.hasRedactions) allDistinctNeedsWork.add('redaction');
|
||||
if (file.hasRequests) allDistinctNeedsWork.add('suggestion');
|
||||
if (file.hasNone) allDistinctNeedsWork.add('none');
|
||||
@ -301,7 +208,7 @@ export class ProjectOverviewScreenComponent implements OnInit, OnDestroy {
|
||||
allDistinctPeople.forEach((userId) => {
|
||||
this.peopleFilters.push({
|
||||
key: userId,
|
||||
label: this.userService.getNameForId(userId)
|
||||
label: this._userService.getNameForId(userId)
|
||||
});
|
||||
});
|
||||
|
||||
@ -312,9 +219,7 @@ export class ProjectOverviewScreenComponent implements OnInit, OnDestroy {
|
||||
label: this._translateService.instant('filter.' + type)
|
||||
});
|
||||
});
|
||||
needsWorkFilters.sort(
|
||||
(a, b) => RedactionFilterSorter[a.key] - RedactionFilterSorter[b.key]
|
||||
);
|
||||
needsWorkFilters.sort((a, b) => RedactionFilterSorter[a.key] - RedactionFilterSorter[b.key]);
|
||||
this.needsWorkFilters = needsWorkFilters;
|
||||
}
|
||||
|
||||
@ -329,34 +234,23 @@ export class ProjectOverviewScreenComponent implements OnInit, OnDestroy {
|
||||
this._filterFiles();
|
||||
}
|
||||
|
||||
fileLink(fileStatus: FileStatusWrapper) {
|
||||
return this.permissionsService.canOpenFile(fileStatus)
|
||||
? ['/ui/projects/' + this.appStateService.activeProject.project.projectId + '/file/' + fileStatus.fileId]
|
||||
: [];
|
||||
}
|
||||
|
||||
private _filterFiles() {
|
||||
const filters = [
|
||||
{ values: this.statusFilters, checker: keyChecker('status') },
|
||||
{ values: this.peopleFilters, checker: keyChecker('currentReviewer') },
|
||||
{ values: this.needsWorkFilters, checker: annotationFilterChecker, matchAll: true }
|
||||
];
|
||||
this.displayedFiles = getFilteredEntities(
|
||||
this.appStateService.activeProject.files,
|
||||
filters
|
||||
);
|
||||
this.displayedFiles = getFilteredEntities(this.appStateService.activeProject.files, filters);
|
||||
this.detailsContainerFilters = {
|
||||
needsWorkFilters: this.needsWorkFilters.map((f) => ({ ...f })),
|
||||
statusFilters: this.statusFilters.map((f) => ({ ...f }))
|
||||
};
|
||||
this._changeDetectorRef.detectChanges();
|
||||
}
|
||||
|
||||
requestApprovalOrApproveFile($event: MouseEvent, fileStatusWrapper: FileStatusWrapper) {
|
||||
$event.stopPropagation();
|
||||
this._fileActionService.requestApprovalOrApproveFile(fileStatusWrapper).subscribe(() => {
|
||||
this.reloadProjects();
|
||||
});
|
||||
}
|
||||
|
||||
undoApproveOrUnderApproval($event: MouseEvent, fileStatusWrapper: FileStatusWrapper) {
|
||||
$event.stopPropagation();
|
||||
this._fileActionService.undoApproveOrUnderApproval(fileStatusWrapper).subscribe(() => {
|
||||
this.reloadProjects();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,14 +13,14 @@ import {
|
||||
import { NotificationService, NotificationType } from '../notification/notification.service';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { UserService, UserWrapper } from '../user/user.service';
|
||||
import { forkJoin, interval, of, timer } from 'rxjs';
|
||||
import { UserService } from '../user/user.service';
|
||||
import { forkJoin, of, timer } from 'rxjs';
|
||||
import { tap } from 'rxjs/operators';
|
||||
import { download } from '../utils/file-download-utils';
|
||||
import { humanize } from '../utils/functions';
|
||||
import { AnnotationWrapper } from '../screens/file/model/annotation.wrapper';
|
||||
import * as moment from 'moment';
|
||||
import { FileStatusWrapper } from '../screens/file/model/file-status.wrapper';
|
||||
import { ProjectWrapper } from './model/project.wrapper';
|
||||
|
||||
export interface AppState {
|
||||
projects: ProjectWrapper[];
|
||||
@ -33,73 +33,6 @@ export interface AppState {
|
||||
ruleVersion?: number;
|
||||
}
|
||||
|
||||
export class ProjectWrapper {
|
||||
totalNumberOfPages?: number;
|
||||
|
||||
hasHints?: boolean;
|
||||
hasRedactions?: boolean;
|
||||
hasRequests?: boolean;
|
||||
|
||||
allFilesApproved?: boolean;
|
||||
|
||||
private _files: FileStatusWrapper[];
|
||||
|
||||
constructor(public project: Project, files: FileStatusWrapper[]) {
|
||||
this._files = files ? files : [];
|
||||
this._recomputeFileStatus();
|
||||
}
|
||||
|
||||
set files(files: FileStatusWrapper[]) {
|
||||
this._files = files ? files : [];
|
||||
this._recomputeFileStatus();
|
||||
}
|
||||
|
||||
get files() {
|
||||
return this._files;
|
||||
}
|
||||
|
||||
get projectDate() {
|
||||
return this.project.date;
|
||||
}
|
||||
|
||||
get dueDate() {
|
||||
return this.project.dueDate;
|
||||
}
|
||||
|
||||
get hasFiles() {
|
||||
return this._files.length > 0;
|
||||
}
|
||||
|
||||
hasStatus(status: string) {
|
||||
return this._files.find((f) => f.status === status);
|
||||
}
|
||||
|
||||
hasMember(key: string) {
|
||||
return this.project.memberIds.indexOf(key) >= 0;
|
||||
}
|
||||
|
||||
dueDateMatches(key: string) {
|
||||
return moment(this.dueDate).format('DD/MM/YYYY') === key;
|
||||
}
|
||||
|
||||
addedDateMatches(key: string) {
|
||||
return moment(this.projectDate).format('DD/MM/YYYY') === key;
|
||||
}
|
||||
|
||||
private _recomputeFileStatus() {
|
||||
this.hasHints = false;
|
||||
this.hasRedactions = false;
|
||||
this.hasRequests = false;
|
||||
this.allFilesApproved = true;
|
||||
this._files.forEach((f) => {
|
||||
this.hasHints = this.hasHints || f.hasHints;
|
||||
this.hasRedactions = this.hasRedactions || f.hasRedactions;
|
||||
this.hasRequests = this.hasRequests || f.hasRequests;
|
||||
this.allFilesApproved = this.allFilesApproved && f.isApproved;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
@ -161,34 +94,19 @@ export class AppStateService {
|
||||
return this._appState.ruleVersion;
|
||||
}
|
||||
|
||||
get isActiveProjectOwner() {
|
||||
return this._appState.activeProject?.project?.ownerId === this._userService.userId;
|
||||
}
|
||||
|
||||
get isActiveProjectOwnerAndManager() {
|
||||
return (
|
||||
this._appState.activeProject?.project?.ownerId === this._userService.userId &&
|
||||
this._userService.isManager(this._userService.user)
|
||||
);
|
||||
}
|
||||
|
||||
get isActiveProjectMember() {
|
||||
return this._appState.activeProject?.project?.memberIds?.includes(this._userService.userId);
|
||||
}
|
||||
|
||||
get dictionaryData() {
|
||||
return this._dictionaryData;
|
||||
}
|
||||
|
||||
getViewedPagesForActiveFile() {
|
||||
if (this.canMarkPagesAsViewedForActiveFile) {
|
||||
return this._viewedPagesControllerService.getViewedPages(
|
||||
this.activeProjectId,
|
||||
this.activeFileId
|
||||
);
|
||||
} else {
|
||||
return of({ pages: [] });
|
||||
return this._viewedPagesControllerService.getViewedPages(this.activeProjectId, this.activeFileId);
|
||||
}
|
||||
|
||||
reanalyzeProject(project?: Project) {
|
||||
if (!project) {
|
||||
project = this.activeProject.project;
|
||||
}
|
||||
return this._reanalysisControllerService.reanalyzeProject(project.projectId);
|
||||
}
|
||||
|
||||
getDictionaryColor(type: string) {
|
||||
@ -200,10 +118,6 @@ export class AppStateService {
|
||||
return this._dictionaryData[type].label;
|
||||
}
|
||||
|
||||
get isActiveFileDocumentReviewer() {
|
||||
return this._appState.activeFile?.currentReviewer === this._userService.userId;
|
||||
}
|
||||
|
||||
get aggregatedFiles(): FileStatusWrapper[] {
|
||||
const result: FileStatusWrapper[] = [];
|
||||
this._appState.projects.forEach((p) => {
|
||||
@ -248,10 +162,6 @@ export class AppStateService {
|
||||
return this._appState.totalDocuments;
|
||||
}
|
||||
|
||||
get canMarkPagesAsViewedForActiveFile() {
|
||||
return this.canPerformAnnotationActionsOnCurrentFile();
|
||||
}
|
||||
|
||||
public getProjectById(id: string) {
|
||||
return this.allProjects.find((project) => project.project.projectId === id);
|
||||
}
|
||||
@ -277,9 +187,7 @@ export class AppStateService {
|
||||
}
|
||||
|
||||
private _getExistingFiles(project: Project) {
|
||||
const found = this._appState.projects.find(
|
||||
(p) => p.project.projectId === project.projectId
|
||||
);
|
||||
const found = this._appState.projects.find((p) => p.project.projectId === project.projectId);
|
||||
return found ? found.files : [];
|
||||
}
|
||||
|
||||
@ -287,9 +195,7 @@ export class AppStateService {
|
||||
if (!project) {
|
||||
project = this.activeProject;
|
||||
}
|
||||
const files = await this._statusControllerService
|
||||
.getProjectStatus(project.project.projectId)
|
||||
.toPromise();
|
||||
const files = await this._statusControllerService.getProjectStatus(project.project.projectId).toPromise();
|
||||
const oldFiles = [...project.files];
|
||||
|
||||
const fileStatusChangedEvent = [];
|
||||
@ -301,10 +207,7 @@ export class AppStateService {
|
||||
if (oldFile.fileId === file.fileId) {
|
||||
// emit when analysis count changed
|
||||
if (oldFile.lastUpdated !== file.lastUpdated) {
|
||||
const fileStatusWrapper = new FileStatusWrapper(
|
||||
file,
|
||||
this._userService.getNameForId(file.currentReviewer)
|
||||
);
|
||||
const fileStatusWrapper = new FileStatusWrapper(file, this._userService.getNameForId(file.currentReviewer));
|
||||
fileStatusChangedEvent.push(fileStatusWrapper);
|
||||
|
||||
if (oldFile.lastProcessed !== file.lastProcessed) {
|
||||
@ -317,17 +220,12 @@ export class AppStateService {
|
||||
}
|
||||
// emit for new file
|
||||
if (!found) {
|
||||
const fsw = new FileStatusWrapper(
|
||||
file,
|
||||
this._userService.getNameForId(file.currentReviewer)
|
||||
);
|
||||
const fsw = new FileStatusWrapper(file, this._userService.getNameForId(file.currentReviewer));
|
||||
fileStatusChangedEvent.push(fsw);
|
||||
}
|
||||
}
|
||||
|
||||
project.files = files.map(
|
||||
(f) => new FileStatusWrapper(f, this._userService.getNameForId(f.currentReviewer))
|
||||
);
|
||||
project.files = files.map((f) => new FileStatusWrapper(f, this._userService.getNameForId(f.currentReviewer)));
|
||||
|
||||
this._computeStats();
|
||||
|
||||
@ -348,18 +246,14 @@ export class AppStateService {
|
||||
if (!project) {
|
||||
project = this.activeProject.project;
|
||||
}
|
||||
this._fileUploadControllerService
|
||||
.downloadRedactionReportForProject(project.projectId, true, 'response')
|
||||
.subscribe((data) => {
|
||||
download(data, 'redaction-report-' + project.projectName + '.docx');
|
||||
});
|
||||
this._fileUploadControllerService.downloadRedactionReportForProject(project.projectId, true, 'response').subscribe((data) => {
|
||||
download(data, 'redaction-report-' + project.projectName + '.docx');
|
||||
});
|
||||
}
|
||||
|
||||
activateProject(projectId: string) {
|
||||
this._appState.activeFile = null;
|
||||
this._appState.activeProject = this._appState.projects.find(
|
||||
(p) => p.project.projectId === projectId
|
||||
);
|
||||
this._appState.activeProject = this._appState.projects.find((p) => p.project.projectId === projectId);
|
||||
if (!this._appState.activeProject) {
|
||||
this._router.navigate(['/ui/projects']);
|
||||
}
|
||||
@ -368,12 +262,8 @@ export class AppStateService {
|
||||
|
||||
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
|
||||
);
|
||||
this._appState.activeProject = this._appState.projects.find((p) => p.project.projectId === projectId);
|
||||
this._appState.activeFile = this._appState.activeProject.files.find((f) => f.fileId === fileId);
|
||||
}
|
||||
|
||||
reset() {
|
||||
@ -387,9 +277,7 @@ export class AppStateService {
|
||||
.toPromise()
|
||||
.then(
|
||||
() => {
|
||||
const index = this._appState.projects.findIndex(
|
||||
(p) => p.project.projectId === project.projectId
|
||||
);
|
||||
const index = this._appState.projects.findIndex((p) => p.project.projectId === project.projectId);
|
||||
this._appState.projects.splice(index, 1);
|
||||
this._appState.projects = [...this._appState.projects];
|
||||
},
|
||||
@ -405,12 +293,8 @@ export class AppStateService {
|
||||
|
||||
async addOrUpdateProject(project: Project) {
|
||||
try {
|
||||
const updatedProject = await this._projectControllerService
|
||||
.createProjectOrUpdateProject(project)
|
||||
.toPromise();
|
||||
let foundProject = this._appState.projects.find(
|
||||
(p) => p.project.projectId === updatedProject.projectId
|
||||
);
|
||||
const updatedProject = await this._projectControllerService.createProjectOrUpdateProject(project).toPromise();
|
||||
let foundProject = this._appState.projects.find((p) => p.project.projectId === updatedProject.projectId);
|
||||
if (foundProject) {
|
||||
Object.assign(foundProject.project, updatedProject);
|
||||
} else {
|
||||
@ -466,25 +350,13 @@ export class AppStateService {
|
||||
}
|
||||
}
|
||||
|
||||
async reanalyseActiveFile() {
|
||||
await this._reanalysisControllerService
|
||||
.reanalyzeFile(
|
||||
this._appState.activeProject.project.projectId,
|
||||
this._appState.activeFile.fileId
|
||||
)
|
||||
.toPromise();
|
||||
await this.reloadActiveProjectFiles();
|
||||
}
|
||||
|
||||
downloadFileRedactionReport(file?: FileStatusWrapper) {
|
||||
if (!file) {
|
||||
file = this.activeFile;
|
||||
}
|
||||
this._fileUploadControllerService
|
||||
.downloadRedactionReport({ fileIds: [file.fileId] }, true, 'response')
|
||||
.subscribe((data) => {
|
||||
download(data, 'redaction-report-' + file.filename + '.docx');
|
||||
});
|
||||
this._fileUploadControllerService.downloadRedactionReport({ fileIds: [file.fileId] }, true, 'response').subscribe((data) => {
|
||||
download(data, 'redaction-report-' + file.filename + '.docx');
|
||||
});
|
||||
}
|
||||
|
||||
async loadDictionaryDataIfNecessary() {
|
||||
@ -527,7 +399,7 @@ export class AppStateService {
|
||||
})
|
||||
);
|
||||
|
||||
const result = await forkJoin([typeObs, colorsObs]).toPromise();
|
||||
await forkJoin([typeObs, colorsObs]).toPromise();
|
||||
|
||||
this._dictionaryData['hint'] = { hexColor: '#283241', type: 'hint', virtual: true };
|
||||
this._dictionaryData['redaction'] = {
|
||||
@ -548,19 +420,8 @@ export class AppStateService {
|
||||
return data ? data : this._dictionaryData['default'];
|
||||
}
|
||||
|
||||
isManagerAndOwner(project: Project, user?: UserWrapper) {
|
||||
if (!user) {
|
||||
user = this._userService.user;
|
||||
}
|
||||
return user.isManager && project.ownerId === user.id;
|
||||
}
|
||||
|
||||
getDictionaryTypeValueForAnnotation(annotation: AnnotationWrapper) {
|
||||
if (
|
||||
annotation.superType === 'suggestion' ||
|
||||
annotation.superType === 'ignore' ||
|
||||
annotation.superType === 'suggestion-remove'
|
||||
) {
|
||||
if (annotation.superType === 'suggestion' || annotation.superType === 'ignore' || annotation.superType === 'suggestion-remove') {
|
||||
return this._dictionaryData[annotation.superType];
|
||||
}
|
||||
if (annotation.superType === 'redaction' || annotation.superType === 'hint') {
|
||||
@ -568,56 +429,19 @@ export class AppStateService {
|
||||
}
|
||||
}
|
||||
|
||||
async updateDictionaryVersion() {
|
||||
const result = await this._versionsControllerService.getVersions().toPromise();
|
||||
this._appState.dictionaryVersion = result.dictionaryVersion;
|
||||
this._appState.ruleVersion = result.rulesVersion;
|
||||
}
|
||||
|
||||
isReviewerOrOwner(fileStatus: FileStatusWrapper) {
|
||||
return (
|
||||
fileStatus.currentReviewer === this._userService.userId ||
|
||||
this.isActiveProjectOwnerAndManager
|
||||
);
|
||||
}
|
||||
|
||||
canReanalyseFile(fileStatus?: FileStatusWrapper) {
|
||||
if (!fileStatus) {
|
||||
fileStatus = this.activeFile;
|
||||
}
|
||||
return (
|
||||
(!fileStatus.isApproved && this.fileNotUpToDateWithDictionary(fileStatus)) ||
|
||||
fileStatus.isError ||
|
||||
fileStatus.hasRequests
|
||||
);
|
||||
}
|
||||
|
||||
fileNotUpToDateWithDictionary(fileStatus?: FileStatusWrapper) {
|
||||
if (!fileStatus) {
|
||||
fileStatus = this.activeFile;
|
||||
}
|
||||
return (
|
||||
(fileStatus.status === 'UNASSIGNED' ||
|
||||
fileStatus.status === 'UNDER_REVIEW' ||
|
||||
fileStatus.status === 'UNDER_APPROVAL') &&
|
||||
(fileStatus.dictionaryVersion !== this.dictionaryVersion ||
|
||||
fileStatus.rulesVersion !== this.rulesVersion)
|
||||
(fileStatus.status === 'UNASSIGNED' || fileStatus.status === 'UNDER_REVIEW' || fileStatus.status === 'UNDER_APPROVAL') &&
|
||||
(fileStatus.dictionaryVersion !== this.dictionaryVersion || fileStatus.rulesVersion !== this.rulesVersion)
|
||||
);
|
||||
}
|
||||
|
||||
canPerformAnnotationActionsOnCurrentFile() {
|
||||
// const status = this.activeFile.status;
|
||||
// if (status === 'UNDER_REVIEW') {
|
||||
// return this.isActiveProjectOwnerAndManager || this.isActiveFileDocumentReviewer;
|
||||
// }
|
||||
// if (status === 'UNDER_APPROVAL') {
|
||||
// return this.isActiveProjectOwnerAndManager;
|
||||
// }
|
||||
// return false;
|
||||
return (
|
||||
(this.activeFile.status === 'UNDER_APPROVAL' ||
|
||||
this.activeFile.status === 'UNDER_REVIEW') &&
|
||||
this._userService.userId === this.activeFile.currentReviewer
|
||||
);
|
||||
async updateDictionaryVersion() {
|
||||
const result = await this._versionsControllerService.getVersions().toPromise();
|
||||
this._appState.dictionaryVersion = result.dictionaryVersion;
|
||||
this._appState.ruleVersion = result.rulesVersion;
|
||||
}
|
||||
}
|
||||
|
||||
74
apps/red-ui/src/app/state/model/project.wrapper.ts
Normal file
74
apps/red-ui/src/app/state/model/project.wrapper.ts
Normal file
@ -0,0 +1,74 @@
|
||||
import { FileStatusWrapper } from '../../screens/file/model/file-status.wrapper';
|
||||
import * as moment from 'moment';
|
||||
import { Project } from '@redaction/red-ui-http';
|
||||
|
||||
export class ProjectWrapper {
|
||||
totalNumberOfPages?: number;
|
||||
|
||||
hintsOnly?: boolean;
|
||||
hasRedactions?: boolean;
|
||||
hasRequests?: boolean;
|
||||
|
||||
allFilesApproved?: boolean;
|
||||
|
||||
private _files: FileStatusWrapper[];
|
||||
|
||||
constructor(public project: Project, files: FileStatusWrapper[]) {
|
||||
this._files = files ? files : [];
|
||||
this._recomputeFileStatus();
|
||||
}
|
||||
|
||||
set files(files: FileStatusWrapper[]) {
|
||||
this._files = files ? files : [];
|
||||
this._recomputeFileStatus();
|
||||
}
|
||||
|
||||
get files() {
|
||||
return this._files;
|
||||
}
|
||||
|
||||
get projectDate() {
|
||||
return this.project.date;
|
||||
}
|
||||
|
||||
get numberOfMembers() {
|
||||
return this.project.memberIds.length;
|
||||
}
|
||||
|
||||
get dueDate() {
|
||||
return this.project.dueDate;
|
||||
}
|
||||
|
||||
get hasFiles() {
|
||||
return this._files.length > 0;
|
||||
}
|
||||
|
||||
hasStatus(status: string) {
|
||||
return this._files.find((f) => f.status === status);
|
||||
}
|
||||
|
||||
hasMember(key: string) {
|
||||
return this.project.memberIds.indexOf(key) >= 0;
|
||||
}
|
||||
|
||||
dueDateMatches(key: string) {
|
||||
return moment(this.dueDate).format('DD/MM/YYYY') === key;
|
||||
}
|
||||
|
||||
addedDateMatches(key: string) {
|
||||
return moment(this.projectDate).format('DD/MM/YYYY') === key;
|
||||
}
|
||||
|
||||
private _recomputeFileStatus() {
|
||||
this.hintsOnly = false;
|
||||
this.hasRedactions = false;
|
||||
this.hasRequests = false;
|
||||
this.allFilesApproved = true;
|
||||
this._files.forEach((f) => {
|
||||
this.hintsOnly = this.hintsOnly || f.hintsOnly;
|
||||
this.hasRedactions = this.hasRedactions || f.hasRedactions;
|
||||
this.hasRequests = this.hasRequests || f.hasRequests;
|
||||
this.allFilesApproved = this.allFilesApproved && f.isApproved;
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -337,7 +337,7 @@
|
||||
"number-of-analyses": "Number of analyses",
|
||||
"custom": "Custom"
|
||||
},
|
||||
"readonly-pill": "Readonly",
|
||||
"readonly-pill": "Read-only",
|
||||
"group": {
|
||||
"redactions": "Redaction Dictionaries",
|
||||
"hints": "Hint Dictionaries"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user