refactor permissions

This commit is contained in:
Timo Bejan 2020-11-09 19:33:50 +02:00
parent d7f8fe6508
commit 1c5f8fd006
26 changed files with 667 additions and 1197 deletions

View File

@ -1,6 +1,6 @@
{
"useTabs": false,
"printWidth": 100,
"printWidth": 160,
"tabWidth": 4,
"singleQuote": true,
"trailingComma": "none",

View File

@ -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,

View File

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

View File

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

View File

@ -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) {

View File

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

View 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) {}
}

View File

@ -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')"

View File

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

View File

@ -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) {

View File

@ -1 +0,0 @@
<p>file-not-available-overlay works!</p>

View File

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

View File

@ -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
>&nbsp;<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>&nbsp;<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>

View File

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

View File

@ -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() {

View File

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

View File

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

View File

@ -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 {

View File

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

View File

@ -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 {

View File

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

View File

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

View File

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

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

View File

@ -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"