Merge branch 'master' into RED-3313

This commit is contained in:
Adina Țeudan 2022-03-08 16:43:41 +02:00
commit 403e6effc3
58 changed files with 741 additions and 305 deletions

View File

@ -1,4 +1,4 @@
<iqser-help-mode *ngIf="userPreferenceService.areDevFeaturesEnabled"></iqser-help-mode>
<iqser-help-mode></iqser-help-mode>
<div class="top-bar">
<div *ngIf="!currentUser.isUser" class="menu-placeholder"></div>

View File

@ -1,6 +1,7 @@
<div class="content-container" iqserHasScrollbar>
<div class="heading-xl" translate="reports-screen.title"></div>
<div class="description" translate="reports-screen.setup"></div>
<div class="description" translate="reports-screen.description"></div>
<div *ngIf="placeholders$ | async as placeholders" class="placeholders">

View File

@ -121,15 +121,15 @@ export class DossierOverviewBulkActionsComponent implements OnChanges {
{
type: ActionTypes.circleBtn,
action: () => this._bulkActionsService.toggleAutomaticAnalysis(this.selectedFiles),
tooltip: _('dossier-overview.disable-auto-analysis'),
icon: 'red:stop',
tooltip: _('dossier-overview.stop-auto-analysis'),
icon: 'red:disable-analysis',
show: this.#canDisableAutoAnalysis,
},
{
type: ActionTypes.circleBtn,
action: () => this._bulkActionsService.toggleAutomaticAnalysis(this.selectedFiles),
tooltip: _('dossier-overview.enable-auto-analysis'),
icon: 'red:play',
tooltip: _('dossier-overview.start-auto-analysis'),
icon: 'red:enable-analysis',
show: this.#canEnableAutoAnalysis,
},

View File

@ -71,7 +71,7 @@ export class DossierOverviewScreenHeaderComponent implements OnInit {
const fileName = this.dossier.dossierName + '.export.csv';
const mapper = (file?: IFile) => ({
...file,
assignee: this._userService.getNameForId(file.assignee),
assignee: this._userService.getNameForId(file.assignee) || '-',
primaryAttribute: this._primaryFileAttributeService.getPrimaryFileAttributeValue(file, this.dossier.dossierTemplateId),
});
const fileFields = [

View File

@ -7,7 +7,7 @@
</div>
<div class="cell">
<redaction-date-column [date]="file.annotationModificationDate" [isError]="file.isError"></redaction-date-column>
<redaction-date-column [date]="file.redactionModificationDate" [isError]="file.isError"></redaction-date-column>
</div>
<div *ngFor="let config of displayedAttributes" class="cell">

View File

@ -16,7 +16,6 @@ import { WorkflowItemComponent } from './components/workflow-item/workflow-item.
import { DossierOverviewScreenHeaderComponent } from './components/screen-header/dossier-overview-screen-header.component';
import { ViewModeSelectionComponent } from './components/view-mode-selection/view-mode-selection.component';
import { FileNameColumnComponent } from './components/table-item/file-name-column/file-name-column.component';
import { DateColumnComponent } from './components/table-item/date-column/date-column.component';
const routes: Routes = [
{
@ -42,7 +41,6 @@ const routes: Routes = [
DossierOverviewScreenHeaderComponent,
ViewModeSelectionComponent,
FileNameColumnComponent,
DateColumnComponent,
],
imports: [RouterModule.forChild(routes), CommonModule, SharedModule, SharedDossiersModule, IqserIconsModule, TranslateModule],
})

View File

@ -3,6 +3,11 @@
<redaction-dossiers-listing-dossier-name [dossierStats]="stats" [dossier]="dossier"></redaction-dossiers-listing-dossier-name>
</div>
<!--TODO: when dossier-stats api is updated show dossier's last modification date-->
<!-- <div class="cell">-->
<!-- <redaction-date-column [date]="stats.redactionModificationDate"></redaction-date-column>-->
<!-- </div>-->
<div class="cell">
<redaction-dossier-workload-column [dossierStats]="stats" [dossier]="dossier"></redaction-dossier-workload-column>
</div>

View File

@ -26,6 +26,7 @@ export class ConfigService {
get tableConfig(): TableColumnConfig<Dossier>[] {
return [
{ label: _('dossier-listing.table-col-names.name'), sortByKey: 'searchKey', width: '2fr' },
// { label: _('dossier-listing.table-col-names.last-modified') },
{ label: _('dossier-listing.table-col-names.needs-work') },
{ label: _('dossier-listing.table-col-names.owner'), class: 'user-column' },
{ label: _('dossier-listing.table-col-names.documents-status'), class: 'flex-end', width: 'auto' },

View File

@ -33,7 +33,7 @@
[tooltip]="'annotation-actions.resize.label' | translate"
[type]="buttonType"
icon="red:resize"
iqserHelpMode="redaction_resize_redaction"
[iqserHelpMode]="helpModeKey + '_resize'"
[scrollableParentView]="scrollableParentView"
></iqser-circle-button>
@ -149,7 +149,7 @@
[tooltip]="'annotation-actions.remove-annotation.remove-from-dict' | translate"
[type]="buttonType"
icon="red:remove-from-dict"
iqserHelpMode="remove_from_dictionary"
[iqserHelpMode]="helpModeKey + '_remove_from_dictionary'"
[scrollableParentView]="scrollableParentView"
></iqser-circle-button>
@ -160,7 +160,7 @@
[tooltip]="'annotation-actions.remove-annotation.false-positive' | translate"
[type]="buttonType"
icon="red:thumb-down"
iqserHelpMode="redaction_false_positive"
[iqserHelpMode]="helpModeKey + '_false_positive'"
[scrollableParentView]="scrollableParentView"
></iqser-circle-button>
@ -171,7 +171,7 @@
[tooltip]="'annotation-actions.remove-annotation.only-here' | translate"
[type]="buttonType"
icon="iqser:trash"
iqserHelpMode="redaction_remove_only_here"
[iqserHelpMode]="helpModeKey + '_remove_only_here'"
[scrollableParentView]="scrollableParentView"
></iqser-circle-button>
</ng-container>

View File

@ -127,4 +127,8 @@ export class AnnotationActionsComponent implements OnChanges {
this.annotations,
);
}
get helpModeKey() {
return this.annotations[0]?.typeLabel?.split('.')[1];
}
}

View File

@ -33,6 +33,11 @@
<div class="right-content">
<div *ngIf="state.isReadonly$ | async" class="justify-center banner read-only d-flex">
<div *ngIf="file.isOcrProcessing" class="ocr-indicator">
OCR
<mat-progress-bar class="white ml-8 w-100" mode="indeterminate"></mat-progress-bar>
</div>
<div class="flex-center">
<mat-icon class="primary-white" svgIcon="red:read-only"></mat-icon>
<span [translate]="(state.dossier$ | async).isActive ? 'readonly' : 'readonly-archived'" class="read-only-text"></span>

View File

@ -24,8 +24,10 @@
}
}
mat-progress-bar {
margin-left: 8px;
.ocr-indicator {
display: flex;
flex: 1;
align-items: center;
}
&.justify-center {

View File

@ -7,8 +7,12 @@
class="page-wrapper"
>
<mat-icon [svgIcon]="showDottedIcon ? 'red:excluded-page' : 'red:page'"></mat-icon>
<div class="text">
{{ number }}
</div>
<div class="text">{{ number }}</div>
<div *ngIf="activeSelection" class="dot"></div>
<div *ngIf="isRotated" class="rotated">
<mat-icon svgIcon="red:rotation"></mat-icon>
</div>
</div>

View File

@ -46,4 +46,24 @@
top: 9px;
right: 10px;
}
.rotated {
position: absolute;
border-radius: 50%;
width: 20px;
height: 20px;
line-height: 20px;
right: 8px;
top: 8px;
display: flex;
justify-content: center;
background-color: var(--iqser-primary);
color: var(--iqser-white);
mat-icon {
width: 12px;
height: 10px;
opacity: 100%;
}
}
}

View File

@ -1,4 +1,14 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, OnChanges, OnDestroy, Output } from '@angular/core';
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
EventEmitter,
Input,
OnChanges,
OnDestroy,
OnInit,
Output,
} from '@angular/core';
import { PermissionsService } from '@services/permissions.service';
import { ConfigService } from '@services/config.service';
import { ViewedPagesService } from '@services/entity-services/viewed-pages.service';
@ -6,6 +16,7 @@ import { IViewedPage } from '@red/domain';
import { AutoUnsubscribe } from '@iqser/common-ui';
import { FilePreviewStateService } from '../../services/file-preview-state.service';
import { firstValueFrom } from 'rxjs';
import { PageRotationService } from '../../services/page-rotation.service';
@Component({
selector: 'redaction-page-indicator',
@ -13,7 +24,7 @@ import { firstValueFrom } from 'rxjs';
styleUrls: ['./page-indicator.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PageIndicatorComponent extends AutoUnsubscribe implements OnDestroy, OnChanges {
export class PageIndicatorComponent extends AutoUnsubscribe implements OnDestroy, OnChanges, OnInit {
@Input() active = false;
@Input() showDottedIcon = false;
@Input() number: number;
@ -24,6 +35,7 @@ export class PageIndicatorComponent extends AutoUnsubscribe implements OnDestroy
pageReadTimeout: number = null;
read = false;
isRotated = false;
constructor(
private readonly _viewedPagesService: ViewedPagesService,
@ -31,6 +43,7 @@ export class PageIndicatorComponent extends AutoUnsubscribe implements OnDestroy
private readonly _changeDetectorRef: ChangeDetectorRef,
private readonly _permissionService: PermissionsService,
private readonly _stateService: FilePreviewStateService,
readonly pageRotationService: PageRotationService,
) {
super();
}
@ -47,6 +60,13 @@ export class PageIndicatorComponent extends AutoUnsubscribe implements OnDestroy
return this._stateService.fileId;
}
ngOnInit() {
this.addSubscription = this.pageRotationService.isRotated(this.number).subscribe(value => {
this.isRotated = value;
this._changeDetectorRef.detectChanges();
});
}
ngOnChanges() {
this._setReadState();
return this.handlePageRead();

View File

@ -2,18 +2,18 @@
<div #viewer [id]="(stateService.file$ | async).fileId" class="viewer"></div>
</div>
<input #compareFileInput (change)="uploadFile($event.target['files'])" class="file-upload-input" type="file" accept="application/pdf" />
<input #compareFileInput (change)="uploadFile($event.target['files'])" accept="application/pdf" class="file-upload-input" type="file" />
<div *ngIf="utils?.totalPages && utils?.currentPage" class="pagination noselect">
<div (click)="utils.previousPage()">
<div *ngIf="pdf?.totalPages && pdf?.currentPage" class="pagination noselect">
<div (click)="pdf.previousPage()">
<mat-icon class="chevron-icon" svgIcon="red:nav-prev"></mat-icon>
</div>
<div>
<input
#pageInput
(change)="utils.navigateToPage(pageInput.value)"
[max]="utils.totalPages"
[value]="utils.currentPage"
(change)="pdf.navigateToPage(pageInput.value)"
[max]="pdf.totalPages"
[value]="pdf.currentPage"
class="page-number-input"
min="1"
type="number"
@ -21,9 +21,9 @@
</div>
<div class="separator">/</div>
<div>
{{ utils.totalPages }}
{{ pdf.totalPages }}
</div>
<div (click)="utils.nextPage()">
<div (click)="pdf.nextPage()">
<mat-icon class="chevron-icon" svgIcon="red:nav-next"></mat-icon>
</div>
</div>

View File

@ -11,8 +11,8 @@ import {
SimpleChanges,
ViewChild,
} from '@angular/core';
import { Dossier, File, IManualRedactionEntry } from '@red/domain';
import WebViewer, { Core, WebViewerInstance } from '@pdftron/webviewer';
import { Dossier, File, IHeaderElement, IManualRedactionEntry, RotationTypes } from '@red/domain';
import { Core, WebViewerInstance } from '@pdftron/webviewer';
import { TranslateService } from '@ngx-translate/core';
import {
ManualRedactionEntryType,
@ -30,7 +30,7 @@ import { ConfigService } from '@services/config.service';
import { AutoUnsubscribe, ConfirmationDialogInput, LoadingService } from '@iqser/common-ui';
import { DossiersDialogService } from '../../../../services/dossiers-dialog.service';
import { loadCompareDocumentWrapper } from '../../../../utils/compare-mode.utils';
import { PdfViewerUtils } from '../../../../utils/pdf-viewer.utils';
import { PdfViewer } from '../../services/pdf-viewer.service';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { ActivatedRoute } from '@angular/router';
import { toPosition } from '../../../../utils/pdf-calculation.utils';
@ -38,25 +38,19 @@ import { ViewModeService } from '../../services/view-mode.service';
import { MultiSelectService } from '../../services/multi-select.service';
import { FilePreviewStateService } from '../../services/file-preview-state.service';
import { tap, withLatestFrom } from 'rxjs/operators';
import { FileManagementService } from '../../../../../../services/entity-services/file-management.service';
import { PageRotationService } from '../../services/page-rotation.service';
import { ALLOWED_KEYBOARD_SHORTCUTS, HeaderElements, TextPopups } from '../../shared/constants';
import Tools = Core.Tools;
import TextTool = Tools.TextTool;
import Annotation = Core.Annotations.Annotation;
const ALLOWED_KEYBOARD_SHORTCUTS: readonly string[] = ['+', '-', 'p', 'r', 'Escape'] as const;
const dataElements = {
ADD_REDACTION: 'add-redaction',
ADD_DICTIONARY: 'add-dictionary',
ADD_RECTANGLE: 'add-rectangle',
ADD_FALSE_POSITIVE: 'add-false-positive',
SHAPE_TOOL_GROUP_BUTTON: 'shapeToolGroupButton',
RECTANGLE_TOOL_DIVIDER: 'rectangleToolDivider',
TOGGLE_TOOLTIPS: 'toggle-tooltips',
TOGGLE_TOOLTIPS_DIVIDER: 'toggleTooltipsDivider',
ANNOTATION_POPUP: 'annotationPopup',
COMPARE_BUTTON: 'compareButton',
CLOSE_COMPARE_BUTTON: 'closeCompareButton',
COMPARE_TOOL_DIVIDER: 'compareToolDivider',
} as const;
function getDivider(hiddenOn?: readonly ('desktop' | 'mobile' | 'tablet')[]) {
return {
type: 'divider',
hidden: hiddenOn,
};
}
@Component({
selector: 'redaction-pdf-viewer',
@ -79,7 +73,6 @@ export class PdfViewerComponent extends AutoUnsubscribe implements OnInit, OnCha
instance: WebViewerInstance;
documentViewer: Core.DocumentViewer;
annotationManager: Core.AnnotationManager;
utils: PdfViewerUtils;
private _selectedText = '';
constructor(
@ -94,9 +87,12 @@ export class PdfViewerComponent extends AutoUnsubscribe implements OnInit, OnCha
private readonly _annotationActionsService: AnnotationActionsService,
private readonly _configService: ConfigService,
private readonly _loadingService: LoadingService,
private readonly _fileManagementService: FileManagementService,
private readonly _pageRotationService: PageRotationService,
readonly stateService: FilePreviewStateService,
readonly viewModeService: ViewModeService,
readonly multiSelectService: MultiSelectService,
readonly pdf: PdfViewer,
) {
super();
}
@ -158,7 +154,7 @@ export class PdfViewerComponent extends AutoUnsubscribe implements OnInit, OnCha
const loadCompareDocument = async () => {
this._loadingService.start();
this.utils.ready = false;
this.pdf.ready = false;
const mergedDocument = await pdfNet.PDFDoc.create();
const file = await this.stateService.file;
await loadCompareDocumentWrapper(
@ -171,7 +167,7 @@ export class PdfViewerComponent extends AutoUnsubscribe implements OnInit, OnCha
this.viewModeService.compareMode = true;
},
() => {
this.utils.navigateToPage(1);
this.pdf.navigateToPage(1);
},
this.instance.Core.PDFNet,
);
@ -214,30 +210,19 @@ export class PdfViewerComponent extends AutoUnsubscribe implements OnInit, OnCha
const filename = (await this.stateService.file).filename ?? 'document.pdf';
this.instance.UI.loadDocument(currentDocument, { filename });
this.instance.UI.disableElements([dataElements.CLOSE_COMPARE_BUTTON]);
this.instance.UI.enableElements([dataElements.COMPARE_BUTTON]);
this.utils.navigateToPage(1);
this.instance.UI.disableElements([HeaderElements.CLOSE_COMPARE_BUTTON]);
this.instance.UI.enableElements([HeaderElements.COMPARE_BUTTON]);
this.pdf.navigateToPage(1);
}
private async _loadViewer() {
this.instance = await WebViewer(
{
licenseKey: environment.licenseKey ? atob(environment.licenseKey) : null,
fullAPI: true,
path: this._convertPath('/assets/wv-resources'),
css: this._convertPath('/assets/pdftron/stylesheet.css'),
backendType: 'ems',
},
this.viewer.nativeElement as HTMLElement,
);
this.documentViewer = this.instance.Core.documentViewer;
this.annotationManager = this.instance.Core.annotationManager;
this.utils = new PdfViewerUtils(this.instance, this.viewModeService);
this.instance = await this.pdf.loadViewer(this.viewer.nativeElement as HTMLElement);
this.documentViewer = this.pdf.documentViewer;
this.annotationManager = this.pdf.annotationManager;
this._setSelectionMode();
this._configureElements();
this.utils.disableHotkeys();
this.pdf.disableHotkeys();
await this._configureTextPopup();
this.annotationManager.addEventListener('annotationSelected', (annotations: Annotation[], action) => {
@ -247,7 +232,7 @@ export class PdfViewerComponent extends AutoUnsubscribe implements OnInit, OnCha
this._toggleRectangleAnnotationAction(true);
} else {
if (!this.multiSelectService.isEnabled) {
this.utils.deselectAnnotations(this.annotations.filter(wrapper => !nextAnnotations.find(ann => ann.Id === wrapper.id)));
this.pdf.deselectAnnotations(this.annotations.filter(wrapper => !nextAnnotations.find(ann => ann.Id === wrapper.id)));
}
this._configureAnnotationSpecificActions(annotations);
this._toggleRectangleAnnotationAction(annotations.length === 1 && annotations[0].ReadOnly);
@ -265,7 +250,7 @@ export class PdfViewerComponent extends AutoUnsubscribe implements OnInit, OnCha
});
this.documentViewer.addEventListener('pageNumberUpdated', (pageNumber: number) => {
this.utils.deselectAllAnnotations();
this.pdf.deselectAllAnnotations();
this._ngZone.run(() => this.pageChanged.emit(pageNumber));
return this._handleCustomActions();
});
@ -276,9 +261,7 @@ export class PdfViewerComponent extends AutoUnsubscribe implements OnInit, OnCha
// arrows and full-screen
if (($event.target as HTMLElement)?.tagName?.toLowerCase() !== 'input') {
if ($event.key.startsWith('Arrow') || $event.key === 'f') {
this._ngZone.run(() => {
this.keyUp.emit($event);
});
this._ngZone.run(() => this.keyUp.emit($event));
$event.preventDefault();
$event.stopPropagation();
}
@ -292,7 +275,7 @@ export class PdfViewerComponent extends AutoUnsubscribe implements OnInit, OnCha
this.documentViewer.addEventListener('textSelected', async (quads, selectedText, pageNumber: number) => {
this._selectedText = selectedText;
const textActions = [dataElements.ADD_DICTIONARY, dataElements.ADD_FALSE_POSITIVE];
const textActions = [TextPopups.ADD_DICTIONARY, TextPopups.ADD_FALSE_POSITIVE];
const file = await this.stateService.file;
@ -302,7 +285,7 @@ export class PdfViewerComponent extends AutoUnsubscribe implements OnInit, OnCha
this.instance.UI.enableElements(['textPopup']);
}
if (selectedText.length > 2 && this.canPerformActions && !this.utils.isCurrentPageExcluded(file)) {
if (selectedText.length > 2 && this.canPerformActions && !this.pdf.isCurrentPageExcluded(file)) {
this.instance.UI.enableElements(textActions);
} else {
this.instance.UI.disableElements(textActions);
@ -340,9 +323,9 @@ export class PdfViewerComponent extends AutoUnsubscribe implements OnInit, OnCha
private _toggleRectangleAnnotationAction(readonly = false) {
if (!readonly) {
this.instance.UI.enableElements([dataElements.ADD_RECTANGLE]);
this.instance.UI.enableElements([TextPopups.ADD_RECTANGLE]);
} else {
this.instance.UI.disableElements([dataElements.ADD_RECTANGLE]);
this.instance.UI.disableElements([TextPopups.ADD_RECTANGLE]);
}
}
@ -370,74 +353,121 @@ export class PdfViewerComponent extends AutoUnsubscribe implements OnInit, OnCha
'annotationGroupButton',
]);
const headerItems = [
const applyRotation: IHeaderElement = {
type: 'customElement',
dataElement: HeaderElements.APPLY_ROTATION,
render: () => {
const paragraph = document.createElement('p');
paragraph.innerText = this._translateService.instant('page-rotation.apply');
paragraph.style.cssText = `
font-size: 11px;
font-weight: 600;
color: #DD4D50;
cursor: pointer;
margin: 0 12px;
`;
paragraph.addEventListener('click', async () => {
await this._pageRotationService.applyRotation();
});
return paragraph;
},
};
const discardRotation: IHeaderElement = {
type: 'customElement',
dataElement: HeaderElements.DISCARD_ROTATION,
render: () => {
const paragraph = document.createElement('p');
paragraph.innerText = this._translateService.instant('page-rotation.discard');
paragraph.style.cssText = `
font-size: 11px;
font-weight: 600;
color: #283241;
cursor: pointer;
opacity: 0.7;
`;
paragraph.addEventListener('click', () => {
this._pageRotationService.discardRotation();
});
return paragraph;
},
};
const divider = getDivider();
const headerItems: IHeaderElement[] = [
divider,
{
type: 'divider',
dataElement: dataElements.TOGGLE_TOOLTIPS_DIVIDER,
type: 'actionButton',
element: 'compare',
dataElement: HeaderElements.COMPARE_BUTTON,
img: this._convertPath('/assets/icons/general/pdftron-action-compare.svg'),
title: 'Compare',
onClick: () => {
this.compareFileInput.nativeElement.click();
},
},
{
type: 'actionButton',
element: 'closeCompare',
dataElement: HeaderElements.CLOSE_COMPARE_BUTTON,
img: this._convertPath('/assets/icons/general/pdftron-action-close-compare.svg'),
title: 'Leave Compare Mode',
onClick: async () => {
await this.closeCompareMode();
},
},
divider,
{
type: 'actionButton',
element: 'tooltips',
dataElement: dataElements.TOGGLE_TOOLTIPS,
dataElement: HeaderElements.TOGGLE_TOOLTIPS,
img: this._toggleTooltipsIcon,
title: this._toggleTooltipsBtnTitle,
onClick: async () => {
await this._userPreferenceService.toggleFilePreviewTooltipsPreference();
this._updateTooltipsVisibility();
this.instance.UI.updateElement(dataElements.TOGGLE_TOOLTIPS, {
this.instance.UI.updateElement(HeaderElements.TOGGLE_TOOLTIPS, {
title: this._toggleTooltipsBtnTitle,
img: this._toggleTooltipsIcon,
});
},
},
{
type: 'divider',
dataElement: dataElements.RECTANGLE_TOOL_DIVIDER,
},
divider,
{
type: 'toolGroupButton',
toolGroup: 'rectangleTools',
dataElement: dataElements.SHAPE_TOOL_GROUP_BUTTON,
dataElement: HeaderElements.SHAPE_TOOL_GROUP_BUTTON,
img: this._convertPath('/assets/icons/general/rectangle.svg'),
title: 'annotation.rectangle',
},
divider,
{
type: 'actionButton',
element: 'tooltips',
dataElement: HeaderElements.ROTATE_LEFT_BUTTON,
img: this._convertPath('/assets/icons/general/rotate-left.svg'),
onClick: () => this._pageRotationService.addRotation(RotationTypes.LEFT),
},
{
type: 'actionButton',
element: 'tooltips',
dataElement: HeaderElements.ROTATE_RIGHT_BUTTON,
img: this._convertPath('/assets/icons/general/rotate-right.svg'),
onClick: () => this._pageRotationService.addRotation(RotationTypes.RIGHT),
},
applyRotation,
discardRotation,
divider,
];
this.instance.UI.setHeaderItems(header => {
const originalHeaderItems = header.getItems();
originalHeaderItems.splice(8, 0, ...headerItems);
const compareHeaderItems = [
{
type: 'actionButton',
element: 'compare',
dataElement: dataElements.COMPARE_BUTTON,
img: this._convertPath('/assets/icons/general/pdftron-action-compare.svg'),
title: 'Compare',
onClick: () => {
this.compareFileInput.nativeElement.click();
},
},
{
type: 'actionButton',
element: 'closeCompare',
dataElement: dataElements.CLOSE_COMPARE_BUTTON,
img: this._convertPath('/assets/icons/general/pdftron-action-close-compare.svg'),
title: 'Leave Compare Mode',
onClick: async () => {
await this.closeCompareMode();
},
},
{
type: 'divider',
dataElement: dataElements.COMPARE_TOOL_DIVIDER,
},
];
originalHeaderItems.splice(9, 0, ...compareHeaderItems);
header.getItems().splice(8, 0, ...headerItems);
});
this.instance.UI.disableElements([dataElements.CLOSE_COMPARE_BUTTON]);
this.instance.UI.disableElements([
HeaderElements.CLOSE_COMPARE_BUTTON,
HeaderElements.APPLY_ROTATION,
HeaderElements.DISCARD_ROTATION,
]);
const dossierTemplateId = this.dossier.dossierTemplateId;
@ -503,9 +533,9 @@ export class PdfViewerComponent extends AutoUnsubscribe implements OnInit, OnCha
this.instance.UI.annotationPopup.add([
{
type: 'actionButton',
dataElement: dataElements.ADD_RECTANGLE,
dataElement: TextPopups.ADD_RECTANGLE,
img: this._convertPath('/assets/icons/general/pdftron-action-add-redaction.svg'),
title: this._translateService.instant(this._manualAnnotationService.getTitle('REDACTION', this.dossier)),
title: this.#getTitle(ManualRedactionEntryTypes.REDACTION),
onClick: () => this._addRectangleManualRedaction(),
},
]);
@ -523,8 +553,8 @@ export class PdfViewerComponent extends AutoUnsubscribe implements OnInit, OnCha
}
private _cleanUpSelectionAndButtonState() {
const rectangleElements = [dataElements.SHAPE_TOOL_GROUP_BUTTON, dataElements.RECTANGLE_TOOL_DIVIDER];
this.utils.deselectAllAnnotations();
const rectangleElements = [HeaderElements.SHAPE_TOOL_GROUP_BUTTON];
this.pdf.deselectAllAnnotations();
this.instance.UI.disableElements(rectangleElements);
this.instance.UI.enableElements(rectangleElements);
}
@ -548,48 +578,43 @@ export class PdfViewerComponent extends AutoUnsubscribe implements OnInit, OnCha
setTimeout(() => this.instance.UI.searchTextFull(text, searchOptions), 250);
},
};
this.instance.UI.textPopup.add([searchButton]);
const popups: IHeaderElement[] = [searchButton];
// Adding directly to the false-positive dict is only available in dev-mode
if (this._userPreferenceService.areDevFeaturesEnabled) {
this.instance.UI.textPopup.add([
{
type: 'actionButton',
dataElement: 'add-false-positive',
img: this._convertPath('/assets/icons/general/pdftron-action-false-positive.svg'),
title: this._translateService.instant(
this._manualAnnotationService.getTitle(ManualRedactionEntryTypes.FALSE_POSITIVE, this.dossier),
),
onClick: () => this._addManualRedactionOfType(ManualRedactionEntryTypes.FALSE_POSITIVE),
},
]);
popups.push({
type: 'actionButton',
dataElement: TextPopups.ADD_FALSE_POSITIVE,
img: this._convertPath('/assets/icons/general/pdftron-action-false-positive.svg'),
title: this.#getTitle(ManualRedactionEntryTypes.FALSE_POSITIVE),
onClick: () => this._addManualRedactionOfType(ManualRedactionEntryTypes.FALSE_POSITIVE),
});
}
this.instance.UI.textPopup.add([
{
type: 'actionButton',
dataElement: dataElements.ADD_REDACTION,
img: this._convertPath('/assets/icons/general/pdftron-action-add-redaction.svg'),
title: this._translateService.instant(
this._manualAnnotationService.getTitle(ManualRedactionEntryTypes.REDACTION, this.dossier),
),
onClick: () => this._addManualRedactionOfType(ManualRedactionEntryTypes.REDACTION),
},
{
type: 'actionButton',
dataElement: dataElements.ADD_DICTIONARY,
img: this._convertPath('/assets/icons/general/pdftron-action-add-dict.svg'),
title: this._translateService.instant(
this._manualAnnotationService.getTitle(ManualRedactionEntryTypes.DICTIONARY, this.dossier),
),
onClick: () => this._addManualRedactionOfType(ManualRedactionEntryTypes.DICTIONARY),
},
]);
popups.push({
type: 'actionButton',
dataElement: TextPopups.ADD_REDACTION,
img: this._convertPath('/assets/icons/general/pdftron-action-add-redaction.svg'),
title: this.#getTitle(ManualRedactionEntryTypes.REDACTION),
onClick: () => this._addManualRedactionOfType(ManualRedactionEntryTypes.REDACTION),
});
popups.push({
type: 'actionButton',
dataElement: TextPopups.ADD_DICTIONARY,
img: this._convertPath('/assets/icons/general/pdftron-action-add-dict.svg'),
title: this.#getTitle(ManualRedactionEntryTypes.DICTIONARY),
onClick: () => this._addManualRedactionOfType(ManualRedactionEntryTypes.DICTIONARY),
});
this.instance.UI.textPopup.add(popups);
return this._handleCustomActions();
}
#getTitle(type: ManualRedactionEntryType) {
return this._translateService.instant(this._manualAnnotationService.getTitle(type, this.dossier));
}
private _addManualRedactionOfType(type: ManualRedactionEntryType) {
const selectedQuads: Readonly<Record<string, Core.Math.Quad[]>> = this.documentViewer.getSelectedTextQuads();
const text = this.documentViewer.getSelectedText();
@ -599,17 +624,17 @@ export class PdfViewerComponent extends AutoUnsubscribe implements OnInit, OnCha
private async _handleCustomActions() {
this.instance.UI.setToolMode('AnnotationEdit');
const { ANNOTATION_POPUP, ADD_RECTANGLE, ADD_REDACTION, SHAPE_TOOL_GROUP_BUTTON } = dataElements;
const elements = [
ADD_REDACTION,
ADD_RECTANGLE,
'add-false-positive',
SHAPE_TOOL_GROUP_BUTTON,
'rectangleToolDivider',
ANNOTATION_POPUP,
const elementsToToggle = [
TextPopups.ADD_REDACTION,
TextPopups.ADD_RECTANGLE,
TextPopups.ADD_FALSE_POSITIVE,
HeaderElements.SHAPE_TOOL_GROUP_BUTTON,
HeaderElements.ANNOTATION_POPUP,
HeaderElements.ROTATE_LEFT_BUTTON,
HeaderElements.ROTATE_RIGHT_BUTTON,
];
const isCurrentPageExcluded = this.utils.isCurrentPageExcluded(await this.stateService.file);
const isCurrentPageExcluded = this.pdf.isCurrentPageExcluded(await this.stateService.file);
if (this.canPerformActions && !isCurrentPageExcluded) {
try {
@ -617,19 +642,24 @@ export class PdfViewerComponent extends AutoUnsubscribe implements OnInit, OnCha
} catch (e) {
// happens
}
this.instance.UI.enableElements(elements);
this.instance.UI.enableElements(elementsToToggle);
if (this._selectedText.length > 2) {
this.instance.UI.enableElements([dataElements.ADD_DICTIONARY, dataElements.ADD_FALSE_POSITIVE]);
this.instance.UI.enableElements([TextPopups.ADD_DICTIONARY, TextPopups.ADD_FALSE_POSITIVE]);
}
return;
}
let elementsToDisable = [...elements, ADD_RECTANGLE];
let elementsToDisable = [...elementsToToggle, TextPopups.ADD_RECTANGLE];
if (isCurrentPageExcluded) {
const allowedActionsWhenPageExcluded: string[] = [ANNOTATION_POPUP, ADD_RECTANGLE, ADD_REDACTION, SHAPE_TOOL_GROUP_BUTTON];
const allowedActionsWhenPageExcluded: string[] = [
HeaderElements.ANNOTATION_POPUP,
TextPopups.ADD_RECTANGLE,
TextPopups.ADD_REDACTION,
HeaderElements.SHAPE_TOOL_GROUP_BUTTON,
];
elementsToDisable = elementsToDisable.filter(element => !allowedActionsWhenPageExcluded.includes(element));
} else {
this.instance.UI.disableTools(['AnnotationCreateRectangle']);
@ -649,7 +679,7 @@ export class PdfViewerComponent extends AutoUnsubscribe implements OnInit, OnCha
for (const quad of quads[key]) {
const page = parseInt(key, 10);
const pageHeight = this.documentViewer.getPageHeight(page);
entry.positions.push(toPosition(page, pageHeight, convertQuads ? this.utils.translateQuad(page, quad) : quad));
entry.positions.push(toPosition(page, pageHeight, convertQuads ? this.pdf.translateQuad(page, quad) : quad));
}
}
@ -664,7 +694,7 @@ export class PdfViewerComponent extends AutoUnsubscribe implements OnInit, OnCha
private _setReadyAndInitialState(): void {
this._ngZone.run(() => {
this.utils.ready = true;
this.pdf.ready = true;
this.viewerReady.emit(this.instance);
const routePageNumber: number = this._activatedRoute.snapshot.queryParams.page;
this.pageChanged.emit(routePageNumber || 1);

View File

@ -13,6 +13,8 @@ import { FilterService } from '@iqser/common-ui';
import { ManualAnnotationService } from '../../services/manual-annotation.service';
import { AnnotationProcessingService } from '../../services/annotation-processing.service';
import { dossiersServiceProvider } from '@services/entity-services/dossiers.service.provider';
import { PageRotationService } from './services/page-rotation.service';
import { PdfViewer } from './services/pdf-viewer.service';
export const filePreviewScreenProviders = [
FilterService,
@ -27,6 +29,8 @@ export const filePreviewScreenProviders = [
FilePreviewStateService,
PdfViewerDataService,
AnnotationReferencesService,
PageRotationService,
PdfViewer,
ManualAnnotationService,
AnnotationProcessingService,
dossiersServiceProvider,

View File

@ -50,6 +50,7 @@ import { FileDataModel } from '../../../../models/file/file-data.model';
import { filePreviewScreenProviders } from './file-preview-providers';
import { ManualAnnotationService } from '../../services/manual-annotation.service';
import { DossiersService } from '../../../../services/dossiers/dossiers.service';
import { PageRotationService } from './services/page-rotation.service';
import Annotation = Core.Annotations.Annotation;
import PDFNet = Core.PDFNet;
@ -106,6 +107,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
private readonly _dossiersService: DossiersService,
private readonly _reanalysisService: ReanalysisService,
private readonly _errorService: ErrorService,
private readonly _pageRotationService: PageRotationService,
private readonly _skippedService: SkippedService,
private readonly _manualAnnotationService: ManualAnnotationService,
readonly excludedPagesService: ExcludedPagesService,
@ -215,6 +217,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
}
ngOnDetach(): void {
this._pageRotationService.clearRotations();
this.displayPdfViewer = false;
super.ngOnDetach();
this._changeDetectorRef.markForCheck();
@ -297,18 +300,18 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
selectAnnotations(annotations?: AnnotationWrapper[]) {
if (annotations) {
const annotationsToSelect = this.multiSelectService.isActive ? [...this.selectedAnnotations, ...annotations] : annotations;
this.viewerComponent?.utils?.selectAnnotations(annotationsToSelect, this.multiSelectService.isActive);
this.viewerComponent?.pdf?.selectAnnotations(annotationsToSelect, this.multiSelectService.isActive);
} else {
this.viewerComponent?.utils?.deselectAllAnnotations();
this.viewerComponent?.pdf?.deselectAllAnnotations();
}
}
deselectAnnotations(annotations: AnnotationWrapper[]) {
this.viewerComponent.utils.deselectAnnotations(annotations);
this.viewerComponent.pdf.deselectAnnotations(annotations);
}
selectPage(pageNumber: number) {
this.viewerComponent.utils.navigateToPage(pageNumber);
this.viewerComponent.pdf.navigateToPage(pageNumber);
this._workloadComponent?.scrollAnnotationsToPage(pageNumber, 'always');
this._lastPage = pageNumber.toString();
}
@ -387,11 +390,12 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
}
}
viewerPageChanged($event: any) {
async viewerPageChanged($event: any) {
if (typeof $event !== 'number') {
return;
}
await firstValueFrom(this._pageRotationService.showConfirmationDialogIfHasRotations());
this._scrollViews();
this.multiSelectService.deactivate();
@ -401,7 +405,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
queryParamsHandling: 'merge',
replaceUrl: true,
};
this._router.navigate([], extras).then();
await this._router.navigate([], extras);
this._setActiveViewerPage();
this._changeDetectorRef.markForCheck();
@ -468,7 +472,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
#deactivateMultiSelect(): void {
this.multiSelectService.deactivate();
this.viewerComponent?.utils?.deselectAllAnnotations();
this.viewerComponent?.pdf?.deselectAllAnnotations();
this.handleAnnotationSelected([]);
}

View File

@ -98,7 +98,9 @@ export class AnnotationDrawService {
dossierId: string,
compareMode: boolean,
) {
const annotations = annotationWrappers.map(annotation => this._computeAnnotation(activeViewer, annotation, dossierId, compareMode));
const annotations = annotationWrappers
.map(annotation => this._computeAnnotation(activeViewer, annotation, dossierId, compareMode))
.filter(a => !!a);
const annotationManager = activeViewer.Core.annotationManager;
annotationManager.addAnnotations(annotations, { imported: true });
await annotationManager.drawAnnotationsFromList(annotations);
@ -156,6 +158,10 @@ export class AnnotationDrawService {
compareMode: boolean,
) {
const pageNumber = compareMode ? annotationWrapper.pageNumber * 2 - 1 : annotationWrapper.pageNumber;
if (pageNumber > activeViewer.Core.documentViewer.getPageCount()) {
// skip imported annotations from files that have more pages than the current one
return;
}
if (annotationWrapper.superType === 'text-highlight') {
const rectangleAnnot = new activeViewer.Core.Annotations.RectangleAnnotation();
@ -172,68 +178,68 @@ export class AnnotationDrawService {
rectangleAnnot.Id = annotationWrapper.id;
return rectangleAnnot;
} else {
const dossierTemplateId = this._dossiersService.find(dossierId).dossierTemplateId;
}
let annotation: Core.Annotations.RectangleAnnotation | Core.Annotations.TextHighlightAnnotation;
if (annotationWrapper.rectangle || annotationWrapper.isImage) {
annotation = new activeViewer.Core.Annotations.RectangleAnnotation();
const pageHeight = activeViewer.Core.documentViewer.getPageHeight(pageNumber);
const firstPosition = annotationWrapper.positions[0];
annotation.X = firstPosition.topLeft.x;
annotation.Y = pageHeight - (firstPosition.topLeft.y + firstPosition.height);
annotation.Width = firstPosition.width;
annotation.FillColor = this.getAndConvertColor(
activeViewer,
dossierTemplateId,
annotationWrapper.superType,
annotationWrapper.type,
);
annotation.Opacity = annotationWrapper.isChangeLogRemoved
? AnnotationDrawService.DEFAULT_REMOVED_ANNOTATION_OPACITY
: AnnotationDrawService.DEFAULT_RECTANGLE_ANNOTATION_OPACITY;
annotation.Height = firstPosition.height;
annotation.Intensity = 100;
} else {
annotation = new activeViewer.Core.Annotations.TextHighlightAnnotation();
annotation.Quads = this._rectanglesToQuads(annotationWrapper.positions, activeViewer, pageNumber);
annotation.Opacity = annotationWrapper.isChangeLogRemoved
? AnnotationDrawService.DEFAULT_REMOVED_ANNOTATION_OPACITY
: AnnotationDrawService.DEFAULT_TEXT_ANNOTATION_OPACITY;
}
const dossierTemplateId = this._dossiersService.find(dossierId).dossierTemplateId;
annotation.setContents(annotationWrapper.content);
annotation.PageNumber = pageNumber;
annotation.StrokeColor = this.getAndConvertColor(
let annotation: Core.Annotations.RectangleAnnotation | Core.Annotations.TextHighlightAnnotation;
if (annotationWrapper.rectangle || annotationWrapper.isImage) {
annotation = new activeViewer.Core.Annotations.RectangleAnnotation();
const pageHeight = activeViewer.Core.documentViewer.getPageHeight(pageNumber);
const firstPosition = annotationWrapper.positions[0];
annotation.X = firstPosition.topLeft.x;
annotation.Y = pageHeight - (firstPosition.topLeft.y + firstPosition.height);
annotation.Width = firstPosition.width;
annotation.FillColor = this.getAndConvertColor(
activeViewer,
dossierTemplateId,
annotationWrapper.superType,
annotationWrapper.type,
);
annotation.Id = annotationWrapper.id;
annotation.ReadOnly = true;
// change log entries are drawn lighter
annotation.Hidden =
annotationWrapper.isChangeLogRemoved ||
(this._skippedService.hideSkipped && annotationWrapper.isSkipped) ||
annotationWrapper.isOCR ||
annotationWrapper.hidden;
annotation.setCustomData('redact-manager', 'true');
annotation.setCustomData('redaction', String(annotationWrapper.previewAnnotation));
annotation.setCustomData('skipped', String(annotationWrapper.isSkipped));
annotation.setCustomData('changeLog', String(annotationWrapper.isChangeLogEntry));
annotation.setCustomData('changeLogRemoved', String(annotationWrapper.isChangeLogRemoved));
annotation.setCustomData('opacity', String(annotation.Opacity));
annotation.setCustomData('redactionColor', String(this.getColor(activeViewer, dossierTemplateId, 'redaction', 'redaction')));
annotation.setCustomData(
'annotationColor',
String(this.getColor(activeViewer, dossierTemplateId, annotationWrapper.superType, annotationWrapper.type)),
);
return annotation;
annotation.Opacity = annotationWrapper.isChangeLogRemoved
? AnnotationDrawService.DEFAULT_REMOVED_ANNOTATION_OPACITY
: AnnotationDrawService.DEFAULT_RECTANGLE_ANNOTATION_OPACITY;
annotation.Height = firstPosition.height;
annotation.Intensity = 100;
} else {
annotation = new activeViewer.Core.Annotations.TextHighlightAnnotation();
annotation.Quads = this._rectanglesToQuads(annotationWrapper.positions, activeViewer, pageNumber);
annotation.Opacity = annotationWrapper.isChangeLogRemoved
? AnnotationDrawService.DEFAULT_REMOVED_ANNOTATION_OPACITY
: AnnotationDrawService.DEFAULT_TEXT_ANNOTATION_OPACITY;
}
annotation.setContents(annotationWrapper.content);
annotation.PageNumber = pageNumber;
annotation.StrokeColor = this.getAndConvertColor(
activeViewer,
dossierTemplateId,
annotationWrapper.superType,
annotationWrapper.type,
);
annotation.Id = annotationWrapper.id;
annotation.ReadOnly = true;
// change log entries are drawn lighter
annotation.Hidden =
annotationWrapper.isChangeLogRemoved ||
(this._skippedService.hideSkipped && annotationWrapper.isSkipped) ||
annotationWrapper.isOCR ||
annotationWrapper.hidden;
annotation.setCustomData('redact-manager', 'true');
annotation.setCustomData('redaction', String(annotationWrapper.previewAnnotation));
annotation.setCustomData('skipped', String(annotationWrapper.isSkipped));
annotation.setCustomData('changeLog', String(annotationWrapper.isChangeLogEntry));
annotation.setCustomData('changeLogRemoved', String(annotationWrapper.isChangeLogRemoved));
annotation.setCustomData('opacity', String(annotation.Opacity));
annotation.setCustomData('redactionColor', String(this.getColor(activeViewer, dossierTemplateId, 'redaction', 'redaction')));
annotation.setCustomData(
'annotationColor',
String(this.getColor(activeViewer, dossierTemplateId, annotationWrapper.superType, annotationWrapper.type)),
);
return annotation;
}
private _rectanglesToQuads(positions: IRectangle[], activeViewer: WebViewerInstance, pageNumber: number): any[] {

View File

@ -1,16 +1,17 @@
import { Injectable, Injector } from '@angular/core';
import { BehaviorSubject, firstValueFrom, Observable, pairwise, switchMap } from 'rxjs';
import { BehaviorSubject, firstValueFrom, from, Observable, pairwise, switchMap } from 'rxjs';
import { FileDataModel } from '@models/file/file-data.model';
import { Dossier, File } from '@red/domain';
import { ActivatedRoute } from '@angular/router';
import { FilesMapService } from '@services/entity-services/files-map.service';
import { PermissionsService } from '@services/permissions.service';
import { boolFactory, shareLast } from '@iqser/common-ui';
import { boolFactory } from '@iqser/common-ui';
import { filter, startWith } from 'rxjs/operators';
import { FileManagementService } from '@services/entity-services/file-management.service';
import { DOSSIER_ID, FILE_ID } from '@utils/constants';
import { DossiersService } from '../../../../../services/dossiers/dossiers.service';
import { dossiersServiceResolver } from '@services/entity-services/dossiers.service.provider';
import { wipeFilesCache } from '../../../../../../../../../libs/red-cache/src';
@Injectable()
export class FilePreviewStateService {
@ -76,11 +77,11 @@ export class FilePreviewStateService {
pairwise(),
filter(([oldFile, newFile]) => oldFile?.cacheIdentifier !== newFile.cacheIdentifier),
switchMap(([, newFile]) => this.#downloadOriginalFile(newFile.cacheIdentifier)),
shareLast(),
);
}
#downloadOriginalFile(cacheIdentifier: string): Observable<Blob> {
return this._fileManagementService.downloadOriginalFile(this.dossierId, this.fileId, 'body', cacheIdentifier);
#downloadOriginalFile(cacheIdentifier?: string): Observable<Blob> {
const downloadFile = this._fileManagementService.downloadOriginalFile(this.dossierId, this.fileId, 'body', cacheIdentifier);
return from(wipeFilesCache()).pipe(switchMap(() => downloadFile));
}
}

View File

@ -0,0 +1,126 @@
import { Injectable } from '@angular/core';
import { BehaviorSubject, firstValueFrom, of } from 'rxjs';
import { PermissionsService } from '@services/permissions.service';
import { RotationType, RotationTypes } from '@red/domain';
import { FileManagementService } from '@services/entity-services/file-management.service';
import { FilePreviewStateService } from './file-preview-state.service';
import { distinctUntilChanged, map, tap } from 'rxjs/operators';
import { PdfViewer } from './pdf-viewer.service';
import { HeaderElements } from '../shared/constants';
import {
ConfirmationDialogComponent,
ConfirmationDialogInput,
ConfirmOptions,
defaultDialogConfig,
LoadingService,
} from '@iqser/common-ui';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { MatDialog } from '@angular/material/dialog';
const actionButtons = [HeaderElements.APPLY_ROTATION, HeaderElements.DISCARD_ROTATION];
const oneRotationDegree = 90;
@Injectable()
export class PageRotationService {
readonly #rotations$ = new BehaviorSubject<Record<number, number>>({});
constructor(
private readonly _pdf: PdfViewer,
private readonly _dialog: MatDialog,
private readonly _loadingService: LoadingService,
private readonly _screenState: FilePreviewStateService,
private readonly _permissionsService: PermissionsService,
private readonly _fileManagementService: FileManagementService,
) {}
isRotated(page: number) {
return this.#rotations$.pipe(
map(rotations => !!rotations[page]),
distinctUntilChanged(),
);
}
hasRotations() {
return Object.values(this.#rotations$.value).filter(v => !!v).length > 0;
}
applyRotation() {
this._loadingService.start();
const pages = this.#rotations$.value;
const { dossierId, fileId } = this._screenState;
const request = this._fileManagementService.rotatePage({ pages }, dossierId, fileId);
this.clearRotationsHideActions();
return firstValueFrom(request.pipe(tap(() => this._loadingService.stop())));
}
discardRotation() {
const rotations = this.#rotations$.value;
for (const page of Object.keys(rotations)) {
const times = rotations[page] / oneRotationDegree;
for (let i = 1; i <= times; i++) {
this._pdf?.documentViewer?.rotateCounterClockwise(Number(page));
}
}
this.clearRotationsHideActions();
}
addRotation(rotation: RotationType): void {
const pageNumber = this._pdf.currentPage;
const pageRotation = this.#rotations$.value[pageNumber];
const rotationValue = pageRotation ? (pageRotation + Number(rotation)) % 360 : rotation;
this.#rotations$.next({ ...this.#rotations$.value, [pageNumber]: rotationValue });
if (rotation === RotationTypes.LEFT) {
this._pdf.documentViewer.rotateCounterClockwise(pageNumber);
} else {
this._pdf.documentViewer.rotateClockwise(pageNumber);
}
if (this.hasRotations()) {
this.#showActionButtons();
} else {
this.#hideActionButtons();
}
}
clearRotations() {
this.#rotations$.next({});
}
clearRotationsHideActions() {
this.clearRotations();
this.#hideActionButtons();
}
showConfirmationDialog() {
const ref = this._dialog.open(ConfirmationDialogComponent, {
...defaultDialogConfig,
data: new ConfirmationDialogInput({
title: _('page-rotation.confirmation-dialog.title'),
question: _('page-rotation.confirmation-dialog.question'),
confirmationText: _('page-rotation.apply'),
discardChangesText: _('page-rotation.discard'),
}),
});
return ref
.afterClosed()
.pipe(tap((option: ConfirmOptions) => (option === ConfirmOptions.CONFIRM ? this.applyRotation() : this.discardRotation())));
}
showConfirmationDialogIfHasRotations() {
return this.hasRotations() ? this.showConfirmationDialog() : of(ConfirmOptions.DISCARD_CHANGES);
}
#showActionButtons() {
this._pdf.UI.enableElements(actionButtons);
}
#hideActionButtons() {
this._pdf.UI.disableElements(actionButtons);
}
}

View File

@ -1,9 +1,14 @@
import { translateQuads } from '@utils/pdf-coordinates';
import { AnnotationWrapper } from '@models/file/annotation.wrapper';
import { Core, WebViewerInstance } from '@pdftron/webviewer';
import { ViewModeService } from '../screens/file-preview-screen/services/view-mode.service';
import { translateQuads } from '../../../../../utils/pdf-coordinates';
import { AnnotationWrapper } from '../../../../../models/file/annotation.wrapper';
import WebViewer, { Core, WebViewerInstance } from '@pdftron/webviewer';
import { ViewModeService } from './view-mode.service';
import { File } from '@red/domain';
import { Inject, Injectable } from '@angular/core';
import { BASE_HREF } from '../../../../../tokens';
import { environment } from '../../../../../../environments/environment';
import Annotation = Core.Annotations.Annotation;
import DocumentViewer = Core.DocumentViewer;
import AnnotationManager = Core.AnnotationManager;
const DISABLED_HOTKEYS = [
'CTRL+SHIFT+EQUAL',
@ -38,10 +43,18 @@ const DISABLED_HOTKEYS = [
'U',
] as const;
export class PdfViewerUtils {
@Injectable()
export class PdfViewer {
ready = false;
instance: WebViewerInstance;
documentViewer: DocumentViewer;
annotationManager: AnnotationManager;
constructor(readonly instance: WebViewerInstance, readonly viewModeService: ViewModeService) {}
constructor(@Inject(BASE_HREF) private readonly _baseHref: string, readonly viewModeService: ViewModeService) {}
get UI() {
return this.instance.UI;
}
get paginationOffset() {
return this.viewModeService.isCompare ? 2 : 1;
@ -83,6 +96,24 @@ export class PdfViewerUtils {
return this.instance?.Core.documentViewer?.getPageCount();
}
async loadViewer(htmlElement: HTMLElement) {
this.instance = await WebViewer(
{
licenseKey: environment.licenseKey ? atob(environment.licenseKey) : null,
fullAPI: true,
path: this.#convertPath('/assets/wv-resources'),
css: this.#convertPath('/assets/pdftron/stylesheet.css'),
backendType: 'ems',
},
htmlElement,
);
this.documentViewer = this.instance.Core.documentViewer;
this.annotationManager = this.instance.Core.annotationManager;
return this.instance;
}
isCurrentPageExcluded(file: File) {
const currentPage = this.currentPage;
return !!file?.excludedPages?.includes(currentPage);
@ -147,4 +178,8 @@ export class PdfViewerUtils {
private _getAnnotationById(id: string): Annotation {
return this._annotationManager.getAnnotationById(id);
}
#convertPath(path: string) {
return `${this._baseHref}${path}`;
}
}

View File

@ -0,0 +1,19 @@
export const ALLOWED_KEYBOARD_SHORTCUTS: readonly string[] = ['+', '-', 'p', 'r', 'Escape'] as const;
export const HeaderElements = {
SHAPE_TOOL_GROUP_BUTTON: 'shapeToolGroupButton',
ROTATE_LEFT_BUTTON: 'rotateLeftButton',
ROTATE_RIGHT_BUTTON: 'rotateRightButton',
APPLY_ROTATION: 'applyRotation',
DISCARD_ROTATION: 'discardRotation',
TOGGLE_TOOLTIPS: 'toggle-tooltips',
ANNOTATION_POPUP: 'annotationPopup',
COMPARE_BUTTON: 'compareButton',
CLOSE_COMPARE_BUTTON: 'closeCompareButton',
} as const;
export const TextPopups = {
ADD_REDACTION: 'add-redaction',
ADD_DICTIONARY: 'add-dictionary',
ADD_RECTANGLE: 'add-rectangle',
ADD_FALSE_POSITIVE: 'add-false-positive',
} as const;

View File

@ -20,6 +20,7 @@ import { HttpErrorResponse, HttpStatusCode } from '@angular/common/http';
import { ActiveDossiersService } from '@services/dossiers/active-dossiers.service';
import { Observable } from 'rxjs';
import { DictionariesMapService } from '@services/entity-services/dictionaries-map.service';
import { ManualRedactionEntryType } from '@models/file/manual-redaction-entry.wrapper';
@Injectable()
export class ManualAnnotationService extends GenericService<IManualAddResponse> {
@ -233,7 +234,7 @@ export class ManualAnnotationService extends GenericService<IManualAddResponse>
// this wraps
// /manualRedaction/redaction/remove/
getTitle(type: 'DICTIONARY' | 'REDACTION' | 'FALSE_POSITIVE', dossier: Dossier) {
getTitle(type: ManualRedactionEntryType, dossier: Dossier) {
if (this._permissionsService.isApprover(dossier)) {
switch (type) {
case 'DICTIONARY':
@ -243,15 +244,15 @@ export class ManualAnnotationService extends GenericService<IManualAddResponse>
case 'REDACTION':
return _('manual-annotation.dialog.header.redaction');
}
} else {
switch (type) {
case 'DICTIONARY':
return _('manual-annotation.dialog.header.request-dictionary');
case 'FALSE_POSITIVE':
return _('manual-annotation.dialog.header.request-false-positive');
case 'REDACTION':
return _('manual-annotation.dialog.header.request-redaction');
}
}
switch (type) {
case 'DICTIONARY':
return _('manual-annotation.dialog.header.request-dictionary');
case 'FALSE_POSITIVE':
return _('manual-annotation.dialog.header.request-false-positive');
case 'REDACTION':
return _('manual-annotation.dialog.header.request-redaction');
}
}

View File

@ -1,3 +1,3 @@
<div [class.error]="isError" class="small-label">
{{ date | date: 'd MMM. yyyy, hh:mm a' }}
{{ date | date: 'd MMM. yyyy' }}
</div>

View File

@ -1,12 +1,12 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
@Component({
selector: 'redaction-date-column',
selector: 'redaction-date-column [date]',
templateUrl: './date-column.component.html',
styleUrls: ['./date-column.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class DateColumnComponent {
@Input() isError: boolean;
@Input() isError = false;
@Input() date: string;
}

View File

@ -39,6 +39,7 @@ import { DocumentInfoService } from '../../../screens/file-preview-screen/servic
import { ExpandableFileActionsComponent } from '@shared/components/expandable-file-actions/expandable-file-actions.component';
import { firstValueFrom } from 'rxjs';
import { RedactionImportService } from '../../services/redaction-import.service';
import { PageRotationService } from '../../../screens/file-preview-screen/services/page-rotation.service';
@Component({
selector: 'redaction-file-actions [file] [type]',
@ -93,6 +94,7 @@ export class FileActionsComponent extends AutoUnsubscribe implements OnDestroy,
constructor(
@Optional() private readonly _excludedPagesService: ExcludedPagesService,
@Optional() private readonly _documentInfoService: DocumentInfoService,
@Optional() private readonly _pageRotationService: PageRotationService,
private readonly _permissionsService: PermissionsService,
private readonly _activeDossiersService: ActiveDossiersService,
private readonly _dialogService: DossiersDialogService,
@ -201,9 +203,9 @@ export class FileActionsComponent extends AutoUnsubscribe implements OnDestroy,
},
{
type: ActionTypes.circleBtn,
action: ($event: MouseEvent) => this.toggleAutomaticAnalysis($event),
tooltip: _('dossier-overview.disable-auto-analysis'),
icon: 'red:stop',
action: ($event: MouseEvent) => this._toggleAutomaticAnalysis($event),
tooltip: _('dossier-overview.stop-auto-analysis'),
icon: 'red:disable-analysis',
show: this.canDisableAutoAnalysis,
},
{
@ -216,10 +218,10 @@ export class FileActionsComponent extends AutoUnsubscribe implements OnDestroy,
},
{
type: ActionTypes.circleBtn,
action: ($event: MouseEvent) => this.toggleAutomaticAnalysis($event),
tooltip: _('dossier-overview.enable-auto-analysis'),
action: ($event: MouseEvent) => this._toggleAutomaticAnalysis($event),
tooltip: _('dossier-overview.start-auto-analysis'),
buttonType: this.isFilePreview ? CircleButtonTypes.warn : CircleButtonTypes.default,
icon: 'red:play',
icon: 'red:enable-analysis',
show: this.canEnableAutoAnalysis,
},
{
@ -364,7 +366,7 @@ export class FileActionsComponent extends AutoUnsubscribe implements OnDestroy,
await firstValueFrom(this._reanalysisService.reanalyzeFilesForDossier([this.file], this.file.dossierId, params));
}
private async toggleAutomaticAnalysis($event: MouseEvent) {
private async _toggleAutomaticAnalysis($event: MouseEvent) {
$event.stopPropagation();
this._loadingService.start();
await firstValueFrom(this._reanalysisService.toggleAutomaticAnalysis(this.file.dossierId, [this.file]));
@ -378,6 +380,9 @@ export class FileActionsComponent extends AutoUnsubscribe implements OnDestroy,
private async _ocrFile($event: MouseEvent) {
$event.stopPropagation();
if (this._pageRotationService) {
await firstValueFrom(this._pageRotationService.showConfirmationDialogIfHasRotations());
}
this._loadingService.start();
await firstValueFrom(this._reanalysisService.ocrFiles([this.file], this.file.dossierId));
this._loadingService.stop();
@ -429,8 +434,8 @@ export class FileActionsComponent extends AutoUnsubscribe implements OnDestroy,
const showReanalyse = this.canReanalyse || this.file.excludedFromAutomaticAnalysis || this.analysisForced;
this.showReanalyseFilePreview = showReanalyse && this.isFilePreview;
this.showReanalyseDossierOverview = showReanalyse && this.isDossierOverview;
this.showReanalyseFilePreview = showReanalyse && this.isFilePreview && !this.file.isApproved;
this.showReanalyseDossierOverview = showReanalyse && this.isDossierOverview && !this.file.isApproved;
this.buttons = this._buttons;

View File

@ -15,6 +15,7 @@ import { EditDossierDictionaryComponent } from '../dialogs/edit-dossier-dialog/d
import { EditDossierAttributesComponent } from '../dialogs/edit-dossier-dialog/attributes/edit-dossier-attributes.component';
import { EditDossierTeamComponent } from '../dialogs/edit-dossier-dialog/edit-dossier-team/edit-dossier-team.component';
import { EditDossierDeletedDocumentsComponent } from '../dialogs/edit-dossier-dialog/deleted-documents/edit-dossier-deleted-documents.component';
import { DateColumnComponent } from './components/date-column/date-column.component';
const components = [
FileActionsComponent,
@ -25,6 +26,8 @@ const components = [
EditDossierAttributesComponent,
EditDossierTeamComponent,
EditDossierDeletedDocumentsComponent,
FileActionsComponent,
DateColumnComponent,
];
const dialogs = [EditDossierDialogComponent, AddDossierDialogComponent, AssignReviewerApproverDialogComponent];
const services = [DossiersDialogService, FileAssignService, RedactionImportService];

View File

@ -27,7 +27,9 @@ export class IconsModule {
'csv',
'dictionary',
'denied',
'disable-analysis',
'double-chevron-right',
'enable-analysis',
'enter',
'entries',
'exclude-pages',
@ -48,7 +50,6 @@ export class IconsModule {
'new-tab',
'notification',
'page',
'play',
'preview',
'put-back',
'read-only',
@ -60,11 +61,11 @@ export class IconsModule {
'remove-from-dict',
'report',
'resize',
'rotation',
'rule',
'secret',
'status',
'status-info',
'stop',
'template',
'thumb-down',
'thumb-up',

View File

@ -54,7 +54,7 @@ export class ExpandableFileActionsComponent implements OnChanges {
// Patch download button
const downloadBtn = this.actions.find(btn => btn.type === ActionTypes.downloadBtn);
if (downloadBtn) {
downloadBtn.action = $event => this._downloadFiles($event, downloadBtn.files);
downloadBtn.action = ($event: MouseEvent) => this._downloadFiles($event, downloadBtn.files);
downloadBtn.disabled = !this._permissionsService.canDownloadFiles(downloadBtn.files);
}
}

View File

@ -5,6 +5,7 @@ import { Observable } from 'rxjs';
import { switchMap } from 'rxjs/operators';
import { FilesService } from '@services/entity-services/files.service';
import { DossierStatsService } from '@services/dossiers/dossier-stats.service';
import { IPageRotationRequest } from '@red/domain';
@Injectable({
providedIn: 'root',
@ -40,6 +41,11 @@ export class FileManagementService extends GenericService<unknown> {
return this._post(fileIds, `delete/restore/${dossierId}`).pipe(switchMap(() => this._filesService.loadAll(dossierId, routerPath)));
}
@Validate()
rotatePage(@RequiredParam() body: IPageRotationRequest, @RequiredParam() dossierId: string, @RequiredParam() fileId: string) {
return this._post(body, `rotate/${dossierId}/${fileId}`);
}
downloadOriginalFile(dossierId: string, fileId: string, observe?: 'body', indicator?: string): Observable<Blob>;
downloadOriginalFile(dossierId: string, fileId: string, observe?: 'response', indicator?: string): Observable<HttpResponse<Blob>>;
@Validate()

View File

@ -141,7 +141,13 @@ export class PermissionsService {
// TODO: Remove '?', after we make sure file is loaded before page
canPerformAnnotationActions(file: File): boolean {
return this._isActive(file) && !file.excluded && (file?.isUnderReview || file?.isUnderApproval) && this.isFileAssignee(file);
return (
this._isActive(file) &&
!file.isOcrProcessing &&
!file.excluded &&
(file?.isUnderReview || file?.isUnderApproval) &&
this.isFileAssignee(file)
);
}
canUndoApproval(file: File | File[]): boolean {
@ -235,11 +241,21 @@ export class PermissionsService {
}
private _canEnableAutoAnalysis(file: File): boolean {
return this._isActive(file) && file.excludedFromAutomaticAnalysis && file.assignee === this._userService.currentUser.id;
return (
this._isActive(file) &&
file.excludedFromAutomaticAnalysis &&
file.assignee === this._userService.currentUser.id &&
!file.isApproved
);
}
private _canDisableAutoAnalysis(file: File): boolean {
return this._isActive(file) && !file.excludedFromAutomaticAnalysis && file.assignee === this._userService.currentUser.id;
return (
this._isActive(file) &&
!file.excludedFromAutomaticAnalysis &&
file.assignee === this._userService.currentUser.id &&
!file.isApproved
);
}
private _canAssignToSelf(file: File, dossier: Dossier): boolean {

View File

@ -125,6 +125,9 @@ export class UserService extends EntitiesService<User, IUser> {
if (id?.toLowerCase() === 'system') {
return new User({ username: 'System' }, [], 'system');
}
if (!id) {
return undefined;
}
return super.find(id) || new User({ username: 'Deleted User' }, [], 'deleted');
}
}

View File

@ -1,7 +1,7 @@
{
"ADMIN_CONTACT_NAME": null,
"ADMIN_CONTACT_URL": null,
"API_URL": "https://dev-05.iqser.cloud/redaction-gateway-v1",
"API_URL": "https://dev-04.iqser.cloud/redaction-gateway-v1",
"APP_NAME": "RedactManager",
"AUTO_READ_TIME": 3,
"BACKEND_APP_VERSION": "4.4.40",
@ -17,7 +17,7 @@
"MAX_RETRIES_ON_SERVER_ERROR": 3,
"OAUTH_CLIENT_ID": "redaction",
"OAUTH_IDP_HINT": null,
"OAUTH_URL": "https://dev-05.iqser.cloud/auth/realms/redaction",
"OAUTH_URL": "https://dev-04.iqser.cloud/auth/realms/redaction",
"RECENT_PERIOD_IN_HOURS": 24,
"SELECTION_MODE": "structural",
"MANUAL_BASE_URL": "https://docs.redactmanager.com/preview"

View File

@ -119,8 +119,26 @@
"it": "",
"fr": ""
},
"remove_from_dictionary": {
"en": "/en/index-en.html?contextId=remove_from_dictionary",
"recommendation_false_positive": {
"en": "/en/index-en.html?contextId=recommendation_false_positive",
"de": "",
"it": "",
"fr": ""
},
"skipped_remove_from_dictionary": {
"en": "/en/index-en.html?contextId=skipped_remove_from_dictionary",
"de": "",
"it": "",
"fr": ""
},
"hint_remove_from_dictionary": {
"en": "/en/index-en.html?contextId=hints_remove_from_dictionary",
"de": "",
"it": "",
"fr": ""
},
"recommendation_remove_from_dictionary": {
"en": "/en/index-en.html?contextId=recommendation_remove_from_dictionary",
"de": "",
"it": "",
"fr": ""
@ -131,6 +149,12 @@
"it": "",
"fr": ""
},
"hint_remove_only_here": {
"en": "/en/index-en.html?contextId=hint_remove_only_here",
"de": "",
"it": "",
"fr": ""
},
"reset_filters": {
"en": "/en/index-en.html?contextId=reset_filters",
"de": "",
@ -179,12 +203,18 @@
"it": "",
"fr": ""
},
"redaction_resize_redaction": {
"redaction_resize": {
"en": "/en/index-en.html?contextId=redaction_resize_redaction",
"de": "",
"it": "",
"fr": ""
},
"recommendation_resize": {
"en": "/en/index-en.html?contextId=recommendation_resize",
"de": "",
"it": "",
"fr": ""
},
"skipped_force_redaction": {
"en": "/en/index-en.html?contextId=skipped_force_redaction",
"de": "",

View File

@ -753,7 +753,6 @@
"delete": {
"action": "Datei löschen"
},
"disable-auto-analysis": "",
"dossier-details": {
"attributes": {
"expand": "{count} {count, plural, one{benutzerdefiniertes Attribut} other{benutzerdefinierte Attribute}}",
@ -777,7 +776,6 @@
},
"download-file": "Herunterladen",
"download-file-disabled": "Nur genehmigte Dateien können heruntergeladen werden",
"enable-auto-analysis": "",
"file-listing": {
"file-entry": {
"file-error": "Reanalyse erforderlich",
@ -823,6 +821,8 @@
"reanalyse": {
"action": "Datei analysieren"
},
"start-auto-analysis": "",
"stop-auto-analysis": "",
"table-col-names": {
"added-on": "Hinzugefügt",
"assigned-to": "Zugewiesen an",
@ -1572,6 +1572,14 @@
"title": "Das Dokument existiert bereits!"
},
"page": "Seite",
"page-rotation": {
"apply": "",
"confirmation-dialog": {
"question": "",
"title": ""
},
"discard": ""
},
"pagination": {
"next": "Nächste",
"previous": "Vorherige"
@ -1657,6 +1665,7 @@
"invalid-upload": "Ungültiges Upload-Format ausgewählt! Unterstützt werden Dokumente im .xlsx- und im .docx-Format",
"multi-file-report": "(Mehrere Dateien)",
"report-documents": "Dokumente für den Bericht",
"setup": "",
"table-header": {
"description": "Beschreibung",
"placeholders": "Platzhalter"

View File

@ -786,7 +786,6 @@
"delete": {
"action": "Delete File"
},
"disable-auto-analysis": "Disable auto-analysis",
"dossier-details": {
"attributes": {
"expand": "{count} custom {count, plural, one{attribute} other{attributes}}",
@ -810,7 +809,6 @@
},
"download-file": "Download",
"download-file-disabled": "You need to be approver in the dossier and the {count, plural, one{file needs} other{files need}} to be approved in order to download.",
"enable-auto-analysis": "Enable auto-analysis",
"file-listing": {
"file-entry": {
"file-error": "Re-processing required",
@ -856,10 +854,12 @@
"reanalyse": {
"action": "Analyze File"
},
"start-auto-analysis": "Start auto-analysis",
"stop-auto-analysis": "Stop auto-analysis",
"table-col-names": {
"added-on": "Added",
"assigned-to": "Assigned to",
"last-modified": "Last modified on",
"last-modified": "Last modified",
"name": "Name",
"needs-work": "Workload",
"pages": "Pages",
@ -1606,6 +1606,14 @@
"title": "Document already exists!"
},
"page": "Page",
"page-rotation": {
"apply": "APPLY",
"confirmation-dialog": {
"question": "You have unapplied page rotations. Choose how to proceed:",
"title": "Pending page rotations"
},
"discard": "DISCARD"
},
"pagination": {
"next": "Next",
"previous": "Prev"
@ -1660,7 +1668,7 @@
},
"reports": "Reports",
"reports-screen": {
"description": "Click the upload button on the right to upload your redaction report templates.",
"description": "Below, you will find a list of placeholders for dossier- and document-specific information. You can include these placeholders in your report templates.",
"descriptions": {
"dossier-attributes": "This placeholder gets replaced with the value of the dossier attribute <code>{attribute}</code>.",
"file-attributes": "This placeholder gets replaced with the value of the file attribute <code>{attribute}</code>.",
@ -1692,6 +1700,7 @@
"invalid-upload": "Invalid format selected for Upload! Supported formats are XLSX and DOCX",
"multi-file-report": "(Multi-file)",
"report-documents": "Report Documents",
"setup": "Click the upload button on the right to upload your redaction report templates.",
"table-header": {
"description": "Description",
"placeholders": "Placeholders"

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="100px" height="100px" viewBox="0 0 100 100" version="1.1" xmlns="http://www.w3.org/2000/svg">
<title>disable analysis</title>
<g id="Symbols" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="disable-analysis" fill="currentColor" fill-rule="nonzero">
<path d="M50,0 C77.5,0 100,22.5 100,50 C100,77.5 77.5,100 50,100 C22.5,100 0,77.5 0,50 C0,22.5 22.5,0 50,0 Z M50,10 C28,10 10,28 10,50 C10,72 28,90 50,90 C72,90 90,72 90,50 C90,28 72,10 50,10 Z M68,32 L68,68 L32,68 L32,32 L68,32 Z" id="Combined-Shape"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 640 B

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="100px" height="100px" viewBox="0 0 100 100" version="1.1" xmlns="http://www.w3.org/2000/svg">
<title>enable analysis</title>
<g id="Symbols" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="enable-analysis" fill="currentColor" fill-rule="nonzero">
<path d="M50,0 C77.5,0 100,22.5 100,50 C100,77.5 77.5,100 50,100 C22.5,100 0,77.5 0,50 C0,22.5 22.5,0 50,0 Z M50,10 C28,10 10,28 10,50 C10,72 28,90 50,90 C72,90 90,72 90,50 C90,28 72,10 50,10 Z M36,25 L74,50.3333333 L36,75.6666667 L36,25 Z" id="Combined-Shape"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 647 B

View File

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Svg Vector Icons : http://www.onlinewebfonts.com/icon -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 1000 1000" enable-background="new 0 0 1000 1000" xml:space="preserve">
<metadata> Svg Vector Icons : http://www.onlinewebfonts.com/icon </metadata>
<g><path d="M990,56c0-25.4-20.6-46-46-46H56c-25.4,0-46,20.6-46,46V944c0,25.4,20.6,46,46,46H944c25.4,0,46-20.6,46-46L990,56L990,56z M898.2,882.8c0,8.5-6.9,15.3-15.3,15.3H117.2c-8.5,0-15.3-6.9-15.3-15.3V117.2c0-8.5,6.9-15.3,15.3-15.3h765.7c8.5,0,15.3,6.9,15.3,15.3V882.8z"/><path d="M362.8,662.5c0,5.6,2.8,11.1,8,14.3c5.1,3.2,11.3,3.3,16.3,0.7l313.3-156.6c5.5-2.8,9.3-8.4,9.3-15c0-6.6-3.8-12.3-9.3-15L387.1,334.2c-5-2.5-11.2-2.4-16.3,0.7c-5.1,3.2-8,8.7-8,14.3L362.8,662.5L362.8,662.5z"/></g>
</svg>

Before

Width:  |  Height:  |  Size: 977 B

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="100px" version="1.1" viewBox="0 0 100 100" width="100px" xmlns="http://www.w3.org/2000/svg"
>
<g fill="none" fill-rule="evenodd" id="Symbols" stroke="none" stroke-width="1">
<g fill="#868E96" fill-rule="nonzero" id="rotate-left">
<path
d="M57.2797203,13.986014 L53.0839161,13.986014 L62.5244755,4.54545455 L57.2797203,-2.84217094e-14 L39.7972028,17.4825175 L57.6293706,35.3146853 L62.5244755,30.4195804 L53.0839161,20.979021 L57.2797203,20.979021 C70.9160839,20.979021 81.7552448,31.8181818 81.7552448,45.4545455 L88.7482517,45.4545455 C88.7482517,28.3216783 74.7622378,13.986014 57.2797203,13.986014 Z M69.7972028,45.4545455 L15.2517483,45.4545455 L15.2517483,100 L69.7972028,100 L69.7972028,45.4545455 Z M60.7062937,90.9090909 L24.3426573,90.9090909 L24.3426573,54.5454545 L60.7062937,54.5454545 L60.7062937,90.9090909 Z"
id="Combined-Shape"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 989 B

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="100px" version="1.1" viewBox="0 0 100 100" width="100px" xmlns="http://www.w3.org/2000/svg"
>
<g fill="none" fill-rule="evenodd" id="Symbols" stroke="none" stroke-width="1">
<g fill="#868E96" fill-rule="nonzero" id="rotate-right">
<path
d="M52.2797203,13.986014 L48.0839161,13.986014 L57.5244755,4.54545455 L52.2797203,-4.97379915e-14 L34.7972028,17.4825175 L52.6293706,35.3146853 L57.5244755,30.4195804 L48.0839161,20.979021 L52.2797203,20.979021 C65.9160839,20.979021 76.7552448,31.8181818 76.7552448,45.4545455 L83.7482517,45.4545455 C83.7482517,28.3216783 69.7622378,13.986014 52.2797203,13.986014 Z M64.7972028,45.4545455 L10.2517483,45.4545455 L10.2517483,100 L64.7972028,100 L64.7972028,45.4545455 Z M55.7062937,90.9090909 L19.3426573,90.9090909 L19.3426573,54.5454545 L55.7062937,54.5454545 L55.7062937,90.9090909 Z"
id="Combined-Shape" transform="translate(47.000000, 50.000000) scale(-1, 1) translate(-47.000000, -50.000000) "></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="100px" version="1.1" viewBox="0 0 100 100" width="100px" xmlns="http://www.w3.org/2000/svg">
<g fill="none" fill-rule="evenodd" id="Symbols" stroke="none" stroke-width="1">
<g fill="currentColor" fill-rule="nonzero" id="rotation">
<path
d="M52.4207252,90.9674374 C54.7349443,92.3043607 55.5280666,95.2640529 54.19265,97.5788783 C52.8561101,99.8937612 49.8972668,100.687111 47.5831053,99.3513114 C45.2688863,98.0143881 44.4757639,95.0546959 45.8111806,92.7398705 C47.1477204,90.4249875 50.1065638,89.6316377 52.4207252,90.9674374 Z M16.8644336,10.884275 C27.4113649,2.07056666 41.2974857,-1.65888061 54.8385703,0.686367352 L54.8385703,0.686367352 L54.8385703,10.5434613 C43.779557,8.10706078 32.2119179,10.85633 23.4310648,18.0114019 C14.6490596,25.1667619 9.61679387,35.9422956 9.76642378,47.2701031 C9.91503701,58.5970463 15.2296881,69.2363121 24.1951557,76.1571646 L24.1951557,76.1571646 L24.1951557,58.05572 L33.8728723,58.05572 L33.8728723,87.0954707 C33.8702514,88.3795865 33.3605887,89.610751 32.4526943,90.5189058 C31.5458944,91.4259659 30.3150831,91.9368696 29.0314219,91.9368696 L29.0314219,91.9368696 L4.54747351e-13,91.9368696 L4.54747351e-13,82.2563765 L16.3546557,82.2563765 C5.93675297,73.2909297 -0.0134903949,60.1974016 0.0843869704,46.4522077 C0.183462458,32.7061496 6.31951845,19.6993374 16.8644336,10.884275 Z M100,49.9891226 C100,52.6618456 97.8331845,54.8293691 95.1611417,54.8293691 C92.4891852,54.8293691 90.3222833,52.661932 90.3222833,49.9891226 C90.3222833,47.3163132 92.4890988,45.1488761 95.1611417,45.1488761 C97.8330981,45.1488761 100,47.3163132 100,49.9891226 M93.5485729,72.5755953 C93.5485729,75.2494419 91.3828807,77.4158419 88.7097146,77.4158419 C86.0377581,77.4158419 83.8720083,75.2495283 83.8720083,72.5755953 C83.8720083,69.9028723 86.0377005,67.7365012 88.7097146,67.7365012 C91.3827943,67.7365012 93.5485729,69.9028147 93.5485729,72.5755953 M74.9949598,7.06906255 C77.3091789,8.40598583 78.1023012,11.365678 76.7657614,13.6805034 C75.4303447,15.9953864 72.4703782,16.7887362 70.1562167,15.451813 C67.8419976,14.1160132 67.0488753,11.1551975 68.3854152,8.8403721 C69.721955,6.52548912 72.6807984,5.73213927 74.9949598,7.06906255 M75.4701766,84.093544 C77.7945048,85.4304673 78.5956626,88.3990904 77.260102,90.7242871 C75.9235621,93.0504057 72.9557904,93.8515341 70.6312606,92.5158784 C68.3058092,91.1789551 67.5049106,88.210332 68.8401832,85.8851353 C70.176723,83.5590167 73.1444947,82.7578884 75.4701766,84.093544 M93.3872872,25.2246686 C94.7227038,27.5507872 93.92169,30.5195255 91.5973618,31.8554116 C89.2719104,33.1923349 86.3040235,32.3910913 84.9673685,30.0649727 C83.6319518,27.7399777 84.4329656,24.7712682 86.7584459,23.4342297 C89.0827741,22.0973064 92.0506322,22.8996735 93.3872872,25.2246686"
transform="translate(50.000000, 50.000000) rotate(90.000000) translate(-50.000000, -50.000000) "></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@ -1,5 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M504 256C504 119 393 8 256 8S8 119 8 256s111 248 248 248 248-111 248-248zm-448 0c0-110.5 89.5-200 200-200s200 89.5 200 200-89.5 200-200 200S56 366.5 56 256zm296-80v160c0 8.8-7.2 16-16 16H176c-8.8 0-16-7.2-16-16V176c0-8.8 7.2-16 16-16h160c8.8 0 16 7.2 16 16z"/></svg>
<!--
Font Awesome Free 5.2.0 by @fontawesome - https://fontawesome.com
License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
-->

Before

Width:  |  Height:  |  Size: 512 B

@ -1 +1 @@
Subproject commit c11f73ba9e202ae86e60a6e6eb1639a3a59d04a8
Subproject commit 2bae4da2b991ad300b893f6b73797b6c1b90448c

View File

@ -31,6 +31,10 @@ export async function wipeCaches(logoutDependant: boolean = false) {
}
}
export function wipeFilesCache() {
return caches.delete('files');
}
export async function wipeCacheEntry(cacheName: string, entry: string) {
caches.open(cacheName).then(cache => {
cache.delete(entry, { ignoreSearch: false });

View File

@ -57,8 +57,8 @@ export class DossierStats implements IDossierStats {
this.numberOfPages = stats.numberOfPages;
this.numberOfFiles = stats.numberOfFiles;
this.numberOfProcessingFiles = Object.entries<number>(this.fileCountPerProcessingStatus)
.filter(([key, _]) => isProcessingStatuses.includes(key as ProcessingFileStatus))
.reduce((count, [_, value]) => count + value, 0);
.filter(([key]) => isProcessingStatuses.includes(key as ProcessingFileStatus))
.reduce((count, [, value]) => count + value, 0);
this.processingStats = this.#processingStats;
this.hasFiles = this.numberOfFiles > 0;
}

View File

@ -9,7 +9,6 @@ export class File extends Entity<IFile> implements IFile, IRouterPath {
readonly allManualRedactionsApplied: boolean;
readonly analysisDuration?: number;
readonly analysisRequired: boolean;
readonly annotationModificationDate?: string;
readonly approvalDate?: string;
readonly assignee?: string;
readonly dictionaryVersion?: number;
@ -41,6 +40,7 @@ export class File extends Entity<IFile> implements IFile, IRouterPath {
readonly processingStatus: ProcessingFileStatus;
readonly workflowStatus: WorkflowFileStatus;
readonly fileManipulationDate: string;
readonly redactionModificationDate: string;
readonly statusSort: number;
readonly cacheIdentifier?: string;
@ -49,6 +49,7 @@ export class File extends Entity<IFile> implements IFile, IRouterPath {
readonly isNew: boolean;
readonly isError: boolean;
readonly isProcessing: boolean;
readonly isOcrProcessing: boolean;
readonly isInitialProcessing: boolean;
readonly isApproved: boolean;
readonly isUnprocessed: boolean;
@ -64,7 +65,6 @@ export class File extends Entity<IFile> implements IFile, IRouterPath {
this.allManualRedactionsApplied = !!file.allManualRedactionsApplied;
this.analysisDuration = file.analysisDuration;
this.analysisRequired = !!file.analysisRequired && !file.excluded;
this.annotationModificationDate = file.annotationModificationDate;
this.approvalDate = file.approvalDate;
this.assignee = file.assignee;
this.dictionaryVersion = file.dictionaryVersion;
@ -97,13 +97,15 @@ export class File extends Entity<IFile> implements IFile, IRouterPath {
this.uploader = file.uploader;
this.excludedPages = file.excludedPages || [];
this.hasSuggestions = !!file.hasSuggestions;
this.fileManipulationDate = file.fileManipulationDate;
this.fileManipulationDate = file.fileManipulationDate ?? '';
this.redactionModificationDate = file.redactionModificationDate ?? '';
this.statusSort = StatusSorter[this.workflowStatus];
this.cacheIdentifier = btoa(this.fileManipulationDate ?? '');
this.hintsOnly = this.hasHints && !this.hasRedactions;
this.hasNone = !this.hasRedactions && !this.hasHints && !this.hasSuggestions;
this.isProcessing = isProcessingStatuses.includes(this.processingStatus);
this.isOcrProcessing = this.processingStatus === ProcessingFileStatuses.OCR_PROCESSING;
this.isInitialProcessing = this.isProcessing && this.numberOfAnalyses === 0;
this.isApproved = this.workflowStatus === WorkflowFileStatuses.APPROVED;
this.isNew = this.workflowStatus === WorkflowFileStatuses.NEW;

View File

@ -32,7 +32,6 @@ export interface IFile {
/**
* Shows which dictionary versions was used during the analysis.
*/
readonly annotationModificationDate?: string;
readonly dictionaryVersion?: number;
/**
* Shows which dossier dictionary versions was used during the analysis.
@ -151,5 +150,6 @@ export interface IFile {
/**
* Last time the actual file was touched
*/
readonly fileManipulationDate: string;
readonly fileManipulationDate: string | null;
readonly redactionModificationDate: string | null;
}

View File

@ -2,3 +2,4 @@ export * from './page-range';
export * from './viewed-page';
export * from './viewed-pages.request';
export * from './page-exclusion.request';
export * from './page-rotation.request';

View File

@ -0,0 +1,3 @@
export interface IPageRotationRequest {
readonly pages: Readonly<Record<number, number>>;
}

View File

@ -1,6 +1,6 @@
import { Observable } from 'rxjs';
import { CircleButtonType } from '@iqser/common-ui';
import { File } from '@red/domain';
import { File } from '../files';
export type ActionType = 'circleBtn' | 'downloadBtn' | 'toggle';
@ -11,7 +11,7 @@ export const ActionTypes = {
};
export interface Action {
action?: (...args: any[]) => void;
action?: (...args: unknown[]) => void;
tooltip?: string;
icon?: string;
show?: boolean;

View File

@ -7,3 +7,4 @@ export * from './default-color-type';
export * from './colors';
export * from './view-mode';
export * from './expandable-file-actions';
export * from './pdf.types';

View File

@ -0,0 +1,19 @@
import { ValuesOf } from '@iqser/common-ui';
export interface IHeaderElement {
readonly type: string;
readonly element?: string;
readonly dataElement?: string;
readonly img?: string;
readonly title?: string;
readonly toolGroup?: string;
readonly render?: () => void;
readonly onClick?: () => void;
}
export const RotationTypes = {
LEFT: 270,
RIGHT: 90,
} as const;
export type RotationType = ValuesOf<typeof RotationTypes>;

View File

@ -1,6 +1,6 @@
{
"name": "redaction",
"version": "3.281.0",
"version": "3.296.0",
"private": true,
"license": "MIT",
"scripts": {

Binary file not shown.