Pull request #224: Exclude pages from redaction

Merge in RED/ui from RED-1618 to master

* commit '751ccb5ef4c844f61bcb777af5f5783b297da9bc':
  Exclude pages icon
  Exclude pages from redaction
  Fixed comments params order
This commit is contained in:
Adina Teudan 2021-06-29 09:10:34 +02:00
commit 2767809cd2
23 changed files with 537 additions and 199 deletions

View File

@ -196,4 +196,8 @@ export class FileStatusWrapper {
get cacheIdentifier() { get cacheIdentifier() {
return btoa(this.fileStatus.lastUploaded + this.fileStatus.lastOCRTime); return btoa(this.fileStatus.lastUploaded + this.fileStatus.lastOCRTime);
} }
get excludedPages(): number[] {
return this.fileStatus.excludedPages;
}
} }

View File

@ -23,7 +23,8 @@
*ngIf="permissionsService.canAddComment()" *ngIf="permissionsService.canAddComment()"
[form]="commentForm" [form]="commentForm"
[placeholder]="translateService.instant('comments.add-comment')" [placeholder]="translateService.instant('comments.add-comment')"
type="submit" icon="red:collapse"
type="action"
width="full" width="full"
></redaction-input-with-action> ></redaction-input-with-action>

View File

@ -1,5 +1,5 @@
import { ChangeDetectorRef, Component, HostBinding, Input } from '@angular/core'; import { ChangeDetectorRef, Component, HostBinding, Input } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { FormBuilder, FormGroup } from '@angular/forms';
import { Comment } from '@redaction/red-ui-http'; import { Comment } from '@redaction/red-ui-http';
import { ManualAnnotationService } from '../../services/manual-annotation.service'; import { ManualAnnotationService } from '../../services/manual-annotation.service';
import { AnnotationWrapper } from '@models/file/annotation.wrapper'; import { AnnotationWrapper } from '@models/file/annotation.wrapper';
@ -28,7 +28,7 @@ export class CommentsComponent {
private readonly _manualAnnotationService: ManualAnnotationService private readonly _manualAnnotationService: ManualAnnotationService
) { ) {
this.commentForm = this._formBuilder.group({ this.commentForm = this._formBuilder.group({
value: ['', Validators.required] value: ['']
}); });
} }

View File

@ -58,6 +58,15 @@
tooltipPosition="below" tooltipPosition="below"
></redaction-circle-button> ></redaction-circle-button>
<redaction-circle-button
(action)="toggleExcludePages()"
*ngIf="screen === 'file-preview'"
[attr.aria-expanded]="activeExcludePages"
icon="red:exclude-pages"
tooltip="file-preview.exclude-pages"
tooltipPosition="below"
></redaction-circle-button>
<!-- Ready for approval--> <!-- Ready for approval-->
<redaction-circle-button <redaction-circle-button
(action)="setFileUnderApproval($event)" (action)="setFileUnderApproval($event)"
@ -136,8 +145,8 @@
<!-- exclude from redaction --> <!-- exclude from redaction -->
<div class="red-input-group"> <div class="red-input-group">
<mat-slide-toggle <mat-slide-toggle
(click)="$event.stopPropagation()"
(change)="toggleAnalysis()" (change)="toggleAnalysis()"
(click)="$event.stopPropagation()"
[checked]="!fileStatus?.isExcluded" [checked]="!fileStatus?.isExcluded"
[class.mr-24]="screen === 'dossier-overview'" [class.mr-24]="screen === 'dossier-overview'"
[disabled]="!permissionsService.canToggleAnalysis(fileStatus)" [disabled]="!permissionsService.canToggleAnalysis(fileStatus)"

View File

@ -13,6 +13,7 @@ import { DossiersDialogService } from '../../services/dossiers-dialog.service';
export class FileActionsComponent implements OnInit { export class FileActionsComponent implements OnInit {
@Input() fileStatus: FileStatusWrapper; @Input() fileStatus: FileStatusWrapper;
@Input() activeDocumentInfo: boolean; @Input() activeDocumentInfo: boolean;
@Input() activeExcludePages: boolean;
@Output() actionPerformed = new EventEmitter<string>(); @Output() actionPerformed = new EventEmitter<string>();
actionMenuOpen: boolean; actionMenuOpen: boolean;
@ -104,6 +105,10 @@ export class FileActionsComponent implements OnInit {
this.actionPerformed.emit('view-document-info'); this.actionPerformed.emit('view-document-info');
} }
toggleExcludePages() {
this.actionPerformed.emit('view-exclude-pages');
}
openDeleteFileDialog($event: MouseEvent) { openDeleteFileDialog($event: MouseEvent) {
this._dialogService.openDeleteFilesDialog( this._dialogService.openDeleteFilesDialog(
$event, $event,

View File

@ -1,4 +1,8 @@
<div class="right-title heading" translate="file-preview.tabs.annotations.label"> <div
*ngIf="!excludePages"
class="right-title heading"
translate="file-preview.tabs.annotations.label"
>
<div> <div>
<div <div
(click)="multiSelectActive = true" (click)="multiSelectActive = true"
@ -16,14 +20,28 @@
></redaction-popup-filter> ></redaction-popup-filter>
</div> </div>
</div> </div>
<div
*ngIf="excludePages"
class="right-title heading"
translate="file-preview.tabs.exclude-pages.label"
>
<div>
<redaction-circle-button
(action)="closeExcludePagesView.emit()"
icon="red:close"
tooltip="file-preview.tabs.exclude-pages.close"
tooltipPosition="before"
></redaction-circle-button>
</div>
</div>
<div class="right-content"> <div class="right-content">
<div class="read-only d-flex" *ngIf="isReadOnly" [class.justify-center]="!isProcessing"> <div *ngIf="isReadOnly" [class.justify-center]="!isProcessing" class="read-only d-flex">
<div class="flex-align-items-center" *ngIf="isProcessing"> <div *ngIf="isProcessing" class="flex-align-items-center">
<span class="read-only-text" [translate]="fileData.fileStatus.status"></span> <span [translate]="fileData.fileStatus.status" class="read-only-text"></span>
<mat-progress-bar class="w-100" [mode]="'indeterminate'"></mat-progress-bar> <mat-progress-bar [mode]="'indeterminate'" class="w-100"></mat-progress-bar>
</div> </div>
<div class="flex-center"> <div class="flex-center">
<mat-icon svgIcon="red:read-only" class="red-white"></mat-icon> <mat-icon class="red-white" svgIcon="red:read-only"></mat-icon>
<span class="read-only-text" translate="readonly"></span> <span class="read-only-text" translate="readonly"></span>
</div> </div>
</div> </div>
@ -89,133 +107,150 @@
</div> </div>
<div style="overflow: hidden; width: 100%"> <div style="overflow: hidden; width: 100%">
<div attr.anotation-page-header="{{ activeViewerPage }}" class="page-separator"> <ng-container *ngIf="!excludePages">
<span *ngIf="!!activeViewerPage" class="all-caps-label"> <div attr.anotation-page-header="{{ activeViewerPage }}" class="page-separator">
<span translate="page"></span> {{ activeViewerPage }} - <span *ngIf="!!activeViewerPage" class="all-caps-label">
{{ activeAnnotationsLength || 0 }} <span translate="page"></span> {{ activeViewerPage }} -
<span {{ activeAnnotationsLength || 0 }}
[translate]="activeAnnotationsLength === 1 ? 'annotation' : 'annotations'" <span
></span> [translate]="
</span> activeAnnotationsLength === 1 ? 'annotation' : 'annotations'
"
></span>
</span>
<div *ngIf="multiSelectActive"> <div *ngIf="multiSelectActive">
<div (click)="selectAllOnActivePage()" class="all-caps-label primary pointer"> <div
All (click)="selectAllOnActivePage()"
</div> class="all-caps-label primary pointer"
<div (click)="deselectAllOnActivePage()" class="all-caps-label primary pointer"> translate="file-preview.tabs.annotations.select-all"
None ></div>
<div
(click)="deselectAllOnActivePage()"
class="all-caps-label primary pointer"
translate="file-preview.tabs.annotations.select-none"
></div>
</div> </div>
</div> </div>
</div>
<div
#annotationsElement
(keydown)="preventKeyDefault($event)"
(keyup)="preventKeyDefault($event)"
[class.active-panel]="!pagesPanelActive"
class="annotations"
redactionHasScrollbar
tabindex="1"
>
<ng-container *ngIf="!displayedAnnotations[activeViewerPage]">
<redaction-empty-state
[horizontalPadding]="24"
[verticalPadding]="40"
icon="red:document"
screen="file-preview"
></redaction-empty-state>
<div class="no-annotations-buttons-container mt-32">
<redaction-icon-button
(action)="jumpToPreviousWithAnnotations()"
[disabled]="activeViewerPage <= displayedPages[0]"
icon="red:nav-prev"
text="file-preview.tabs.annotations.jump-to-previous"
type="show-bg"
></redaction-icon-button>
<redaction-icon-button
(action)="jumpToNextWithAnnotations()"
[disabled]="
activeViewerPage >= displayedPages[displayedPages.length - 1]
"
class="mt-8"
icon="red:nav-next"
text="file-preview.tabs.annotations.jump-to-next"
type="show-bg"
></redaction-icon-button>
</div>
</ng-container>
<div <div
(click)="annotationClicked(annotation, $event)" #annotationsElement
*ngFor="let annotation of displayedAnnotations[activeViewerPage]?.annotations" (keydown)="preventKeyDefault($event)"
[class.active]="annotationIsSelected(annotation)" (keyup)="preventKeyDefault($event)"
[class.multi-select-active]="multiSelectActive" [class.active-panel]="!pagesPanelActive"
attr.annotation-id="{{ annotation.id }}" class="annotations"
attr.annotation-page="{{ activeViewerPage }}" redactionHasScrollbar
class="annotation-wrapper" tabindex="1"
> >
<div class="active-bar-marker"></div> <ng-container *ngIf="!displayedAnnotations[activeViewerPage]">
<div [class.removed]="annotation.isChangeLogRemoved" class="annotation"> <redaction-empty-state
<redaction-hidden-action [horizontalPadding]="24"
(action)="logAnnotation(annotation)" [verticalPadding]="40"
[requiredClicks]="2" icon="red:document"
> screen="file-preview"
<div ></redaction-empty-state>
[matTooltip]="annotation.content" <div class="no-annotations-buttons-container mt-32">
class="details" <redaction-icon-button
matTooltipPosition="above" (action)="jumpToPreviousWithAnnotations()"
[disabled]="activeViewerPage <= displayedPages[0]"
icon="red:nav-prev"
text="file-preview.tabs.annotations.jump-to-previous"
type="show-bg"
></redaction-icon-button>
<redaction-icon-button
(action)="jumpToNextWithAnnotations()"
[disabled]="
activeViewerPage >= displayedPages[displayedPages.length - 1]
"
class="mt-8"
icon="red:nav-next"
text="file-preview.tabs.annotations.jump-to-next"
type="show-bg"
></redaction-icon-button>
</div>
</ng-container>
<div
(click)="annotationClicked(annotation, $event)"
*ngFor="
let annotation of displayedAnnotations[activeViewerPage]?.annotations
"
[class.active]="annotationIsSelected(annotation)"
[class.multi-select-active]="multiSelectActive"
attr.annotation-id="{{ annotation.id }}"
attr.annotation-page="{{ activeViewerPage }}"
class="annotation-wrapper"
>
<div class="active-bar-marker"></div>
<div [class.removed]="annotation.isChangeLogRemoved" class="annotation">
<redaction-hidden-action
(action)="logAnnotation(annotation)"
[requiredClicks]="2"
> >
<redaction-type-annotation-icon
[annotation]="annotation"
></redaction-type-annotation-icon>
<div class="flex-1">
<div>
<strong>{{ annotation.typeLabel | translate }}</strong>
</div>
<div *ngIf="annotation?.dictionary !== 'manual'">
<strong>
<span>{{ annotation.descriptor | translate }}</span
>: </strong
>{{ annotation.dictionary | humanize: false }}
</div>
<div *ngIf="annotation.shortContent && !annotation.isHint">
<strong><span translate="content"></span>: </strong
>{{ annotation.shortContent }}
</div>
</div>
<div class="active-icon-marker-container">
<redaction-round-checkbox
*ngIf="
multiSelectActive && annotationIsSelected(annotation)
"
[active]="true"
></redaction-round-checkbox>
</div>
</div>
<div class="actions-wrapper">
<div <div
(click)="toggleExpandComments(annotation, $event)" [matTooltip]="annotation.content"
[matTooltip]="commentsTooltip(annotation)" class="details"
class="comments-counter"
matTooltipPosition="above" matTooltipPosition="above"
> >
<mat-icon svgIcon="red:comment"></mat-icon> <redaction-type-annotation-icon
{{ annotation.comments.length }} [annotation]="annotation"
></redaction-type-annotation-icon>
<div class="flex-1">
<div>
<strong>{{ annotation.typeLabel | translate }}</strong>
</div>
<div *ngIf="annotation?.dictionary !== 'manual'">
<strong>
<span>{{ annotation.descriptor | translate }}</span
>: </strong
>{{ annotation.dictionary | humanize: false }}
</div>
<div *ngIf="annotation.shortContent && !annotation.isHint">
<strong><span translate="content"></span>: </strong
>{{ annotation.shortContent }}
</div>
</div>
<div class="active-icon-marker-container">
<redaction-round-checkbox
*ngIf="
multiSelectActive &&
annotationIsSelected(annotation)
"
[active]="true"
></redaction-round-checkbox>
</div>
</div> </div>
<div class="actions" *ngIf="!multiSelectActive">
<ng-container <div class="actions-wrapper">
[ngTemplateOutletContext]="{ annotation: annotation }" <div
[ngTemplateOutlet]="annotationActionsTemplate" (click)="toggleExpandComments(annotation, $event)"
></ng-container> [matTooltip]="commentsTooltip(annotation)"
class="comments-counter"
matTooltipPosition="above"
>
<mat-icon svgIcon="red:comment"></mat-icon>
{{ annotation.comments.length }}
</div>
<div *ngIf="!multiSelectActive" class="actions">
<ng-container
[ngTemplateOutletContext]="{ annotation: annotation }"
[ngTemplateOutlet]="annotationActionsTemplate"
></ng-container>
</div>
</div> </div>
</div> <redaction-comments [annotation]="annotation"></redaction-comments>
<redaction-comments [annotation]="annotation"></redaction-comments> </redaction-hidden-action>
</redaction-hidden-action> </div>
</div> </div>
</div> </div>
</div> </ng-container>
<redaction-page-exclusion
(actionPerformed)="actionPerformed.emit($event)"
*ngIf="excludePages"
[fileData]="fileData"
></redaction-page-exclusion>
</div> </div>
</div> </div>
</div> </div>

View File

@ -233,7 +233,7 @@
} }
&:hover { &:hover {
background-color: #f9fafb; background-color: $grey-8;
::ng-deep .annotation-actions { ::ng-deep .annotation-actions {
display: flex; display: flex;

View File

@ -40,6 +40,7 @@ export class FileWorkloadComponent {
@Input() secondaryFilters: FilterModel[]; @Input() secondaryFilters: FilterModel[];
@Input() fileData: FileDataModel; @Input() fileData: FileDataModel;
@Input() hideSkipped: boolean; @Input() hideSkipped: boolean;
@Input() excludePages: boolean;
@Input() annotationActionsTemplate: TemplateRef<any>; @Input() annotationActionsTemplate: TemplateRef<any>;
@Output() shouldDeselectAnnotationsOnPageChangeChange = new EventEmitter<boolean>(); @Output() shouldDeselectAnnotationsOnPageChangeChange = new EventEmitter<boolean>();
@Output() selectAnnotations = new EventEmitter< @Output() selectAnnotations = new EventEmitter<
@ -49,6 +50,8 @@ export class FileWorkloadComponent {
@Output() selectPage = new EventEmitter<number>(); @Output() selectPage = new EventEmitter<number>();
@Output() toggleSkipped = new EventEmitter<any>(); @Output() toggleSkipped = new EventEmitter<any>();
@Output() annotationsChanged = new EventEmitter<AnnotationWrapper>(); @Output() annotationsChanged = new EventEmitter<AnnotationWrapper>();
@Output() closeExcludePagesView = new EventEmitter();
@Output() actionPerformed = new EventEmitter<string>();
displayedPages: number[] = []; displayedPages: number[] = [];
pagesPanelActive = true; pagesPanelActive = true;
@ViewChildren(CommentsComponent) annotationCommentsComponents: QueryList<CommentsComponent>; @ViewChildren(CommentsComponent) annotationCommentsComponents: QueryList<CommentsComponent>;
@ -56,9 +59,9 @@ export class FileWorkloadComponent {
@ViewChild('quickNavigation') private _quickNavigationElement: ElementRef; @ViewChild('quickNavigation') private _quickNavigationElement: ElementRef;
constructor( constructor(
private readonly _permissionsService: PermissionsService,
private readonly _changeDetectorRef: ChangeDetectorRef, private readonly _changeDetectorRef: ChangeDetectorRef,
private readonly _annotationProcessingService: AnnotationProcessingService, private readonly _annotationProcessingService: AnnotationProcessingService,
private readonly _permissionsService: PermissionsService,
private readonly _translateService: TranslateService private readonly _translateService: TranslateService
) {} ) {}
@ -71,18 +74,6 @@ export class FileWorkloadComponent {
private _multiSelectActive = false; private _multiSelectActive = false;
get isProcessing(): boolean {
return this.fileData.fileStatus?.isProcessing;
}
get activeAnnotationsLength(): number | undefined {
return this.displayedAnnotations[this.activeViewerPage]?.annotations?.length;
}
get isReadOnly(): boolean {
return !this._permissionsService.canPerformAnnotationActions();
}
get multiSelectActive(): boolean { get multiSelectActive(): boolean {
return this._multiSelectActive; return this._multiSelectActive;
} }
@ -97,6 +88,18 @@ export class FileWorkloadComponent {
} }
} }
get isProcessing(): boolean {
return this.fileData?.fileStatus?.isProcessing;
}
get activeAnnotationsLength(): number | undefined {
return this.displayedAnnotations[this.activeViewerPage]?.annotations?.length;
}
get isReadOnly(): boolean {
return !this._permissionsService.canPerformAnnotationActions();
}
private get _firstSelectedAnnotation() { private get _firstSelectedAnnotation() {
return this.selectedAnnotations?.length ? this.selectedAnnotations[0] : null; return this.selectedAnnotations?.length ? this.selectedAnnotations[0] : null;
} }
@ -224,15 +227,21 @@ export class FileWorkloadComponent {
} }
scrollAnnotationsToPage(page: number, mode: 'always' | 'if-needed' = 'if-needed') { scrollAnnotationsToPage(page: number, mode: 'always' | 'if-needed' = 'if-needed') {
const elements: any[] = this._annotationsElement.nativeElement.querySelectorAll( if (this._annotationsElement) {
`div[anotation-page-header="${page}"]` const elements: any[] = this._annotationsElement.nativeElement.querySelectorAll(
); `div[anotation-page-header="${page}"]`
FileWorkloadComponent._scrollToFirstElement(elements, mode); );
FileWorkloadComponent._scrollToFirstElement(elements, mode);
}
} }
@debounce() @debounce()
scrollToSelectedAnnotation() { scrollToSelectedAnnotation() {
if (!this.selectedAnnotations || this.selectedAnnotations.length === 0) { if (
!this.selectedAnnotations ||
this.selectedAnnotations.length === 0 ||
!this._annotationsElement
) {
return; return;
} }
const elements: any[] = this._annotationsElement.nativeElement.querySelectorAll( const elements: any[] = this._annotationsElement.nativeElement.querySelectorAll(
@ -420,9 +429,11 @@ export class FileWorkloadComponent {
} }
private _scrollQuickNavigationToPage(page: number) { private _scrollQuickNavigationToPage(page: number) {
const elements: any[] = this._quickNavigationElement.nativeElement.querySelectorAll( if (this._quickNavigationElement) {
`#quick-nav-page-${page}` const elements: any[] = this._quickNavigationElement.nativeElement.querySelectorAll(
); `#quick-nav-page-${page}`
FileWorkloadComponent._scrollToFirstElement(elements); );
FileWorkloadComponent._scrollToFirstElement(elements);
}
} }
} }

View File

@ -0,0 +1,41 @@
<div *ngIf="permissionsService.canExcludePages()" class="exclude-pages-input-container">
<redaction-input-with-action
(action)="excludePagesRange()"
[form]="excludePagesForm"
hint="file-preview.tabs.exclude-pages.hint"
icon="red:check"
placeholder="file-preview.tabs.exclude-pages.input-placeholder"
type="action"
width="full"
></redaction-input-with-action>
</div>
<div *ngIf="excludedPagesRanges.length" class="all-caps-label-container">
<div
class="all-caps-label"
translate="file-preview.tabs.exclude-pages.removed-from-redaction"
></div>
</div>
<div class="ranges" redactionHasScrollbar>
<div *ngFor="let range of excludedPagesRanges" class="range">
<ng-container *ngIf="range.startPage === range.endPage">
{{ range.startPage }}
</ng-container>
<ng-container *ngIf="range.startPage !== range.endPage">
{{ range.startPage }}-{{ range.endPage }}
</ng-container>
<redaction-circle-button
(action)="includePagesRange(range)"
*ngIf="permissionsService.canExcludePages()"
icon="red:undo"
tooltip="file-preview.tabs.exclude-pages.put-back"
></redaction-circle-button>
</div>
</div>
<div
*ngIf="!permissionsService.canExcludePages() && !excludedPagesRanges.length"
class="no-excluded heading-l"
translate="file-preview.tabs.exclude-pages.no-excluded"
></div>

View File

@ -0,0 +1,60 @@
@import '../../../../../assets/styles/red-variables';
@import '../../../../../assets/styles/red-mixins';
:host {
height: 100%;
display: flex;
flex-direction: column;
.exclude-pages-input-container {
background-color: $grey-6;
padding: 15px 15px 16px 14px;
}
.all-caps-label-container {
padding: 8px 16px;
border-bottom: 1px solid $separator;
}
.ranges {
overflow: hidden;
.range {
padding-left: 17px;
padding-right: 16px;
border-bottom: 1px solid $separator;
transition: background-color 0.2s;
display: flex;
align-items: center;
justify-content: space-between;
height: 50px;
redaction-circle-button {
display: none;
}
&:hover {
background-color: $grey-8;
redaction-circle-button {
display: initial;
}
}
}
&.has-scrollbar:hover {
@include scroll-bar;
overflow: auto;
.range {
padding-right: 5px;
}
}
}
.no-excluded {
padding: 50px 16px;
text-align: center;
opacity: 0.7;
}
}

View File

@ -0,0 +1,108 @@
import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core';
import { PermissionsService } from '../../../../services/permissions.service';
import { FormBuilder, FormGroup } from '@angular/forms';
import { PageRange, ReanalysisControllerService } from '@redaction/red-ui-http';
import { FileDataModel } from '../../../../models/file/file-data.model';
import { NotificationService, NotificationType } from '../../../../services/notification.service';
import { LoadingService } from '../../../../services/loading.service';
import { TranslateService } from '@ngx-translate/core';
@Component({
selector: 'redaction-page-exclusion',
templateUrl: './page-exclusion.component.html',
styleUrls: ['./page-exclusion.component.scss']
})
export class PageExclusionComponent implements OnChanges {
@Input() fileData: FileDataModel;
@Output() actionPerformed = new EventEmitter<string>();
excludePagesForm: FormGroup;
excludedPagesRanges: PageRange[] = [];
constructor(
readonly permissionsService: PermissionsService,
private readonly _formBuilder: FormBuilder,
private readonly _reanalysisControllerService: ReanalysisControllerService,
private readonly _notificationService: NotificationService,
private readonly _loadingService: LoadingService,
private readonly _translateService: TranslateService
) {
this.excludePagesForm = this._formBuilder.group({
value: ['']
});
}
ngOnChanges(changes: SimpleChanges) {
if (changes.fileData) {
const excludedPages = (this.fileData?.fileStatus?.excludedPages || []).sort(
(p1, p2) => p1 - p2
);
this.excludedPagesRanges = excludedPages.reduce((ranges, page) => {
if (!ranges.length) {
return [{ startPage: page, endPage: page }];
}
if (page === ranges[ranges.length - 1].endPage + 1) {
ranges[ranges.length - 1].endPage = page;
} else {
ranges.push({ startPage: page, endPage: page });
}
return ranges;
}, []);
}
}
async excludePagesRange() {
this._loadingService.start();
try {
const pageRanges = this.excludePagesForm
.get('value')
.value.split(',')
.map(range => {
const splitted = range.split('-');
const startPage = parseInt(splitted[0], 10);
const endPage = splitted.length > 1 ? parseInt(splitted[1], 10) : startPage;
if (!startPage || !endPage) {
throw new Error();
}
return {
startPage,
endPage
};
});
await this._reanalysisControllerService
.excludePages(
{
pageRanges: pageRanges
},
this.fileData.fileStatus.dossierId,
this.fileData.fileStatus.fileId
)
.toPromise();
this.excludePagesForm.reset();
this.actionPerformed.emit('exclude-pages');
} catch (e) {
this._notificationService.showToastNotification(
this._translateService.instant('file-preview.tabs.exclude-pages.error'),
null,
NotificationType.ERROR
);
this._loadingService.stop();
}
}
async includePagesRange(range: PageRange) {
this._loadingService.start();
await this._reanalysisControllerService
.includePages(
{
pageRanges: [range]
},
this.fileData.fileStatus.dossierId,
this.fileData.fileStatus.fileId
)
.toPromise();
this.excludePagesForm.reset();
this.actionPerformed.emit('exclude-pages');
}
}

View File

@ -45,6 +45,7 @@ import { EditDossierTeamMembersComponent } from './dialogs/edit-dossier-dialog/t
import { TeamMembersManagerComponent } from './components/team-members-manager/team-members-manager.component'; import { TeamMembersManagerComponent } from './components/team-members-manager/team-members-manager.component';
import { TeamMembersDialogComponent } from './dialogs/team-members-dialog/team-members-dialog.component'; import { TeamMembersDialogComponent } from './dialogs/team-members-dialog/team-members-dialog.component';
import { ScrollButtonComponent } from './components/scroll-button/scroll-button.component'; import { ScrollButtonComponent } from './components/scroll-button/scroll-button.component';
import { PageExclusionComponent } from './components/page-exclusion/page-exclusion.component';
const screens = [ const screens = [
DossierListingScreenComponent, DossierListingScreenComponent,
@ -87,6 +88,7 @@ const components = [
EditDossierTeamMembersComponent, EditDossierTeamMembersComponent,
TeamMembersManagerComponent, TeamMembersManagerComponent,
ScrollButtonComponent, ScrollButtonComponent,
PageExclusionComponent,
...screens, ...screens,
...dialogs ...dialogs

View File

@ -56,11 +56,11 @@
</ng-container> </ng-container>
<redaction-assign-user-dropdown <redaction-assign-user-dropdown
*ngIf="editingReviewer"
[value]="currentReviewer"
[options]="singleUsersSelectOptions"
(save)="assignReviewer($event)"
(cancel)="editingReviewer = false" (cancel)="editingReviewer = false"
(save)="assignReviewer($event)"
*ngIf="editingReviewer"
[options]="singleUsersSelectOptions"
[value]="currentReviewer"
></redaction-assign-user-dropdown> ></redaction-assign-user-dropdown>
<div *ngIf="canAssign" class="assign-actions-wrapper"> <div *ngIf="canAssign" class="assign-actions-wrapper">
@ -95,6 +95,7 @@
(actionPerformed)="fileActionPerformed($event)" (actionPerformed)="fileActionPerformed($event)"
*ngIf="viewReady" *ngIf="viewReady"
[activeDocumentInfo]="viewDocumentInfo" [activeDocumentInfo]="viewDocumentInfo"
[activeExcludePages]="excludePages"
></redaction-file-actions> ></redaction-file-actions>
<redaction-circle-button <redaction-circle-button
@ -152,7 +153,12 @@
<div class="right-container"> <div class="right-container">
<redaction-empty-state <redaction-empty-state
*ngIf="viewReady && appStateService.activeFile.isExcluded && !viewDocumentInfo" *ngIf="
viewReady &&
appStateService.activeFile.isExcluded &&
!viewDocumentInfo &&
!excludePages
"
[horizontalPadding]="40" [horizontalPadding]="40"
icon="red:needs-work" icon="red:needs-work"
text="file-preview.tabs.is-excluded" text="file-preview.tabs.is-excluded"
@ -166,7 +172,9 @@
<redaction-file-workload <redaction-file-workload
#fileWorkloadComponent #fileWorkloadComponent
(actionPerformed)="fileActionPerformed($event)"
(annotationsChanged)="annotationsChangedByReviewAction($event)" (annotationsChanged)="annotationsChangedByReviewAction($event)"
(closeExcludePagesView)="excludePages = false"
(deselectAnnotations)="deselectAnnotations($event)" (deselectAnnotations)="deselectAnnotations($event)"
(selectAnnotations)="selectAnnotations($event)" (selectAnnotations)="selectAnnotations($event)"
(selectPage)="selectPage($event)" (selectPage)="selectPage($event)"
@ -177,6 +185,7 @@
[annotationActionsTemplate]="annotationActionsTemplate" [annotationActionsTemplate]="annotationActionsTemplate"
[annotations]="annotations" [annotations]="annotations"
[dialogRef]="dialogRef" [dialogRef]="dialogRef"
[excludePages]="excludePages"
[fileData]="fileData" [fileData]="fileData"
[hideSkipped]="hideSkipped" [hideSkipped]="hideSkipped"
[primaryFilters]="primaryFilters" [primaryFilters]="primaryFilters"

View File

@ -45,6 +45,7 @@ import {
handleFilterDelta, handleFilterDelta,
processFilters processFilters
} from '@shared/components/filters/popup-filter/utils/filter-utils'; } from '@shared/components/filters/popup-filter/utils/filter-utils';
import { LoadingService } from '../../../../services/loading.service';
const ALL_HOTKEY_ARRAY = ['Escape', 'F', 'f']; const ALL_HOTKEY_ARRAY = ['Escape', 'F', 'f'];
@ -71,6 +72,7 @@ export class FilePreviewScreenComponent implements OnInit, OnDestroy, OnAttach,
hideSkipped = false; hideSkipped = false;
displayPDFViewer = false; displayPDFViewer = false;
viewDocumentInfo = false; viewDocumentInfo = false;
excludePages = false;
private _instance: WebViewerInstance; private _instance: WebViewerInstance;
private _lastPage: string; private _lastPage: string;
@ -94,7 +96,8 @@ export class FilePreviewScreenComponent implements OnInit, OnDestroy, OnAttach,
private readonly _fileDownloadService: PdfViewerDataService, private readonly _fileDownloadService: PdfViewerDataService,
private readonly _statusControllerService: StatusControllerService, private readonly _statusControllerService: StatusControllerService,
private readonly _ngZone: NgZone, private readonly _ngZone: NgZone,
private readonly _fileManagementControllerService: FileManagementControllerService private readonly _fileManagementControllerService: FileManagementControllerService,
private readonly _loadingService: LoadingService
) { ) {
document.documentElement.addEventListener('fullscreenchange', () => { document.documentElement.addEventListener('fullscreenchange', () => {
if (!document.fullscreenElement) { if (!document.fullscreenElement) {
@ -169,6 +172,36 @@ export class FilePreviewScreenComponent implements OnInit, OnDestroy, OnAttach,
return this.appStateService.activeFile.fileStatus.lastReviewer; return this.appStateService.activeFile.fileStatus.lastReviewer;
} }
get assignOrChangeReviewerTooltip() {
return this.currentReviewer
? 'file-preview.change-reviewer'
: 'file-preview.assign-reviewer';
}
get currentReviewer(): string {
return this.appStateService.activeFile.currentReviewer;
}
get status(): FileStatus.StatusEnum {
return this.appStateService.activeFile.status;
}
get statusBarConfig(): [{ length: number; color: FileStatus.StatusEnum }] {
return [{ length: 1, color: this.status }];
}
get isUnderReviewOrApproval(): boolean {
return this.status === 'UNDER_REVIEW' || this.status === 'UNDER_APPROVAL';
}
get canAssignReviewer(): boolean {
return (
!this.currentReviewer &&
this.permissionsService.canAssignUser() &&
this.appStateService.activeDossier.hasMoreThanOneReviewer
);
}
updateViewMode() { updateViewMode() {
const annotations = this._getAnnotations(a => a.getCustomData('redacto-manager')); const annotations = this._getAnnotations(a => a.getCustomData('redacto-manager'));
const redactions = annotations.filter(a => a.getCustomData('redaction')); const redactions = annotations.filter(a => a.getCustomData('redaction'));
@ -441,10 +474,22 @@ export class FilePreviewScreenComponent implements OnInit, OnDestroy, OnAttach,
await this.appStateService.reloadActiveDossierFiles(); await this.appStateService.reloadActiveDossierFiles();
return; return;
case 'exclude-pages':
await this.appStateService.reloadActiveDossierFiles();
await this._loadFileData();
this._loadingService.stop();
return;
case 'view-document-info': case 'view-document-info':
this.viewDocumentInfo = !this.viewDocumentInfo; this.viewDocumentInfo = !this.viewDocumentInfo;
return; return;
case 'view-exclude-pages':
this.excludePages = !this.excludePages;
this._workloadComponent.multiSelectActive = false;
this.viewDocumentInfo = false;
return;
default: default:
this._updateCanPerformActions(); this._updateCanPerformActions();
} }
@ -509,36 +554,6 @@ export class FilePreviewScreenComponent implements OnInit, OnDestroy, OnAttach,
return false; return false;
} }
get assignOrChangeReviewerTooltip() {
return this.currentReviewer
? 'file-preview.change-reviewer'
: 'file-preview.assign-reviewer';
}
get currentReviewer(): string {
return this.appStateService.activeFile.currentReviewer;
}
get status(): FileStatus.StatusEnum {
return this.appStateService.activeFile.status;
}
get statusBarConfig(): [{ length: number; color: FileStatus.StatusEnum }] {
return [{ length: 1, color: this.status }];
}
get isUnderReviewOrApproval(): boolean {
return this.status === 'UNDER_REVIEW' || this.status === 'UNDER_APPROVAL';
}
get canAssignReviewer(): boolean {
return (
!this.currentReviewer &&
this.permissionsService.canAssignUser() &&
this.appStateService.activeDossier.hasMoreThanOneReviewer
);
}
// <!-- Dev Mode Features--> // <!-- Dev Mode Features-->
async openSSRFilePreview() { async openSSRFilePreview() {
window.open(`/pdf-preview/${this.dossierId}/${this.fileId}`, '_blank'); window.open(`/pdf-preview/${this.dossierId}/${this.fileId}`, '_blank');
@ -576,7 +591,6 @@ export class FilePreviewScreenComponent implements OnInit, OnDestroy, OnAttach,
private async _loadFileData(performUpdate: boolean = false): Promise<void> { private async _loadFileData(performUpdate: boolean = false): Promise<void> {
const fileData = await this._fileDownloadService.loadActiveFileData().toPromise(); const fileData = await this._fileDownloadService.loadActiveFileData().toPromise();
console.log(fileData);
if (!fileData.fileStatus.isPending && !fileData.fileStatus.isError) { if (!fileData.fileStatus.isPending && !fileData.fileStatus.isError) {
if (performUpdate) { if (performUpdate) {
this.fileData.redactionLog = fileData.redactionLog; this.fileData.redactionLog = fileData.redactionLog;

View File

@ -30,19 +30,19 @@ export class ManualAnnotationService {
addComment(comment: string, annotationId: string) { addComment(comment: string, annotationId: string) {
return this._manualRedactionControllerService.addComment( return this._manualRedactionControllerService.addComment(
{ text: comment }, { text: comment },
annotationId,
this._appStateService.activeDossierId, this._appStateService.activeDossierId,
this._appStateService.activeFileId, this._appStateService.activeFileId
annotationId
); );
} }
// this wraps /manualRedaction/comment/undo // this wraps /manualRedaction/comment/undo
deleteComment(commentId: string, annotationId: string) { deleteComment(commentId: string, annotationId: string) {
return this._manualRedactionControllerService.undoComment( return this._manualRedactionControllerService.undoComment(
this._appStateService.activeDossierId,
this._appStateService.activeFileId,
annotationId, annotationId,
commentId commentId,
this._appStateService.activeDossierId,
this._appStateService.activeFileId
); );
} }

View File

@ -39,6 +39,7 @@ export class IconsModule {
'edit', 'edit',
'entries', 'entries',
'error', 'error',
'exclude-pages',
'exit-fullscreen', 'exit-fullscreen',
'expand', 'expand',
'folder', 'folder',

View File

@ -1,4 +1,4 @@
<form (submit)="type === 'submit' && submit()" [formGroup]="form"> <form [formGroup]="form">
<div [style.max-width]="computedWidth" [style.width]="computedWidth" class="red-input-group"> <div [style.max-width]="computedWidth" [style.width]="computedWidth" class="red-input-group">
<input <input
[formControlName]="formControlName" [formControlName]="formControlName"
@ -8,6 +8,8 @@
type="text" type="text"
/> />
<span *ngIf="hint" [translate]="hint" class="hint"></span>
<!-- Search--> <!-- Search-->
<mat-icon <mat-icon
*ngIf="type === 'search' && !hasContent" *ngIf="type === 'search' && !hasContent"
@ -26,11 +28,11 @@
<!-- Submit--> <!-- Submit-->
<redaction-circle-button <redaction-circle-button
(action)="submit($event)" (action)="executeAction($event)"
*ngIf="type === 'submit'" *ngIf="type === 'action'"
[disabled]="form.invalid" [disabled]="!hasContent"
[icon]="icon"
[size]="25" [size]="25"
icon="red:collapse"
> >
</redaction-circle-button> </redaction-circle-button>
</div> </div>

View File

@ -9,6 +9,6 @@ mat-icon.disabled {
redaction-circle-button { redaction-circle-button {
position: absolute; position: absolute;
bottom: 5px; top: 4px;
right: 5px; right: 5px;
} }

View File

@ -9,8 +9,10 @@ import { FormGroup } from '@angular/forms';
export class InputWithActionComponent { export class InputWithActionComponent {
@Input() form: FormGroup; @Input() form: FormGroup;
@Input() placeholder: string; @Input() placeholder: string;
@Input() hint: string;
@Input() width: number | 'full' = 250; @Input() width: number | 'full' = 250;
@Input() type: 'search' | 'submit'; @Input() type: 'search' | 'action';
@Input() icon: string;
@Output() action = new EventEmitter<any>(); @Output() action = new EventEmitter<any>();
get formControlName(): 'query' | 'value' { get formControlName(): 'query' | 'value' {
@ -18,7 +20,7 @@ export class InputWithActionComponent {
} }
get hasContent() { get hasContent() {
return !!this.form.get(this.formControlName).value.length; return !!this.form.get(this.formControlName).value?.length;
} }
get computedWidth() { get computedWidth() {
@ -29,7 +31,7 @@ export class InputWithActionComponent {
this.form.patchValue({ query: '' }); this.form.patchValue({ query: '' });
} }
submit($event?: MouseEvent) { executeAction($event?: MouseEvent) {
$event?.stopPropagation(); $event?.stopPropagation();
if (this.hasContent) { if (this.hasContent) {
this.action.emit(); this.action.emit();

View File

@ -216,4 +216,11 @@ export class PermissionsService {
canAddComment(fileStatus = this._activeFile): boolean { canAddComment(fileStatus = this._activeFile): boolean {
return this.isFileReviewer(fileStatus) || this.isApprover(); return this.isFileReviewer(fileStatus) || this.isApprover();
} }
canExcludePages(fileStatus = this._activeFile): boolean {
return (
['UNDER_REVIEW', 'UNDER_APPROVAL'].includes(fileStatus.status) &&
(this.isFileReviewer(fileStatus) || this.isApprover())
);
}
} }

View File

@ -375,7 +375,19 @@
"label": "Workload", "label": "Workload",
"select": "Select", "select": "Select",
"jump-to-previous": "Jump to Previous", "jump-to-previous": "Jump to Previous",
"jump-to-next": "Jump to Next" "jump-to-next": "Jump to Next",
"select-all": "All",
"select-none": "None"
},
"exclude-pages": {
"label": "Exclude Pages",
"close": "Close",
"error": "Error! Invalid page selection.",
"input-placeholder": "e.g. 1-20,22,32",
"hint": "Minus (-) for range and comma (,) for enumeration.",
"removed-from-redaction": "Removed from redaction",
"put-back": "Put back",
"no-excluded": "No excluded pages."
}, },
"is-excluded": "Redaction is disabled for this document." "is-excluded": "Redaction is disabled for this document."
}, },
@ -392,6 +404,7 @@
"last-reviewer": "Last Reviewed by:", "last-reviewer": "Last Reviewed by:",
"fullscreen": "Full Screen (F)", "fullscreen": "Full Screen (F)",
"document-info": "Your Document Info lives here. This includes metadata required on each document.", "document-info": "Your Document Info lives here. This includes metadata required on each document.",
"exclude-pages": "Exclude pages from redaction",
"new-tab-ssr": "Open Document in Server Side Rendering Mode", "new-tab-ssr": "Open Document in Server Side Rendering Mode",
"html-debug": "Open Document HTML Debug", "html-debug": "Open Document HTML Debug",
"download-original-file": "Download Original File", "download-original-file": "Download Original File",

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="14px" version="1.1" viewBox="0 0 14 14" width="14px"
xmlns="http://www.w3.org/2000/svg">
<g fill="none" fill-rule="evenodd" id="Styleguide" stroke="none" stroke-width="1">
<g fill="currentColor" fill-rule="nonzero"
id="Export-just-icons" transform="translate(-346.000000, -902.000000)">
<g id="status" transform="translate(346.000000, 902.000000)">
<path
d="M14,0 L14,11.2 L11.2,11.2 L11.2,14 L0,14 L0,2.8 L2.8,2.8 L2.8,0 L14,0 Z M2.8,4.2 L1.4,4.2 L1.4,12.6 L9.8,12.6 L9.8,11.2 L2.8,11.2 L2.8,4.2 Z M12.6,1.4 L4.2,1.4 L4.2,9.8 L12.6,9.8 L12.6,1.4 Z M9.88492424,3.12512627 L10.8748737,4.11507576 L9.39,5.6 L10.8748737,7.08492424 L9.88492424,8.07487373 L8.4,6.59 L6.91507576,8.07487373 L5.92512627,7.08492424 L7.41,5.6 L5.92512627,4.11507576 L6.91507576,3.12512627 L8.4,4.61 L9.88492424,3.12512627 Z"
id="pages"></path>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1012 B

View File

@ -106,7 +106,7 @@ cdk-virtual-scroll-viewport {
&:hover { &:hover {
> div { > div {
background-color: #f9fafb; background-color: $grey-8;
&.selection-column redaction-round-checkbox { &.selection-column redaction-round-checkbox {
opacity: 1; opacity: 1;