Merge branch 'master' into VM/RED-3982

This commit is contained in:
Valentin Mihai 2022-05-26 13:37:05 +03:00
commit 88a3625f80
105 changed files with 4411 additions and 2786 deletions

View File

@ -11,7 +11,7 @@
"builder": "@nrwl/jest:jest",
"outputs": ["coverage/libs/common-ui"],
"options": {
"jestConfig": "libs/common-ui/jest.config.js",
"jestConfig": "libs/common-ui/jest.config.ts",
"passWithNoTests": true
}
},
@ -41,7 +41,7 @@
"test": {
"builder": "@nrwl/jest:jest",
"options": {
"jestConfig": "libs/red-cache/jest.config.js",
"jestConfig": "libs/red-cache/jest.config.ts",
"passWithNoTests": true
},
"outputs": ["coverage/libs/red-cache"]
@ -64,7 +64,7 @@
"builder": "@nrwl/jest:jest",
"outputs": ["coverage/libs/red-domain"],
"options": {
"jestConfig": "libs/red-domain/jest.config.js",
"jestConfig": "libs/red-domain/jest.config.ts",
"passWithNoTests": true
}
},
@ -200,7 +200,7 @@
"test": {
"builder": "@nrwl/jest:jest",
"options": {
"jestConfig": "apps/red-ui/jest.config.js",
"jestConfig": "apps/red-ui/jest.config.ts",
"passWithNoTests": true
},
"outputs": ["coverage/apps/red-ui"]

View File

@ -1,4 +1,5 @@
module.exports = {
/* eslint-disable */
export default {
preset: '../../jest.preset.js',
setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],
globals: {

View File

@ -16,14 +16,6 @@
"resources": {
"files": ["/assets/**", "/*.(eot|svg|cur|jpg|png|webp|gif|otf|ttf|woff|woff2|ani)"]
}
},
{
"name": "pdftron",
"installMode": "prefetch",
"updateMode": "prefetch",
"resources": {
"files": ["/assets/wv-resources/**/*.*"]
}
}
]
}

View File

@ -15,6 +15,7 @@ import { DashboardGuard } from '@guards/dashboard-guard.service';
import { TrashGuard } from '@guards/trash.guard';
import { ARCHIVE_ROUTE, BreadcrumbTypes, DOSSIER_ID, DOSSIER_TEMPLATE_ID, DOSSIERS_ARCHIVE, DOSSIERS_ROUTE, FILE_ID } from '@red/domain';
import { DossierFilesGuard } from '@guards/dossier-files-guard';
import { WebViewerLoadedGuard } from './modules/pdf-viewer/services/webviewer-loaded.guard';
const routes: Routes = [
{
@ -94,7 +95,7 @@ const routes: Routes = [
},
{
path: `:${DOSSIER_ID}/file/:${FILE_ID}`,
canActivate: [CompositeRouteGuard],
canActivate: [CompositeRouteGuard, WebViewerLoadedGuard],
data: {
routeGuards: [DossierFilesGuard],
breadcrumbs: [BreadcrumbTypes.dossierTemplate, BreadcrumbTypes.dossier, BreadcrumbTypes.file],

View File

@ -1,4 +1,7 @@
<router-outlet></router-outlet>
<redaction-pdf-viewer [style.visibility]="(documentViewer.loaded$ | async) ? 'visible' : 'hidden'"></redaction-pdf-viewer>
<iqser-full-page-loading-indicator></iqser-full-page-loading-indicator>
<iqser-connection-status></iqser-connection-status>
<iqser-full-page-error></iqser-full-page-error>

View File

@ -1,6 +1,7 @@
import { Component, ViewContainerRef } from '@angular/core';
import { RouterHistoryService } from '@services/router-history.service';
import { UserService } from '@services/user.service';
import { REDDocumentViewer } from './modules/pdf-viewer/services/document-viewer.service';
@Component({
selector: 'redaction-root',
@ -14,5 +15,6 @@ export class AppComponent {
public viewContainerRef: ViewContainerRef,
private readonly _routerHistoryService: RouterHistoryService,
private readonly _userService: UserService,
readonly documentViewer: REDDocumentViewer,
) {}
}

View File

@ -20,7 +20,7 @@ import { AppRoutingModule } from './app-routing.module';
import { SharedModule } from '@shared/shared.module';
import { FileUploadDownloadModule } from '@upload-download/file-upload-download.module';
import { DatePipe as BaseDatePipe, PlatformLocation } from '@angular/common';
import { ACTIVE_DOSSIERS_SERVICE, ARCHIVED_DOSSIERS_SERVICE, BASE_HREF } from './tokens';
import { ACTIVE_DOSSIERS_SERVICE, ARCHIVED_DOSSIERS_SERVICE, BASE_HREF, BASE_HREF_FN } from './tokens';
import { MonacoEditorModule } from '@materia-ui/ngx-monaco-editor';
import { GlobalErrorHandler } from '@utils/global-error-handler.service';
import { REDMissingTranslationHandler } from '@utils/missing-translations-handler';
@ -51,6 +51,8 @@ import { MAT_TOOLTIP_DEFAULT_OPTIONS } from '@angular/material/tooltip';
import { LoggerModule, NgxLoggerLevel, TOKEN_LOGGER_CONFIG, TOKEN_LOGGER_RULES_SERVICE } from 'ngx-logger';
import { LoggerRulesService } from '@services/logger-rules.service';
import { ILoggerConfig } from '@red/domain';
import { SystemPreferencesService } from '@services/system-preferences.service';
import { PdfViewerModule } from './modules/pdf-viewer/pdf-viewer.module';
export function httpLoaderFactory(httpClient: HttpClient, configService: ConfigService): PruningTranslationLoader {
return new PruningTranslationLoader(httpClient, '/assets/i18n/', `.json?version=${configService.values.FRONTEND_APP_VERSION}`);
@ -82,6 +84,7 @@ const components = [AppComponent, AuthErrorComponent, NotificationsComponent, Sp
AppRoutingModule,
MonacoEditorModule,
IqserHelpModeModule,
PdfViewerModule,
ToastrModule.forRoot({
closeButton: true,
enableHtml: true,
@ -148,6 +151,11 @@ const components = [AppComponent, AuthErrorComponent, NotificationsComponent, Sp
useFactory: (s: PlatformLocation) => cleanupBaseUrl(s.getBaseHrefFromDOM()),
deps: [PlatformLocation],
},
{
provide: BASE_HREF_FN,
useFactory: (baseHref: string) => (path: string) => baseHref + path,
deps: [BASE_HREF],
},
{
provide: HTTP_INTERCEPTORS,
multi: true,
@ -167,6 +175,7 @@ const components = [AppComponent, AuthErrorComponent, NotificationsComponent, Sp
KeycloakService,
Title,
ConfigService,
SystemPreferencesService,
FeaturesService,
GeneralSettingsService,
LanguageService,

View File

@ -7,7 +7,7 @@
<redaction-breadcrumbs></redaction-breadcrumbs>
</div>
<a [routerLink]="['/']" class="logo">
<a [matTooltip]="'top-bar.navigation-items.back-to-dashboard' | translate" [routerLink]="['/']" class="logo">
<iqser-hidden-action (action)="userPreferenceService.toggleDevFeatures()">
<iqser-logo icon="red:logo"></iqser-logo>
</iqser-hidden-action>

View File

@ -43,6 +43,7 @@ import { ConfirmDeleteDossierStateDialogComponent } from './dialogs/confirm-dele
import { BaseEntityScreenComponent } from './base-entity-screen/base-entity-screen.component';
import { CloneDossierTemplateDialogComponent } from './dialogs/clone-dossier-template-dialog/clone-dossier-template-dialog.component';
import { AdminSideNavComponent } from './admin-side-nav/admin-side-nav.component';
import { SystemPreferencesFormComponent } from './screens/general-config/system-preferences-form/system-preferences-form.component';
import { ConfigureCertificateDialogComponent } from './dialogs/configure-digital-signature-dialog/configure-certificate-dialog.component';
import { PkcsSignatureConfigurationComponent } from './dialogs/configure-digital-signature-dialog/form/pkcs-signature-configuration/pkcs-signature-configuration.component';
import { KmsSignatureConfigurationComponent } from './dialogs/configure-digital-signature-dialog/form/kms-signature-configuration/kms-signature-configuration.component';
@ -88,6 +89,7 @@ const components = [
BaseEntityScreenComponent,
GeneralConfigFormComponent,
SmtpFormComponent,
SystemPreferencesFormComponent,
PkcsSignatureConfigurationComponent,
KmsSignatureConfigurationComponent,

View File

@ -24,20 +24,6 @@ export class GeneralConfigFormComponent extends BaseFormComponent implements OnI
this.form = this._getForm();
}
get generalConfigurationChanged(): boolean {
if (!this._initialConfiguration) {
return true;
}
for (const key of Object.keys(this.form.getRawValue())) {
if (this._initialConfiguration[key] !== this.form.get(key).value) {
return true;
}
}
return false;
}
async ngOnInit(): Promise<void> {
await this._loadData();
}

View File

@ -24,6 +24,9 @@
<div class="dialog mb-0">
<redaction-general-config-form></redaction-general-config-form>
</div>
<div class="dialog mt-24 mb-0">
<redaction-system-preferences-form></redaction-system-preferences-form>
</div>
<div class="dialog mt-24">
<redaction-smtp-form></redaction-smtp-form>
</div>

View File

@ -3,6 +3,7 @@ import { UserService } from '@services/user.service';
import { GeneralConfigFormComponent } from './general-config-form/general-config-form.component';
import { SmtpFormComponent } from './smtp-form/smtp-form.component';
import { BaseFormComponent } from '@iqser/common-ui';
import { SystemPreferencesFormComponent } from './system-preferences-form/system-preferences-form.component';
@Component({
selector: 'redaction-general-config-screen',
@ -13,6 +14,7 @@ export class GeneralConfigScreenComponent extends BaseFormComponent implements A
readonly currentUser = this._userService.currentUser;
@ViewChild(GeneralConfigFormComponent) generalConfigFormComponent: GeneralConfigFormComponent;
@ViewChild(SystemPreferencesFormComponent) systemPreferencesFormComponent: SystemPreferencesFormComponent;
@ViewChild(SmtpFormComponent) smtpFormComponent: SmtpFormComponent;
children: BaseFormComponent[];
@ -20,10 +22,6 @@ export class GeneralConfigScreenComponent extends BaseFormComponent implements A
super();
}
ngAfterViewInit() {
this.children = [this.generalConfigFormComponent, this.smtpFormComponent];
}
get changed(): boolean {
for (const child of this.children) {
if (child.changed) {
@ -42,6 +40,10 @@ export class GeneralConfigScreenComponent extends BaseFormComponent implements A
return true;
}
ngAfterViewInit() {
this.children = [this.generalConfigFormComponent, this.systemPreferencesFormComponent, this.smtpFormComponent];
}
async save(): Promise<void> {
for (const child of this.children) {
if (child.changed) {

View File

@ -0,0 +1,36 @@
<div class="dialog-header">
<div class="heading-l" translate="general-config-screen.system-preferences.title"></div>
</div>
<form (submit)="save()" *ngIf="form" [formGroup]="form">
<div class="dialog-content">
<div class="dialog-content-left">
<div *ngFor="let key of keys" [class.required]="key.type !== 'boolean'" class="iqser-input-group">
<ng-container *ngIf="key.type !== 'boolean'">
<label [translate]="translations.label[key.name]"></label>
<input
*ngIf="key.type === 'number'"
[formControlName]="key.name"
[name]="key.name"
[placeholder]="translations.placeholder[key.name] | translate"
type="number"
/>
<input
*ngIf="key.type === 'string'"
[formControlName]="key.name"
[name]="key.name"
[placeholder]="translations.placeholder[key.name] | translate"
type="text"
/>
</ng-container>
<mat-slide-toggle *ngIf="key.type === 'boolean'" [formControlName]="key.name" color="primary">
{{ translations.label[key.name] | translate }}
</mat-slide-toggle>
</div>
</div>
</div>
<div class="dialog-actions">
<button [disabled]="form?.invalid || !changed" color="primary" mat-flat-button type="submit">
{{ 'general-config-screen.actions.save' | translate }}
</button>
</div>
</form>

View File

@ -0,0 +1,54 @@
import { Component } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { SystemPreferences } from '@red/domain';
import { BaseFormComponent, KeysOf, LoadingService } from '@iqser/common-ui';
import { firstValueFrom } from 'rxjs';
import { SystemPreferencesService } from '@services/system-preferences.service';
import { systemPreferencesTranslations } from '@translations/system-preferences-translations';
export type ValueType = 'number' | 'string' | 'boolean';
@Component({
selector: 'redaction-system-preferences-form',
templateUrl: './system-preferences-form.component.html',
styleUrls: ['./system-preferences-form.component.scss'],
})
export class SystemPreferencesFormComponent extends BaseFormComponent {
readonly translations = systemPreferencesTranslations;
readonly keys: { name: KeysOf<SystemPreferences>; type: ValueType }[] = [
{ name: 'softDeleteCleanupTime', type: 'number' },
{ name: 'downloadCleanupDownloadFilesHours', type: 'number' },
{ name: 'downloadCleanupNotDownloadFilesHours', type: 'number' },
{ name: 'removeDigitalSignaturesOnUpload', type: 'boolean' },
];
private _initialConfiguration: SystemPreferences;
constructor(
private readonly _loadingService: LoadingService,
private readonly _systemPreferencesService: SystemPreferencesService,
private readonly _formBuilder: FormBuilder,
) {
super();
this.form = this._getForm();
this._loadData();
}
async save(): Promise<void> {
this._loadingService.start();
await firstValueFrom(this._systemPreferencesService.update(this.form.getRawValue()));
this._loadData();
this._loadingService.stop();
}
private _getForm(): FormGroup {
const controlsConfig = {};
this.keys.forEach(key => (controlsConfig[key.name] = [this._systemPreferencesService.values[key.name], Validators.required]));
return this._formBuilder.group(controlsConfig);
}
private _loadData() {
this._initialConfiguration = this._systemPreferencesService.values;
this.form.patchValue(this._initialConfiguration, { emitEvent: false });
this.initialFormValue = this.form.getRawValue();
}
}

View File

@ -6,7 +6,7 @@ import { HttpClient } from '@angular/common/http';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Debounce, IconButtonTypes, LoadingService, Toaster } from '@iqser/common-ui';
import { DOSSIER_TEMPLATE_ID, IWatermark, WatermarkOrientation, WatermarkOrientations } from '@red/domain';
import { BASE_HREF } from '../../../../../tokens';
import { BASE_HREF, BASE_HREF_FN, BaseHrefFn } from '../../../../../tokens';
import { stampPDFPage } from '@utils/page-stamper';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { WatermarkService } from '@services/entity-services/watermark.service';
@ -44,7 +44,7 @@ export class WatermarkScreenComponent implements OnInit {
private readonly _formBuilder: FormBuilder,
readonly permissionsService: PermissionsService,
private readonly _loadingService: LoadingService,
@Inject(BASE_HREF) private readonly _baseHref: string,
@Inject(BASE_HREF_FN) private readonly _convertPath: BaseHrefFn,
private readonly _watermarkService: WatermarkService,
private readonly _changeDetectorRef: ChangeDetectorRef,
) {
@ -126,8 +126,8 @@ export class WatermarkScreenComponent implements OnInit {
WebViewer(
{
licenseKey: environment.licenseKey ? atob(environment.licenseKey) : null,
path: this._baseHref + '/assets/wv-resources',
css: this._baseHref + '/assets/pdftron/stylesheet.css',
path: this._convertPath('/assets/wv-resources'),
css: this._convertPath('/assets/pdftron/stylesheet.css'),
fullAPI: true,
isReadOnly: true,
backendType: 'ems',

View File

@ -29,7 +29,7 @@
<span [innerHTML]="'dossier-template-stats.total-people' | translate: { count: dossierTemplate.numberOfPeople }"></span>
</div>
<div>
<mat-icon svgIcon="iqser:trash"></mat-icon>
<mat-icon svgIcon="iqser:pages"></mat-icon>
<span
[innerHTML]="'dossier-template-stats.analyzed-pages' | translate: { count: dossierTemplate.numberOfPages }"
></span>

View File

@ -7,11 +7,10 @@ import { UserService } from '@services/user.service';
import { AnnotationReferencesService } from '../../services/annotation-references.service';
import { MultiSelectService } from '../../services/multi-select.service';
import { FilePreviewStateService } from '../../services/file-preview-state.service';
import { HelpModeService, ScrollableParentView, ScrollableParentViews } from '@iqser/common-ui';
import { PdfViewer } from '../../services/pdf-viewer.service';
import { FileDataService } from '../../services/file-data.service';
import { HelpModeService } from '@iqser/common-ui';
import { DictionariesMapService } from '@services/entity-services/dictionaries-map.service';
import { ViewModeService } from '../../services/view-mode.service';
import { REDAnnotationManager } from '../../../pdf-viewer/services/annotation-manager.service';
export const AnnotationButtonTypes = {
dark: 'dark',
@ -40,10 +39,9 @@ export class AnnotationActionsComponent implements OnChanges {
readonly helpModeService: HelpModeService,
private readonly _changeRef: ChangeDetectorRef,
private readonly _userService: UserService,
private readonly _pdf: PdfViewer,
private readonly _annotationManager: REDAnnotationManager,
private readonly _state: FilePreviewStateService,
private readonly _permissionsService: PermissionsService,
private readonly _fileDataService: FileDataService,
private readonly _dictionariesMapService: DictionariesMapService,
) {}
@ -59,11 +57,7 @@ export class AnnotationActionsComponent implements OnChanges {
}
get viewerAnnotations() {
if (this._pdf.annotationManager) {
return this._annotations.map(a => this._pdf.annotationManager.getAnnotationById(a.id));
} else {
return [];
}
return this._annotationManager.get(this._annotations);
}
get isVisible() {
@ -98,16 +92,16 @@ export class AnnotationActionsComponent implements OnChanges {
hideAnnotation($event: MouseEvent) {
$event.stopPropagation();
this._pdf.annotationManager.hideAnnotations(this.viewerAnnotations);
this._pdf.annotationManager.deselectAllAnnotations();
this._fileDataService.updateHiddenAnnotations(this.viewerAnnotations, true);
this._annotationManager.hide(this.viewerAnnotations);
this._annotationManager.deselect();
this._annotationManager.hidden.add(this.viewerAnnotations[0].Id);
}
showAnnotation($event: MouseEvent) {
$event.stopPropagation();
this._pdf.annotationManager.showAnnotations(this.viewerAnnotations);
this._pdf.annotationManager.deselectAllAnnotations();
this._fileDataService.updateHiddenAnnotations(this.viewerAnnotations, false);
this._annotationManager.show(this.viewerAnnotations);
this._annotationManager.deselect();
this._annotationManager.hidden.delete(this.viewerAnnotations[0].Id);
}
resize($event: MouseEvent) {

View File

@ -4,9 +4,10 @@ import { TranslateService } from '@ngx-translate/core';
import { annotationChangesTranslations } from '@translations/annotation-changes-translations';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { MultiSelectService } from '../../services/multi-select.service';
import { KeysOf, ListingService } from '@iqser/common-ui';
import { KeysOf } from '@iqser/common-ui';
import { BehaviorSubject, Observable } from 'rxjs';
import { filter, map, switchMap } from 'rxjs/operators';
import { AnnotationsListingService } from '../../services/annotations-listing.service';
interface Engine {
readonly icon: string;
@ -41,7 +42,7 @@ export class AnnotationDetailsComponent implements OnChanges {
constructor(
private readonly _translateService: TranslateService,
private readonly _listingService: ListingService<AnnotationWrapper>,
private readonly _listingService: AnnotationsListingService,
readonly multiSelectService: MultiSelectService,
) {
this.isSelected$ = this._annotationChanged$.pipe(switchMap(annotation => this._listingService.isSelected$(annotation)));

View File

@ -2,8 +2,8 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, HostBinding, Inp
import { AnnotationWrapper } from '@models/file/annotation.wrapper';
import { BehaviorSubject, filter } from 'rxjs';
import { switchMap, tap } from 'rxjs/operators';
import { ListingService } from '@iqser/common-ui';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { AnnotationsListingService } from '../../services/annotations-listing.service';
@UntilDestroy()
@Component({
@ -17,7 +17,7 @@ export class AnnotationReferenceComponent implements OnChanges {
@HostBinding('class.active') isSelected = false;
private readonly _annotationChanged$ = new BehaviorSubject<AnnotationWrapper>(undefined);
constructor(private readonly _listingService: ListingService<AnnotationWrapper>, private readonly _changeRef: ChangeDetectorRef) {
constructor(private readonly _listingService: AnnotationsListingService, private readonly _changeRef: ChangeDetectorRef) {
this._annotationChanged$
.pipe(
filter(annotation => !!annotation),

View File

@ -1,8 +1,8 @@
import { ChangeDetectionStrategy, Component, EventEmitter, Output } from '@angular/core';
import { AnnotationWrapper } from '@models/file/annotation.wrapper';
import { AnnotationReferencesService } from '../../services/annotation-references.service';
import { ListingService } from '@iqser/common-ui';
import { Observable, switchMap } from 'rxjs';
import { AnnotationsListingService } from '../../services/annotations-listing.service';
@Component({
selector: 'redaction-annotation-references-list',
@ -15,7 +15,7 @@ export class AnnotationReferencesListComponent {
readonly isSelected$: Observable<boolean>;
constructor(
private readonly _listingService: ListingService<AnnotationWrapper>,
private readonly _listingService: AnnotationsListingService,
readonly annotationReferencesService: AnnotationReferencesService,
) {
this.isSelected$ = this.annotationReferencesService.annotation$.pipe(

View File

@ -1,9 +1,9 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, HostBinding, Input, OnChanges, TemplateRef } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { ListingService } from '@iqser/common-ui';
import { switchMap, tap } from 'rxjs/operators';
import { AnnotationWrapper } from '@models/file/annotation.wrapper';
import { MultiSelectService } from '../../services/multi-select.service';
import { AnnotationsListingService } from '../../services/annotations-listing.service';
@Component({
selector: 'redaction-annotation-wrapper [annotation] [annotationActionsTemplate]',
@ -23,7 +23,7 @@ export class AnnotationWrapperComponent implements OnChanges {
constructor(
private readonly _changeRef: ChangeDetectorRef,
readonly listingService: ListingService<AnnotationWrapper>,
readonly listingService: AnnotationsListingService,
readonly multiSelectService: MultiSelectService,
) {
this.isSelected$ = this._annotationChanged$.pipe(

View File

@ -10,14 +10,15 @@ import {
TemplateRef,
} from '@angular/core';
import { AnnotationWrapper } from '@models/file/annotation.wrapper';
import { FilterService, HasScrollbarDirective, IqserEventTarget, ListingService } from '@iqser/common-ui';
import { FilterService, HasScrollbarDirective, IqserEventTarget } from '@iqser/common-ui';
import { MultiSelectService } from '../../services/multi-select.service';
import { AnnotationReferencesService } from '../../services/annotation-references.service';
import { UserPreferenceService } from '@services/user-preference.service';
import { ViewModeService } from '../../services/view-mode.service';
import { BehaviorSubject } from 'rxjs';
import { TextHighlightsGroup } from '@red/domain';
import { PdfViewer } from '../../services/pdf-viewer.service';
import { REDAnnotationManager } from '../../../pdf-viewer/services/annotation-manager.service';
import { AnnotationsListingService } from '../../services/annotations-listing.service';
@Component({
selector: 'redaction-annotations-list',
@ -40,8 +41,8 @@ export class AnnotationsListComponent extends HasScrollbarDirective implements O
private readonly _filterService: FilterService,
private readonly _userPreferenceService: UserPreferenceService,
private readonly _viewModeService: ViewModeService,
private readonly _pdf: PdfViewer,
private readonly _listingService: ListingService<AnnotationWrapper>,
private readonly _annotationManager: REDAnnotationManager,
private readonly _listingService: AnnotationsListingService,
readonly annotationReferencesService: AnnotationReferencesService,
) {
super(_elementRef, _changeDetector);
@ -69,13 +70,13 @@ export class AnnotationsListComponent extends HasScrollbarDirective implements O
this.pagesPanelActive.emit(false);
if (this._listingService.isSelected(annotation)) {
this._pdf.deselectAnnotations([annotation]);
this._annotationManager.deselect(annotation);
} else {
const canMultiSelect = this._multiSelectService.isEnabled;
if (canMultiSelect && ($event?.ctrlKey || $event?.metaKey) && this._listingService.selected.length > 0) {
this._multiSelectService.activate();
}
this._pdf.selectAnnotations([annotation]);
this._listingService.selectAnnotations([annotation]);
}
}

View File

@ -48,7 +48,7 @@
<div *ngIf="multiSelectService.active$ | async" class="multi-select">
<div class="selected-wrapper">
<iqser-round-checkbox
(click)="pdf.deselectAllAnnotations()"
(click)="annotationManager.deselect()"
[indeterminate]="listingService.areSomeSelected$ | async"
type="with-bg"
></iqser-round-checkbox>

View File

@ -3,10 +3,9 @@ import {
ChangeDetectorRef,
Component,
ElementRef,
EventEmitter,
HostListener,
Input,
Output,
OnDestroy,
TemplateRef,
ViewChild,
} from '@angular/core';
@ -15,17 +14,17 @@ import { AnnotationProcessingService } from '../../services/annotation-processin
import { MatDialogRef, MatDialogState } from '@angular/material/dialog';
import scrollIntoView from 'scroll-into-view-if-needed';
import {
AutoUnsubscribe,
CircleButtonTypes,
Debounce,
FilterService,
IconButtonTypes,
INestedFilter,
IqserEventTarget,
ListingService,
shareDistinctLast,
shareLast,
} from '@iqser/common-ui';
import { combineLatest, firstValueFrom, Observable, takeWhile } from 'rxjs';
import { combineLatest, firstValueFrom, Observable } from 'rxjs';
import { map, tap } from 'rxjs/operators';
import { File } from '@red/domain';
import { ExcludedPagesService } from '../../services/excluded-pages.service';
@ -36,7 +35,10 @@ import { FilePreviewStateService } from '../../services/file-preview-state.servi
import { ViewModeService } from '../../services/view-mode.service';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { FileDataService } from '../../services/file-data.service';
import { PdfViewer } from '../../services/pdf-viewer.service';
import { PdfViewer } from '../../../pdf-viewer/services/pdf-viewer.service';
import { REDAnnotationManager } from '../../../pdf-viewer/services/annotation-manager.service';
import { AnnotationsListingService } from '../../services/annotations-listing.service';
import { REDDocumentViewer } from '../../../pdf-viewer/services/document-viewer.service';
const COMMAND_KEY_ARRAY = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Escape'];
const ALL_HOTKEY_ARRAY = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'];
@ -47,7 +49,7 @@ const ALL_HOTKEY_ARRAY = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'];
styleUrls: ['./file-workload.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class FileWorkloadComponent {
export class FileWorkloadComponent extends AutoUnsubscribe implements OnDestroy {
readonly iconButtonTypes = IconButtonTypes;
readonly circleButtonTypes = CircleButtonTypes;
@ -56,7 +58,6 @@ export class FileWorkloadComponent {
@Input() dialogRef: MatDialogRef<unknown>;
@Input() file!: File;
@Input() annotationActionsTemplate: TemplateRef<unknown>;
@Output() readonly selectPage = new EventEmitter<number>();
displayedPages: number[] = [];
pagesPanelActive = true;
readonly displayedAnnotations$: Observable<Map<number, AnnotationWrapper[]>>;
@ -71,26 +72,36 @@ export class FileWorkloadComponent {
readonly filterService: FilterService,
readonly skippedService: SkippedService,
readonly state: FilePreviewStateService,
readonly multiSelectService: MultiSelectService,
readonly documentInfoService: DocumentInfoService,
readonly excludedPagesService: ExcludedPagesService,
readonly pdf: PdfViewer,
readonly fileDataService: FileDataService,
readonly viewModeService: ViewModeService,
readonly listingService: ListingService<AnnotationWrapper>,
readonly pdf: PdfViewer,
readonly multiSelectService: MultiSelectService,
readonly annotationManager: REDAnnotationManager,
private readonly _documentViewer: REDDocumentViewer,
readonly documentInfoService: DocumentInfoService,
readonly listingService: AnnotationsListingService,
readonly excludedPagesService: ExcludedPagesService,
private readonly _changeDetectorRef: ChangeDetectorRef,
private readonly _annotationProcessingService: AnnotationProcessingService,
) {
this.pdf.currentPage$.pipe(takeWhile(() => !!this)).subscribe(pageNumber => {
super();
this.addActiveScreenSubscription = this.pdf.currentPage$.subscribe(pageNumber => {
this._scrollViews();
this.scrollAnnotationsToPage(pageNumber, 'always');
});
this.listingService.selected$.pipe(takeWhile(() => !!this)).subscribe(annotationIds => {
this.addActiveScreenSubscription = this.listingService.selected$.subscribe(annotationIds => {
if (annotationIds.length > 0) {
this.pagesPanelActive = false;
}
this.scrollToSelectedAnnotation();
});
this.addActiveScreenSubscription = this._documentViewer.keyUp$.subscribe($event => {
this.handleKeyEvent($event);
});
this.displayedAnnotations$ = this._displayedAnnotations$;
this.multiSelectInactive$ = this._multiSelectInactive$;
this.showExcludedPages$ = this._showExcludedPages$;
@ -134,7 +145,7 @@ export class FileWorkloadComponent {
return this.multiSelectService.inactive$.pipe(
tap(value => {
if (value) {
this.pdf.deselectAllAnnotations();
this.annotationManager.deselect();
}
}),
shareDistinctLast(),
@ -179,11 +190,11 @@ export class FileWorkloadComponent {
}
selectAllOnActivePage() {
this.pdf.selectAnnotations(this.activeAnnotations);
this.listingService.selectAnnotations(this.activeAnnotations);
}
deselectAllOnActivePage(): void {
this.pdf.deselectAnnotations(this.activeAnnotations);
this.annotationManager.deselect(this.activeAnnotations);
}
@HostListener('window:keyup', ['$event'])
@ -261,16 +272,16 @@ export class FileWorkloadComponent {
}
scrollQuickNavFirst(): void {
this.selectPage.emit(1);
this.pdf.navigateTo(1);
}
scrollQuickNavLast(): Promise<void> {
return firstValueFrom(this.state.file$).then(file => this.selectPage.emit(file.numberOfPages));
return firstValueFrom(this.state.file$).then(file => this.pdf.navigateTo(file.numberOfPages));
}
pageSelectedByClick($event: number): void {
this.pagesPanelActive = true;
this.selectPage.emit($event);
this.pdf.navigateTo($event);
}
preventKeyDefault($event: KeyboardEvent): void {
@ -280,30 +291,30 @@ export class FileWorkloadComponent {
}
jumpToPreviousWithAnnotations(): void {
this.selectPage.emit(this._prevPageWithAnnotations());
this.pdf.navigateTo(this._prevPageWithAnnotations());
}
jumpToNextWithAnnotations(): void {
this.selectPage.emit(this._nextPageWithAnnotations());
this.pdf.navigateTo(this._nextPageWithAnnotations());
}
navigateAnnotations($event: KeyboardEvent) {
if (!this._firstSelectedAnnotation || this.activeViewerPage !== this._firstSelectedAnnotation.pageNumber) {
if (this.displayedPages.indexOf(this.activeViewerPage) !== -1) {
// Displayed page has annotations
return this.pdf.selectAnnotations(this.activeAnnotations ? [this.activeAnnotations[0]] : null);
return this.listingService.selectAnnotations(this.activeAnnotations ? [this.activeAnnotations[0]] : null);
}
// Displayed page doesn't have annotations
if ($event.key === 'ArrowDown') {
const nextPage = this._nextPageWithAnnotations();
return this.pdf.selectAnnotations([this.displayedAnnotations.get(nextPage)[0]]);
return this.listingService.selectAnnotations([this.displayedAnnotations.get(nextPage)[0]]);
}
const prevPage = this._prevPageWithAnnotations();
const prevPageAnnotations = this.displayedAnnotations.get(prevPage);
return this.pdf.selectAnnotations([prevPageAnnotations[prevPageAnnotations.length - 1]]);
return this.listingService.selectAnnotations([prevPageAnnotations[prevPageAnnotations.length - 1]]);
}
const page = this._firstSelectedAnnotation.pageNumber;
@ -316,13 +327,13 @@ export class FileWorkloadComponent {
if ($event.key === 'ArrowDown') {
if (idx + 1 !== annotationsOnPage.length) {
// If not last item in page
this.pdf.selectAnnotations([annotationsOnPage[idx + 1]]);
this.listingService.selectAnnotations([annotationsOnPage[idx + 1]]);
} else if (nextPageIdx < this.displayedPages.length) {
// If not last page
for (let i = nextPageIdx; i < this.displayedPages.length; i++) {
const nextPageAnnotations = this.displayedAnnotations.get(this.displayedPages[i]);
if (nextPageAnnotations) {
this.pdf.selectAnnotations([nextPageAnnotations[0]]);
this.listingService.selectAnnotations([nextPageAnnotations[0]]);
break;
}
}
@ -332,7 +343,7 @@ export class FileWorkloadComponent {
if (idx !== 0) {
// If not first item in page
return this.pdf.selectAnnotations([annotationsOnPage[idx - 1]]);
return this.listingService.selectAnnotations([annotationsOnPage[idx - 1]]);
}
if (pageIdx) {
@ -340,7 +351,7 @@ export class FileWorkloadComponent {
for (let i = previousPageIdx; i >= 0; i--) {
const prevPageAnnotations = this.displayedAnnotations.get(this.displayedPages[i]);
if (prevPageAnnotations) {
this.pdf.selectAnnotations([prevPageAnnotations[prevPageAnnotations.length - 1]]);
this.listingService.selectAnnotations([prevPageAnnotations[prevPageAnnotations.length - 1]]);
break;
}
}
@ -394,7 +405,7 @@ export class FileWorkloadComponent {
this.displayedPages.indexOf(this.activeViewerPage) >= 0 &&
this.activeAnnotations.length > 0
) {
this.pdf.selectAnnotations([this.activeAnnotations[0]]);
this.listingService.selectAnnotations([this.activeAnnotations[0]]);
}
}
@ -405,26 +416,26 @@ export class FileWorkloadComponent {
if (pageIdx !== -1) {
// If active page has annotations
if (pageIdx !== this.displayedPages.length - 1) {
this.selectPage.emit(this.displayedPages[pageIdx + 1]);
this.pdf.navigateTo(this.displayedPages[pageIdx + 1]);
}
} else {
// If active page doesn't have annotations
const nextPage = this._nextPageWithAnnotations();
if (nextPage) {
this.selectPage.emit(nextPage);
this.pdf.navigateTo(nextPage);
}
}
} else {
if (pageIdx !== -1) {
// If active page has annotations
if (pageIdx !== 0) {
this.selectPage.emit(this.displayedPages[pageIdx - 1]);
this.pdf.navigateTo(this.displayedPages[pageIdx - 1]);
}
} else {
// If active page doesn't have annotations
const prevPage = this._prevPageWithAnnotations();
if (prevPage) {
this.selectPage.emit(prevPage);
this.pdf.navigateTo(prevPage);
}
}
}

View File

@ -16,7 +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';
import { PageRotationService } from '../../../pdf-viewer/services/page-rotation.service';
@Component({
selector: 'redaction-page-indicator',
@ -61,7 +61,7 @@ export class PageIndicatorComponent extends AutoUnsubscribe implements OnDestroy
}
ngOnInit() {
this.addSubscription = this.pageRotationService.isRotated(this.number).subscribe(value => {
this.addSubscription = this.pageRotationService.isRotated$(this.number).subscribe(value => {
this.isRotated = value;
this._changeDetectorRef.detectChanges();
});

View File

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

View File

@ -1,578 +0,0 @@
import {
Component,
ElementRef,
EventEmitter,
Inject,
Input,
NgZone,
OnChanges,
OnInit,
Output,
SimpleChanges,
ViewChild,
} from '@angular/core';
import { Dossier, File, IHeaderElement, IManualRedactionEntry } from '@red/domain';
import { Core, WebViewerInstance } from '@pdftron/webviewer';
import { TranslateService } from '@ngx-translate/core';
import {
ManualRedactionEntryType,
ManualRedactionEntryTypes,
ManualRedactionEntryWrapper,
} from '@models/file/manual-redaction-entry.wrapper';
import { AnnotationWrapper } from '@models/file/annotation.wrapper';
import { environment } from '@environments/environment';
import { AnnotationDrawService } from '../../services/annotation-draw.service';
import { AnnotationActionsService } from '../../services/annotation-actions.service';
import { UserPreferenceService } from '@services/user-preference.service';
import { BASE_HREF } from '../../../../tokens';
import { ConfigService } from '@services/config.service';
import { AutoUnsubscribe, ConfirmationDialogInput, CustomError, ErrorService, LoadingService } from '@iqser/common-ui';
import { PdfViewer } from '../../services/pdf-viewer.service';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { toPosition } from '../../utils/pdf-calculation.utils';
import { ViewModeService } from '../../services/view-mode.service';
import { MultiSelectService } from '../../services/multi-select.service';
import { FilePreviewStateService } from '../../services/file-preview-state.service';
import { map, switchMap, tap, withLatestFrom } from 'rxjs/operators';
import { PageRotationService } from '../../services/page-rotation.service';
import { ALLOWED_KEYBOARD_SHORTCUTS, HeaderElements, TextPopups } from '../../utils/constants';
import { FilePreviewDialogService } from '../../services/file-preview-dialog.service';
import { loadCompareDocumentWrapper } from '../../utils/compare-mode.utils';
import { from } from 'rxjs';
import { FileDataService } from '../../services/file-data.service';
import { ViewerHeaderConfigService } from '../../services/viewer-header-config.service';
import { TooltipsService } from '../../services/tooltips.service';
import { ManualRedactionService } from '../../services/manual-redaction.service';
import Tools = Core.Tools;
import TextTool = Tools.TextTool;
import Annotation = Core.Annotations.Annotation;
const DocLoadingError = new CustomError(_('error.file-preview.label'), _('error.file-preview.action'), 'iqser:refresh');
@Component({
selector: 'redaction-pdf-viewer',
templateUrl: './pdf-viewer.component.html',
styleUrls: ['./pdf-viewer.component.scss'],
})
export class PdfViewerComponent extends AutoUnsubscribe implements OnInit, OnChanges {
@Input() dossier: Dossier;
@Input() canPerformActions = false;
@Output() readonly annotationSelected = new EventEmitter<string[]>();
@Output() readonly manualAnnotationRequested = new EventEmitter<ManualRedactionEntryWrapper>();
@Output() readonly pageChanged = new EventEmitter<number>();
@Output() readonly keyUp = new EventEmitter<KeyboardEvent>();
@ViewChild('viewer', { static: true }) viewer: ElementRef;
@ViewChild('compareFileInput', { static: true }) compareFileInput: ElementRef;
instance: WebViewerInstance;
documentViewer: Core.DocumentViewer;
annotationManager: Core.AnnotationManager;
private _selectedText = '';
constructor(
@Inject(BASE_HREF) private readonly _baseHref: string,
private readonly _translateService: TranslateService,
private readonly _manualRedactionService: ManualRedactionService,
private readonly _dialogService: FilePreviewDialogService,
private readonly _ngZone: NgZone,
private readonly _userPreferenceService: UserPreferenceService,
private readonly _annotationDrawService: AnnotationDrawService,
private readonly _annotationActionsService: AnnotationActionsService,
private readonly _configService: ConfigService,
private readonly _loadingService: LoadingService,
private readonly _pageRotationService: PageRotationService,
private readonly _fileDataService: FileDataService,
private readonly _headerConfigService: ViewerHeaderConfigService,
private readonly _tooltipsService: TooltipsService,
private readonly _errorService: ErrorService,
readonly stateService: FilePreviewStateService,
readonly viewModeService: ViewModeService,
readonly multiSelectService: MultiSelectService,
readonly pdfViewer: PdfViewer,
) {
super();
}
async ngOnInit() {
this._setReadyAndInitialState = this._setReadyAndInitialState.bind(this);
await this._loadViewer();
this.addActiveScreenSubscription = this.stateService.blob$
.pipe(
switchMap(blob => from(this.pdfViewer.lockDocument()).pipe(map(() => blob))),
withLatestFrom(this.stateService.file$),
tap(() => this._errorService.clear()),
tap(([blob, file]) => this._loadDocument(blob, file)),
)
.subscribe();
}
ngOnChanges(changes: SimpleChanges): void {
if (!this.instance) {
return;
}
if (changes.canPerformActions) {
this._handleCustomActions();
}
}
uploadFile(files: FileList) {
const fileToCompare = files[0];
this.compareFileInput.nativeElement.value = null;
if (!fileToCompare) {
console.error('No file to compare!');
return;
}
const fileReader = new FileReader();
fileReader.onload = async () => {
const pdfNet = this.instance.Core.PDFNet;
await pdfNet.initialize(environment.licenseKey ? atob(environment.licenseKey) : null);
const compareDocument = await pdfNet.PDFDoc.createFromBuffer(fileReader.result as ArrayBuffer);
const blob = await this.stateService.blob;
const currentDocument = await pdfNet.PDFDoc.createFromBuffer(await blob.arrayBuffer());
const loadCompareDocument = async () => {
this._loadingService.start();
const mergedDocument = await pdfNet.PDFDoc.create();
const file = this.stateService.file;
await loadCompareDocumentWrapper(
currentDocument,
compareDocument,
mergedDocument,
this.instance,
file,
() => {
this.viewModeService.compareMode = true;
},
() => {
this.pdfViewer.navigateToPage(1);
},
this.instance.Core.PDFNet,
);
this._headerConfigService.disable([HeaderElements.COMPARE_BUTTON]);
this._headerConfigService.enable([HeaderElements.CLOSE_COMPARE_BUTTON]);
this._loadingService.stop();
};
const currentDocumentPageCount = await currentDocument.getPageCount();
const compareDocumentPageCount = await compareDocument.getPageCount();
if (currentDocumentPageCount !== compareDocumentPageCount) {
this._dialogService.openDialog(
'confirm',
null,
new ConfirmationDialogInput({
title: _('confirmation-dialog.compare-file.title'),
question: _('confirmation-dialog.compare-file.question'),
translateParams: {
fileName: fileToCompare.name,
currentDocumentPageCount,
compareDocumentPageCount,
},
}),
loadCompareDocument,
);
} else {
await loadCompareDocument();
}
};
fileReader.readAsArrayBuffer(fileToCompare);
}
private async _loadViewer() {
this.instance = await this.pdfViewer.loadViewer(this.viewer.nativeElement as HTMLElement);
this.documentViewer = this.pdfViewer.documentViewer;
this.annotationManager = this.pdfViewer.annotationManager;
this._setSelectionMode();
this._configureElements();
this.pdfViewer.disableHotkeys();
this._configureTextPopup();
this.annotationManager.addEventListener('annotationSelected', (annotations: Annotation[], action) => {
let nextAnnotations: Annotation[];
if (action === 'deselected') {
// Remove deselected annotations from selected list
nextAnnotations = this.annotationManager.getSelectedAnnotations().filter(ann => !annotations.find(a => a.Id === ann.Id));
} else if (!this.multiSelectService.isEnabled) {
// Only choose the last selected annotation, to bypass viewer multi select
nextAnnotations = annotations;
} else {
// Get selected annotations from the manager, no intervention needed
nextAnnotations = this.annotationManager.getSelectedAnnotations();
}
this.annotationSelected.emit(nextAnnotations.map(ann => ann.Id));
if (action === 'deselected') {
return this._toggleRectangleAnnotationAction(true);
}
if (!this.multiSelectService.isEnabled) {
this.pdfViewer.deselectAnnotations(
this._fileDataService.all.filter(wrapper => !nextAnnotations.find(ann => ann.Id === wrapper.id)),
);
}
this.#configureAnnotationSpecificActions(annotations);
this._toggleRectangleAnnotationAction(annotations.length === 1 && annotations[0].ReadOnly);
});
this.annotationManager.addEventListener('annotationChanged', (annotations: Annotation[]) => {
// when a rectangle is drawn,
// it returns one annotation with tool name 'AnnotationCreateRectangle;
// this will auto select rectangle after drawing
if (annotations.length === 1 && annotations[0].ToolName === 'AnnotationCreateRectangle') {
this.annotationManager.selectAnnotations(annotations);
annotations[0].disableRotationControl();
}
});
this.documentViewer.addEventListener('pageNumberUpdated', (pageNumber: number) => {
this.pdfViewer.deselectAllAnnotations();
this._ngZone.run(() => this.pageChanged.emit(pageNumber));
return this._handleCustomActions();
});
this.documentViewer.addEventListener('documentLoaded', this._setReadyAndInitialState);
this.documentViewer.addEventListener('keyUp', ($event: KeyboardEvent) => {
// 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));
$event.preventDefault();
$event.stopPropagation();
}
}
if (!ALLOWED_KEYBOARD_SHORTCUTS.includes($event.key)) {
$event.preventDefault();
$event.stopPropagation();
}
});
this.documentViewer.addEventListener('textSelected', (quads, selectedText, pageNumber: number) => {
this._selectedText = selectedText;
const textActions = [TextPopups.ADD_DICTIONARY, TextPopups.ADD_FALSE_POSITIVE];
const file = this.stateService.file;
if (this.viewModeService.isCompare && pageNumber % 2 === 0) {
this.instance.UI.disableElements(['textPopup']);
} else {
this.instance.UI.enableElements(['textPopup']);
}
if (selectedText.length > 2 && this.canPerformActions && !this.pdfViewer.isCurrentPageExcluded(file)) {
this.instance.UI.enableElements(textActions);
} else {
this.instance.UI.disableElements(textActions);
}
});
this.instance.UI.iframeWindow.addEventListener('visibilityChanged', (event: any) => {
if (event.detail.element === 'searchPanel') {
const inputElement = this.instance.UI.iframeWindow.document.getElementById('SearchPanel__input') as HTMLInputElement;
setTimeout(() => {
inputElement.value = '';
}, 0);
if (!event.detail.isVisible) {
this.documentViewer.clearSearchResults();
}
}
});
}
private _setInitialDisplayMode() {
this.instance.UI.setFitMode('FitPage');
const instanceDisplayMode = this.documentViewer.getDisplayModeManager().getDisplayMode();
instanceDisplayMode.mode = this.viewModeService.isCompare ? 'Facing' : 'Single';
this.documentViewer.getDisplayModeManager().setDisplayMode(instanceDisplayMode);
}
private _convertPath(path: string): string {
return this._baseHref + path;
}
private _setSelectionMode(): void {
const textTool = this.instance.Core.Tools.TextTool as unknown as TextTool;
textTool.SELECTION_MODE = this._configService.values.SELECTION_MODE;
}
private _toggleRectangleAnnotationAction(readonly = false) {
if (!readonly) {
this.instance.UI.enableElements([TextPopups.ADD_RECTANGLE]);
} else {
this.instance.UI.disableElements([TextPopups.ADD_RECTANGLE]);
}
}
private _configureElements() {
this.instance.UI.disableElements([
'pageNavOverlay',
'menuButton',
'selectToolButton',
'textHighlightToolButton',
'textUnderlineToolButton',
'textSquigglyToolButton',
'textStrikeoutToolButton',
'viewControlsButton',
'contextMenuPopup',
'linkButton',
'toggleNotesButton',
'notesPanel',
'thumbnailControl',
'documentControl',
'ribbons',
'toolsHeader',
'rotateClockwiseButton',
'rotateCounterClockwiseButton',
'annotationStyleEditButton',
'annotationGroupButton',
]);
this._headerConfigService.initialize(this.compareFileInput);
const dossierTemplateId = this.dossier.dossierTemplateId;
this.documentViewer.getTool('AnnotationCreateRectangle').setStyles({
StrokeThickness: 2,
StrokeColor: this._annotationDrawService.getAndConvertColor(dossierTemplateId, 'manual'),
FillColor: this._annotationDrawService.getAndConvertColor(dossierTemplateId, 'manual'),
Opacity: 0.6,
});
}
#configureAnnotationSpecificActions(viewerAnnotations: Annotation[]) {
if (!this.canPerformActions) {
if (this.instance.UI.annotationPopup.getItems().length) {
this.instance.UI.annotationPopup.update([]);
}
return;
}
const annotationWrappers: AnnotationWrapper[] = viewerAnnotations.map(va => this._fileDataService.find(va.Id)).filter(va => !!va);
this.instance.UI.annotationPopup.update([]);
if (annotationWrappers.length === 0) {
this._configureRectangleAnnotationPopup(viewerAnnotations[0]);
return;
}
// Add hide action as last item
const allAnnotationsHaveImageAction = annotationWrappers.reduce((acc, next) => acc && next.isImage, true);
if (allAnnotationsHaveImageAction) {
const allAreVisible = viewerAnnotations.reduce((acc, next) => next.isVisible() && acc, true);
this.instance.UI.annotationPopup.add([
{
type: 'actionButton',
img: allAreVisible
? this._convertPath('/assets/icons/general/visibility-off.svg')
: this._convertPath('/assets/icons/general/visibility.svg'),
title: this._translateService.instant(`annotation-actions.${allAreVisible ? 'hide' : 'show'}`),
onClick: () => {
this._ngZone.run(() => {
if (allAreVisible) {
this.annotationManager.hideAnnotations(viewerAnnotations);
} else {
this.annotationManager.showAnnotations(viewerAnnotations);
}
this.annotationManager.deselectAllAnnotations();
this._fileDataService.updateHiddenAnnotations(viewerAnnotations, allAreVisible);
});
},
},
]);
}
const actions = this._annotationActionsService.getViewerAvailableActions();
this.instance.UI.annotationPopup.add(actions);
}
private _configureRectangleAnnotationPopup(annotation: Annotation) {
if (!this.viewModeService.isCompare || annotation.getPageNumber() % 2 === 1) {
this.instance.UI.annotationPopup.add([
{
type: 'actionButton',
dataElement: TextPopups.ADD_RECTANGLE,
img: this._convertPath('/assets/icons/general/pdftron-action-add-redaction.svg'),
title: this.#getTitle(ManualRedactionEntryTypes.REDACTION),
onClick: () => this._addRectangleManualRedaction(),
},
]);
}
}
private _addRectangleManualRedaction() {
const activeAnnotation = this.annotationManager.getSelectedAnnotations()[0];
const activePage = activeAnnotation.getPageNumber();
const quads = [this._annotationDrawService.annotationToQuads(activeAnnotation)];
const manualRedactionEntry = this._getManualRedaction({ [activePage]: quads });
this._cleanUpSelectionAndButtonState();
this.manualAnnotationRequested.emit({ manualRedactionEntry, type: 'REDACTION' });
}
private _cleanUpSelectionAndButtonState() {
this._headerConfigService.disable([HeaderElements.SHAPE_TOOL_GROUP_BUTTON]);
this._headerConfigService.enable([HeaderElements.SHAPE_TOOL_GROUP_BUTTON]);
}
private _configureTextPopup() {
const searchButton = {
type: 'actionButton',
img: this._convertPath('/assets/icons/general/pdftron-action-search.svg'),
title: this._translateService.instant('pdf-viewer.text-popup.actions.search'),
onClick: () => {
const text = this.documentViewer.getSelectedText();
const searchOptions = {
caseSensitive: true, // match case
wholeWord: true, // match whole words only
wildcard: false, // allow using '*' as a wildcard value
regex: false, // string is treated as a regular expression
searchUp: false, // search from the end of the document upwards
ambientString: true, // return ambient string as part of the result
};
this.instance.UI.openElements(['searchPanel']);
setTimeout(() => this.instance.UI.searchTextFull(text, searchOptions), 250);
},
};
const popups: IHeaderElement[] = [searchButton];
// Adding directly to the false-positive dict is only available in dev-mode
if (this._userPreferenceService.areDevFeaturesEnabled) {
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),
});
}
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._manualRedactionService.getTitle(type, this.dossier));
}
private _addManualRedactionOfType(type: ManualRedactionEntryType) {
const selectedQuads: Readonly<Record<string, Core.Math.Quad[]>> = this.documentViewer.getSelectedTextQuads();
const text = this.documentViewer.getSelectedText();
const manualRedactionEntry = this._getManualRedaction(selectedQuads, text, true);
this.manualAnnotationRequested.emit({ manualRedactionEntry, type });
}
private _handleCustomActions() {
const textPopupsToToggle = [TextPopups.ADD_REDACTION, TextPopups.ADD_RECTANGLE, TextPopups.ADD_FALSE_POSITIVE];
const headerItemsToToggle = [
HeaderElements.SHAPE_TOOL_GROUP_BUTTON,
HeaderElements.ROTATE_LEFT_BUTTON,
HeaderElements.ROTATE_RIGHT_BUTTON,
];
const isCurrentPageExcluded = this.pdfViewer.isCurrentPageExcluded(this.stateService.file);
if (this.canPerformActions && !isCurrentPageExcluded) {
try {
this.instance.UI.enableTools(['AnnotationCreateRectangle']);
} catch (e) {
// happens
}
this.instance.UI.enableElements(textPopupsToToggle);
this._headerConfigService.enable(headerItemsToToggle);
if (this._selectedText.length > 2) {
this.instance.UI.enableElements([TextPopups.ADD_DICTIONARY, TextPopups.ADD_FALSE_POSITIVE]);
}
return;
}
let textPopupElementsToDisable = [...textPopupsToToggle];
let headerElementsToDisable = [...headerItemsToToggle];
if (isCurrentPageExcluded) {
const allowedActionsWhenPageExcluded: string[] = [
TextPopups.ADD_RECTANGLE,
TextPopups.ADD_REDACTION,
HeaderElements.SHAPE_TOOL_GROUP_BUTTON,
];
textPopupElementsToDisable = textPopupElementsToDisable.filter(element => !allowedActionsWhenPageExcluded.includes(element));
headerElementsToDisable = headerElementsToDisable.filter(element => !allowedActionsWhenPageExcluded.includes(element));
} else {
this.instance.UI.disableTools(['AnnotationCreateRectangle']);
}
this.instance.UI.disableElements(textPopupElementsToDisable);
this._headerConfigService.disable(headerElementsToDisable);
}
private _getManualRedaction(
quads: Readonly<Record<string, Core.Math.Quad[]>>,
text?: string,
convertQuads = false,
): IManualRedactionEntry {
const entry: IManualRedactionEntry = { positions: [] };
for (const key of Object.keys(quads)) {
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.pdfViewer.translateQuad(page, quad) : quad));
}
}
entry.value = text;
entry.rectangle = !text;
return entry;
}
private async _loadDocument(blob: Blob, file: File) {
const onError = () => {
this._loadingService.stop();
this._errorService.set(DocLoadingError);
this.stateService.reloadBlob();
};
const pdfNet = this.instance.Core.PDFNet;
await pdfNet.initialize(environment.licenseKey ? atob(environment.licenseKey) : null);
const document = await pdfNet.PDFDoc.createFromBuffer(await blob.arrayBuffer());
await document.flattenAnnotations(false);
this.instance.UI.loadDocument(document, { filename: file?.filename + '.pdf' ?? 'document.pdf', onError });
this._pageRotationService.clearRotationsHideActions();
}
private _setReadyAndInitialState() {
this._ngZone.run(() => {
this.pdfViewer.emitDocumentLoaded();
this._setInitialDisplayMode();
this._tooltipsService.updateTooltipsVisibility();
});
}
}

View File

@ -4,20 +4,16 @@ import { MultiSelectService } from './services/multi-select.service';
import { DocumentInfoService } from './services/document-info.service';
import { CommentingService } from './services/commenting.service';
import { SkippedService } from './services/skipped.service';
import { AnnotationDrawService } from './services/annotation-draw.service';
import { AnnotationActionsService } from './services/annotation-actions.service';
import { FilePreviewStateService } from './services/file-preview-state.service';
import { AnnotationReferencesService } from './services/annotation-references.service';
import { EntitiesService, FilterService, ListingService, SearchService } from '@iqser/common-ui';
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';
import { FileDataService } from './services/file-data.service';
import { ViewerHeaderConfigService } from './services/viewer-header-config.service';
import { TooltipsService } from './services/tooltips.service';
import { AnnotationsListingService } from './services/annotations-listing.service';
import { StampService } from './services/stamp.service';
import { PdfProxyService } from './services/pdf-proxy.service';
export const filePreviewScreenProviders = [
FilterService,
@ -27,19 +23,16 @@ export const filePreviewScreenProviders = [
DocumentInfoService,
CommentingService,
SkippedService,
AnnotationDrawService,
AnnotationActionsService,
FilePreviewStateService,
AnnotationReferencesService,
PageRotationService,
PdfViewer,
AnnotationProcessingService,
FileDataService,
{ provide: EntitiesService, useExisting: FileDataService },
dossiersServiceProvider,
ViewerHeaderConfigService,
TooltipsService,
{ provide: ListingService, useClass: AnnotationsListingService },
AnnotationsListingService,
{ provide: ListingService, useExisting: AnnotationsListingService },
SearchService,
StampService,
PdfProxyService,
];

View File

@ -1,5 +1,3 @@
<ng-container *ngIf="state.dossierFileChange$ | async"></ng-container>
<ng-container *ngIf="state.dossier$ | async as dossier">
<section *ngIf="file$ | async as file" [class.fullscreen]="fullScreen">
<div class="page-header">
@ -63,16 +61,7 @@
<div class="content-inner">
<div class="content-container">
<redaction-pdf-viewer
(annotationSelected)="handleAnnotationSelected($event)"
(keyUp)="handleKeyEvent($event); workloadComponent.handleKeyEvent($event)"
(manualAnnotationRequested)="openManualAnnotationDialog($event)"
(pageChanged)="viewerPageChanged($event)"
*ngIf="displayPdfViewer"
[canPerformActions]="canPerformAnnotationActions$ | async"
[class.hidden]="!ready"
[dossier]="dossier"
></redaction-pdf-viewer>
<!-- Here comes PDF Viewer-->
</div>
<div class="right-container">
@ -86,7 +75,6 @@
<redaction-document-info *ngIf="documentInfoService.shown$ | async"></redaction-document-info>
<redaction-file-workload
(selectPage)="selectPage($event)"
*ngIf="!file.excluded"
[activeViewerPage]="pdf.currentPage$ | async"
[annotationActionsTemplate]="annotationActionsTemplate"
@ -101,7 +89,7 @@
<ng-template #annotationActionsTemplate let-annotation="annotation">
<redaction-annotation-actions
[annotations]="[annotation]"
[canPerformAnnotationActions]="canPerformAnnotationActions$ | async"
[canPerformAnnotationActions]="pdfProxyService.canPerformAnnotationActions$ | async"
[iqserHelpMode]="getActionsHelpModeKey(annotation)"
[scrollableParentView]="scrollableParentView"
></redaction-annotation-actions>

View File

@ -3,12 +3,12 @@ import { ActivatedRoute, ActivatedRouteSnapshot, NavigationExtras, Router } from
import { Core } from '@pdftron/webviewer';
import {
AutoUnsubscribe,
bool,
CircleButtonTypes,
CustomError,
Debounce,
ErrorService,
FilterService,
ListingService,
LoadingService,
NestedFilter,
OnAttach,
@ -16,22 +16,20 @@ import {
processFilters,
ScrollableParentView,
ScrollableParentViews,
shareDistinctLast,
} from '@iqser/common-ui';
import { MatDialogRef, MatDialogState } from '@angular/material/dialog';
import { ManualRedactionEntryWrapper } from '@models/file/manual-redaction-entry.wrapper';
import { AnnotationWrapper } from '@models/file/annotation.wrapper';
import { AnnotationDrawService } from './services/annotation-draw.service';
import { AnnotationDrawService } from '../pdf-viewer/services/annotation-draw.service';
import { AnnotationProcessingService } from './services/annotation-processing.service';
import { File, ViewMode, ViewModes } from '@red/domain';
import { PermissionsService } from '@services/permissions.service';
import { combineLatest, firstValueFrom, Observable, of, pairwise } from 'rxjs';
import { combineLatest, firstValueFrom, from, of, pairwise } from 'rxjs';
import { UserPreferenceService } from '@services/user-preference.service';
import { download, handleFilterDelta } from '../../utils';
import { FileWorkloadComponent } from './components/file-workload/file-workload.component';
import { FilesService } from '@services/files/files.service';
import { FileManagementService } from '@services/files/file-management.service';
import { catchError, map, startWith, switchMap, tap } from 'rxjs/operators';
import { catchError, filter, map, startWith, switchMap, tap } from 'rxjs/operators';
import { FilesMapService } from '@services/files/files-map.service';
import { ExcludedPagesService } from './services/excluded-pages.service';
import { ViewModeService } from './services/view-mode.service';
@ -43,16 +41,25 @@ import { FilePreviewStateService } from './services/file-preview-state.service';
import { filePreviewScreenProviders } from './file-preview-providers';
import { ManualRedactionService } from './services/manual-redaction.service';
import { DossiersService } from '@services/dossiers/dossiers.service';
import { PageRotationService } from './services/page-rotation.service';
import { PageRotationService } from '../pdf-viewer/services/page-rotation.service';
import { ComponentCanDeactivate } from '@guards/can-deactivate.guard';
import { PdfViewer } from './services/pdf-viewer.service';
import { FilePreviewDialogService } from './services/file-preview-dialog.service';
import { FileDataService } from './services/file-data.service';
import { ActionsHelpModeKeys, ALL_HOTKEYS } from './utils/constants';
import { ActionsHelpModeKeys, ALL_HOTKEYS, TextPopups } from './utils/constants';
import { NGXLogger } from 'ngx-logger';
import { StampService } from './services/stamp.service';
import { PdfViewer } from '../pdf-viewer/services/pdf-viewer.service';
import { REDAnnotationManager } from '../pdf-viewer/services/annotation-manager.service';
import { ViewerHeaderService } from '../pdf-viewer/services/viewer-header.service';
import { ROTATION_ACTION_BUTTONS } from '../pdf-viewer/utils/constants';
import { SkippedService } from './services/skipped.service';
import { REDDocumentViewer } from '../pdf-viewer/services/document-viewer.service';
import { AnnotationsListingService } from './services/annotations-listing.service';
import { PdfProxyService } from './services/pdf-proxy.service';
import Annotation = Core.Annotations.Annotation;
const textActions = [TextPopups.ADD_DICTIONARY, TextPopups.ADD_FALSE_POSITIVE];
@Component({
templateUrl: './file-preview-screen.component.html',
styleUrls: ['./file-preview-screen.component.scss'],
@ -63,14 +70,9 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
dialogRef: MatDialogRef<unknown>;
fullScreen = false;
displayPdfViewer = false;
readonly canPerformAnnotationActions$: Observable<boolean>;
readonly fileId = this.state.fileId;
readonly dossierId = this.state.dossierId;
readonly file$ = this.state.file$.pipe(tap(file => this._fileDataService.loadAnnotations(file)));
ready = false;
@ViewChild(FileWorkloadComponent) readonly workloadComponent: FileWorkloadComponent;
private _lastPage: string;
@ViewChild('annotationFilterTemplate', {
read: TemplateRef,
static: false,
@ -81,7 +83,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
readonly pdf: PdfViewer,
readonly documentInfoService: DocumentInfoService,
readonly state: FilePreviewStateService,
readonly listingService: ListingService<AnnotationWrapper>,
readonly listingService: AnnotationsListingService,
readonly permissionsService: PermissionsService,
readonly multiSelectService: MultiSelectService,
readonly excludedPagesService: ExcludedPagesService,
@ -90,25 +92,28 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
private readonly _ngZone: NgZone,
private readonly _logger: NGXLogger,
private readonly _filesService: FilesService,
private readonly _annotationManager: REDAnnotationManager,
private readonly _errorService: ErrorService,
private readonly _filterService: FilterService,
private readonly _activatedRoute: ActivatedRoute,
private readonly _loadingService: LoadingService,
private readonly _filesMapService: FilesMapService,
private readonly _dossiersService: DossiersService,
private readonly _skippedService: SkippedService,
private readonly _fileDataService: FileDataService,
private readonly _viewModeService: ViewModeService,
private readonly _documentViewer: REDDocumentViewer,
private readonly _changeDetectorRef: ChangeDetectorRef,
private readonly _dialogService: FilePreviewDialogService,
private readonly _pageRotationService: PageRotationService,
private readonly _viewerHeaderService: ViewerHeaderService,
private readonly _annotationDrawService: AnnotationDrawService,
private readonly _annotationProcessingService: AnnotationProcessingService,
private readonly _stampService: StampService,
readonly pdfProxyService: PdfProxyService,
private readonly _injector: Injector,
) {
super();
this.canPerformAnnotationActions$ = this._canPerformAnnotationActions$;
document.documentElement.addEventListener('fullscreenchange', () => {
if (!document.fullscreenElement) {
this.fullScreen = false;
@ -117,34 +122,43 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
}
get changed() {
return this._pageRotationService.hasRotations();
return this._pageRotationService.hasRotations;
}
get scrollableParentView(): ScrollableParentView {
return ScrollableParentViews.ANNOTATIONS_LIST;
}
private get _canPerformAnnotationActions$() {
const viewMode$ = this._viewModeService.viewMode$.pipe(tap(() => this.#deactivateMultiSelect()));
get #textSelected$() {
const textSelected$ = combineLatest([
this._documentViewer.textSelected$,
this.pdfProxyService.canPerformAnnotationActions$,
this.state.file$,
]);
return combineLatest([this.state.file$, this.state.dossier$, viewMode$, this._viewModeService.compareMode$]).pipe(
map(
([file, dossier, viewMode]) =>
this.permissionsService.canPerformAnnotationActions(file, dossier) && viewMode === 'STANDARD',
),
shareDistinctLast(),
return textSelected$.pipe(
tap(([selectedText, canPerformActions, file]) => {
const isCurrentPageExcluded = file.isPageExcluded(this.pdf.currentPage);
if (selectedText.length > 2 && canPerformActions && !isCurrentPageExcluded) {
this.pdf.enable(textActions);
} else {
this.pdf.disable(textActions);
}
}),
);
}
async save() {
await this._pageRotationService.applyRotation();
this._viewerHeaderService.disable(ROTATION_ACTION_BUTTONS);
}
async updateViewMode(): Promise<void> {
this._logger.info(`[PDF] Update ${this._viewModeService.viewMode} view mode`);
const annotations = this.pdf.getAnnotations(a => a.getCustomData('redact-manager'));
const redactions = annotations.filter(a => a.getCustomData('redaction'));
const annotations = this._annotationManager.get(a => bool(a.getCustomData('redact-manager')));
const redactions = annotations.filter(a => bool(a.getCustomData('redaction')));
switch (this._viewModeService.viewMode) {
case 'STANDARD': {
@ -152,36 +166,36 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
const wrappers = await this._fileDataService.annotations;
const ocrAnnotationIds = wrappers.filter(a => a.isOCR).map(a => a.id);
const standardEntries = annotations
.filter(a => a.getCustomData('changeLogRemoved') === 'false')
.filter(a => !bool(a.getCustomData('changeLogRemoved')))
.filter(a => !ocrAnnotationIds.includes(a.Id));
const nonStandardEntries = annotations.filter(a => a.getCustomData('changeLogRemoved') === 'true');
const nonStandardEntries = annotations.filter(a => bool(a.getCustomData('changeLogRemoved')));
this._setAnnotationsOpacity(standardEntries, true);
this.pdf.showAnnotations(standardEntries);
this.pdf.hideAnnotations(nonStandardEntries);
this._annotationManager.show(standardEntries);
this._annotationManager.hide(nonStandardEntries);
break;
}
case 'DELTA': {
const changeLogEntries = annotations.filter(a => a.getCustomData('changeLog') === 'true');
const nonChangeLogEntries = annotations.filter(a => a.getCustomData('changeLog') === 'false');
const changeLogEntries = annotations.filter(a => bool(a.getCustomData('changeLog')));
const nonChangeLogEntries = annotations.filter(a => !bool(a.getCustomData('changeLog')));
this._setAnnotationsColor(redactions, 'annotationColor');
this._setAnnotationsOpacity(changeLogEntries, true);
this.pdf.showAnnotations(changeLogEntries);
this.pdf.hideAnnotations(nonChangeLogEntries);
this._annotationManager.show(changeLogEntries);
this._annotationManager.hide(nonChangeLogEntries);
break;
}
case 'REDACTED': {
const nonRedactionEntries = annotations.filter(a => a.getCustomData('redaction') === 'false');
const nonRedactionEntries = annotations.filter(a => !bool(a.getCustomData('redaction')));
this._setAnnotationsOpacity(redactions);
this._setAnnotationsColor(redactions, 'redactionColor');
this.pdf.showAnnotations(redactions);
this.pdf.hideAnnotations(nonRedactionEntries);
this._annotationManager.show(redactions);
this._annotationManager.hide(nonRedactionEntries);
break;
}
case 'TEXT_HIGHLIGHTS': {
this._loadingService.start();
this.pdf.hideAnnotations(annotations);
this._annotationManager.hide(annotations);
const highlights = await this._fileDataService.loadTextHighlights();
await this._annotationDrawService.draw(highlights);
await this._annotationDrawService.draw(highlights, this.state.dossierTemplateId, this._skippedService.hideSkipped);
this._loadingService.stop();
}
}
@ -190,24 +204,23 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
this.#rebuildFilters();
}
ngOnDetach(): void {
this._pageRotationService.clearRotations();
this.displayPdfViewer = false;
ngOnDetach() {
this._viewerHeaderService.resetCompareButtons();
super.ngOnDetach();
this._changeDetectorRef.markForCheck();
}
async ngOnAttach(previousRoute: ActivatedRouteSnapshot): Promise<void> {
async ngOnAttach(previousRoute: ActivatedRouteSnapshot) {
if (!this.state.file.canBeOpened) {
return this._navigateToDossier();
}
this._viewModeService.compareMode = false;
this._viewModeService.switchToStandard();
await this.ngOnInit();
await this._fileDataService.loadRedactionLog();
this._lastPage = previousRoute.queryParams.page;
this._viewerHeaderService.updateElements();
await this.#updateQueryParamsPage(Number(previousRoute.queryParams.page ?? '1'));
this._changeDetectorRef.markForCheck();
}
@ -218,7 +231,6 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
return this._handleDeletedFile();
}
this.ready = false;
this._loadingService.start();
await this.userPreferenceService.saveLastOpenedFileForDossier(this.dossierId, this.fileId);
this._subscribeToFileUpdates();
@ -229,17 +241,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
await firstValueFrom(reanalyzeFiles);
}
this.displayPdfViewer = true;
}
handleAnnotationSelected(annotationIds: string[]) {
this.listingService.setSelected(annotationIds.map(id => this._fileDataService.find(id)).filter(ann => ann !== undefined));
this._changeDetectorRef.markForCheck();
}
selectPage(pageNumber: number) {
this.pdf.navigateToPage(pageNumber);
this._lastPage = pageNumber.toString();
this.pdfProxyService.loadViewer();
}
openManualAnnotationDialog(manualRedactionEntryWrapper: ManualRedactionEntryWrapper) {
@ -251,9 +253,9 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
null,
{ manualRedactionEntryWrapper, dossierId: this.dossierId, file },
(wrappers: ManualRedactionEntryWrapper[]) => {
const selectedAnnotations = this.pdf.annotationManager.getSelectedAnnotations();
const selectedAnnotations = this._annotationManager.selected;
if (selectedAnnotations.length > 0) {
this.pdf.deleteAnnotations([selectedAnnotations[0].Id]);
this._annotationManager.delete([selectedAnnotations[0].Id]);
}
const manualRedactionService = this._injector.get(ManualRedactionService);
const add$ = manualRedactionService.addAnnotation(
@ -311,15 +313,8 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
@Debounce(100)
async viewerReady() {
this.ready = true;
this._setExcludedPageStyles();
this.pdf.documentViewer.addEventListener('pageComplete', () => {
this._setExcludedPageStyles();
});
// Go to initial page from query params
const pageNumber: string = this._lastPage || this._activatedRoute.snapshot.queryParams.page;
const pageNumber: string = this._activatedRoute.snapshot.queryParams.page;
if (pageNumber) {
const file = this.state.file;
let page = parseInt(pageNumber, 10);
@ -331,7 +326,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
page = file.numberOfPages;
}
this.selectPage(page);
this.pdf.navigateTo(page);
}
this._loadingService.stop();
@ -356,12 +351,23 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
}
loadAnnotations() {
const documentLoaded$ = this.pdf.documentLoaded$.pipe(tap(() => this.viewerReady()));
const documentLoaded$ = this._documentViewer.loaded$.pipe(
tap(loaded => {
if (!loaded) {
return;
}
this._pageRotationService.clearRotations();
this._viewerHeaderService.disable(ROTATION_ACTION_BUTTONS);
return this.viewerReady();
}),
);
const currentPageAnnotations$ = combineLatest([this.pdf.currentPage$, this._fileDataService.annotations$]).pipe(
map(([page, annotations]) => annotations.filter(annotation => annotation.pageNumber === page)),
);
let start;
return combineLatest([currentPageAnnotations$, documentLoaded$]).pipe(
filter(([, loaded]) => loaded),
tap(() => (start = new Date().getTime())),
map(([annotations]) => annotations),
startWith([] as AnnotationWrapper[]),
@ -382,13 +388,13 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
}
this._logger.info('[ANNOTATIONS] To delete: ', annotationsToDelete);
this.pdf.deleteAnnotations(annotationsToDelete.map(annotation => annotation.id));
this._annotationManager.delete(annotationsToDelete);
}
drawChangedAnnotations(oldAnnotations: AnnotationWrapper[], newAnnotations: AnnotationWrapper[]) {
let annotationsToDraw: readonly AnnotationWrapper[];
const annotationsList = this.pdf.annotationManager.getAnnotationsList();
const ann = annotationsList.map(a => oldAnnotations.find(oldAnnotation => oldAnnotation.id === a.Id));
const annotations = this._annotationManager.annotations;
const ann = annotations.map(a => oldAnnotations.some(oldAnnotation => oldAnnotation.id === a.Id));
const hasAnnotations = ann.filter(a => !!a).length > 0;
if (hasAnnotations) {
@ -402,7 +408,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
}
this._logger.info('[ANNOTATIONS] To draw: ', annotationsToDraw);
this.pdf.deleteAnnotations(annotationsToDraw.map(a => a.annotationId));
this._annotationManager.delete(annotationsToDraw);
return this._cleanupAndRedrawAnnotations(annotationsToDraw);
}
@ -439,7 +445,6 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
}
async #updateQueryParamsPage(page: number): Promise<void> {
// Add current page in URL query params
const extras: NavigationExtras = {
queryParams: { page },
queryParamsHandling: 'merge',
@ -476,19 +481,15 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
});
}
#deactivateMultiSelect() {
this.multiSelectService.deactivate();
this.pdf.deselectAllAnnotations();
this.handleAnnotationSelected([]);
}
private _setExcludedPageStyles() {
const file = this._filesMapService.get(this.dossierId, this.fileId);
setTimeout(() => {
const iframeDoc = this.pdf.UI.iframeWindow.document;
const pageContainer = iframeDoc.getElementById(`pageWidgetContainer${this.pdf.currentPage}`);
const iframeDoc = this.pdf.instance.UI.iframeWindow.document;
const currentPage = this.pdf.currentPage;
const elementId = `pageWidgetContainer${currentPage}`;
const pageContainer = iframeDoc.getElementById(elementId);
if (pageContainer) {
if (file.excludedPages.includes(this.pdf.currentPage)) {
if (file.excludedPages.includes(currentPage)) {
pageContainer.classList.add('excluded-page');
} else {
pageContainer.classList.remove('excluded-page');
@ -519,6 +520,34 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
}),
)
.subscribe();
this.addActiveScreenSubscription = this._documentViewer.pageComplete$.subscribe(() => {
this._setExcludedPageStyles();
});
this.addActiveScreenSubscription = this._documentViewer.keyUp$.subscribe($event => {
this.handleKeyEvent($event);
});
this.addActiveScreenSubscription = this.#textSelected$.subscribe();
this.addActiveScreenSubscription = this.state.dossierFileChange$.subscribe();
this.addActiveScreenSubscription = this.state.blob$
.pipe(
switchMap(blob => from(this._documentViewer.lock()).pipe(map(() => blob))),
tap(() => this._errorService.clear()),
tap(blob => this.pdf.loadDocument(blob, this.state.file)),
)
.subscribe();
this.addActiveScreenSubscription = this.pdfProxyService.manualAnnotationRequested$.subscribe($event => {
this.openManualAnnotationDialog($event);
});
this.addActiveScreenSubscription = this.pdfProxyService.pageChanged$.subscribe(page =>
this._ngZone.run(() => this.viewerPageChanged(page)),
);
this.addActiveScreenSubscription = this.pdfProxyService.annotationSelected$.subscribe();
}
private _handleDeletedDossier(): void {
@ -536,7 +565,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
_('error.deleted-entity.file.action'),
'iqser:expand',
null,
this._navigateToDossier.bind(this),
() => this._navigateToDossier(),
);
this._errorService.set(error);
}
@ -551,7 +580,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
this._handleDeltaAnnotationFilters(currentFilters, this._fileDataService.all);
}
await this._annotationDrawService.draw(newAnnotations);
await this._annotationDrawService.draw(newAnnotations, this.state.dossierTemplateId, this._skippedService.hideSkipped);
this._logger.info(`[ANNOTATIONS] Redraw time: ${new Date().getTime() - startTime} ms for ${newAnnotations.length} annotations`);
}
@ -596,6 +625,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
}
private _navigateToDossier() {
this._router.navigate([this._dossiersService.find(this.dossierId)?.routerLink]);
this._logger.info('Navigating to ', this.state.dossier.dossierName);
return this._router.navigate([this.state.dossier.routerLink]);
}
}

View File

@ -11,7 +11,6 @@ import { AnnotationDetailsComponent } from './components/annotation-details/anno
import { AnnotationsListComponent } from './components/annotations-list/annotations-list.component';
import { PageIndicatorComponent } from './components/page-indicator/page-indicator.component';
import { PageExclusionComponent } from './components/page-exclusion/page-exclusion.component';
import { PdfViewerComponent } from './components/pdf-viewer/pdf-viewer.component';
import { AnnotationActionsComponent } from './components/annotation-actions/annotation-actions.component';
import { CommentsComponent } from './components/comments/comments.component';
import { DocumentInfoComponent } from './components/document-info/document-info.component';
@ -39,6 +38,7 @@ import { ManualRedactionService } from './services/manual-redaction.service';
import { AnnotationWrapperComponent } from './components/annotation-wrapper/annotation-wrapper.component';
import { AnnotationReferenceComponent } from './components/annotation-reference/annotation-reference.component';
import { ImportRedactionsDialogComponent } from './dialogs/import-redactions-dialog/import-redactions-dialog';
import { DocumentUnloadedGuard } from './services/document-unloaded.guard';
const routes: Routes = [
{
@ -46,7 +46,7 @@ const routes: Routes = [
component: FilePreviewScreenComponent,
pathMatch: 'full',
data: { reuse: true },
canDeactivate: [PendingChangesGuard],
canDeactivate: [PendingChangesGuard, DocumentUnloadedGuard],
},
];
@ -70,7 +70,6 @@ const components = [
AnnotationsListComponent,
PageIndicatorComponent,
PageExclusionComponent,
PdfViewerComponent,
AnnotationActionsComponent,
CommentsComponent,
DocumentInfoComponent,
@ -97,6 +96,6 @@ const components = [
OverlayModule,
ColorPickerModule,
],
providers: [FilePreviewDialogService, ManualRedactionService],
providers: [FilePreviewDialogService, ManualRedactionService, DocumentUnloadedGuard],
})
export class FilePreviewModule {}

View File

@ -4,7 +4,7 @@ import { ManualRedactionService } from './manual-redaction.service';
import { AnnotationWrapper } from '@models/file/annotation.wrapper';
import { Observable } from 'rxjs';
import { TranslateService } from '@ngx-translate/core';
import { getFirstRelevantTextPart, translateQuads } from '../../../utils';
import { getFirstRelevantTextPart } from '../../../utils';
import { AnnotationPermissions } from '@models/file/annotation.permissions';
import { BASE_HREF } from '../../../tokens';
import { UserService } from '@services/user.service';
@ -13,6 +13,7 @@ import {
DictionaryEntryTypes,
Dossier,
IAddRedactionRequest,
IHeaderElement,
ILegalBasisChangeRequest,
IRecategorizationRequest,
IRectangle,
@ -20,22 +21,25 @@ import {
TextHighlightOperation,
} from '@red/domain';
import { toPosition } from '../utils/pdf-calculation.utils';
import { AnnotationDrawService } from './annotation-draw.service';
import { AnnotationDrawService } from '../../pdf-viewer/services/annotation-draw.service';
import { ActiveDossiersService } from '@services/dossiers/active-dossiers.service';
import {
AcceptRecommendationData,
AcceptRecommendationDialogComponent,
AcceptRecommendationReturnType,
} from '../dialogs/accept-recommendation-dialog/accept-recommendation-dialog.component';
import { defaultDialogConfig, List, ListingService } from '@iqser/common-ui';
import { defaultDialogConfig, List } from '@iqser/common-ui';
import { filter } from 'rxjs/operators';
import { MatDialog } from '@angular/material/dialog';
import { FilePreviewStateService } from './file-preview-state.service';
import { PdfViewer } from './pdf-viewer.service';
import { FilePreviewDialogService } from './file-preview-dialog.service';
import { DictionariesMapService } from '@services/entity-services/dictionaries-map.service';
import { FileDataService } from './file-data.service';
import Quad = Core.Math.Quad;
import { PdfViewer } from '../../pdf-viewer/services/pdf-viewer.service';
import { REDAnnotationManager } from '../../pdf-viewer/services/annotation-manager.service';
import { SkippedService } from './skipped.service';
import { REDDocumentViewer } from '../../pdf-viewer/services/document-viewer.service';
import { AnnotationsListingService } from './annotations-listing.service';
@Injectable()
export class AnnotationActionsService {
@ -49,12 +53,15 @@ export class AnnotationActionsService {
private readonly _dialogService: FilePreviewDialogService,
private readonly _dialog: MatDialog,
private readonly _pdf: PdfViewer,
private readonly _documentViewer: REDDocumentViewer,
private readonly _annotationManager: REDAnnotationManager,
private readonly _annotationDrawService: AnnotationDrawService,
private readonly _activeDossiersService: ActiveDossiersService,
private readonly _dictionariesMapService: DictionariesMapService,
private readonly _state: FilePreviewStateService,
private readonly _fileDataService: FileDataService,
private readonly _listingService: ListingService<AnnotationWrapper>,
private readonly _skippedService: SkippedService,
private readonly _listingService: AnnotationsListingService,
) {}
private get _dossier(): Dossier {
@ -191,11 +198,10 @@ export class AnnotationActionsService {
});
}
getViewerAvailableActions(): Record<string, unknown>[] {
getViewerAvailableActions(annotations: AnnotationWrapper[]): IHeaderElement[] {
const dossier = this._state.dossier;
const annotations = this._listingService.selected;
const availableActions = [];
const availableActions: IHeaderElement[] = [];
const annotationPermissions = annotations.map(annotation => ({
annotation,
permissions: AnnotationPermissions.forUser(
@ -395,21 +401,20 @@ export class AnnotationActionsService {
annotationWrapper.resizing = true;
const viewerAnnotation = this._pdf.annotationManager.getAnnotationById(annotationWrapper.id);
const viewerAnnotation = this._annotationManager.get(annotationWrapper);
if (annotationWrapper.rectangle || annotationWrapper.imported || annotationWrapper.isImage) {
this._pdf.deleteAnnotations([annotationWrapper.id]);
this._annotationManager.delete(annotationWrapper);
const rectangleAnnotation = this.#generateRectangle(annotationWrapper);
this._pdf.annotationManager.addAnnotation(rectangleAnnotation, { imported: true });
await this._pdf.annotationManager.drawAnnotationsFromList([rectangleAnnotation]);
this._pdf.annotationManager.selectAnnotation(rectangleAnnotation);
await this._annotationManager.add(rectangleAnnotation);
this._annotationManager.select(rectangleAnnotation);
return;
}
viewerAnnotation.ReadOnly = false;
viewerAnnotation.Hidden = false;
viewerAnnotation.disableRotationControl();
this._pdf.annotationManager.redrawAnnotation(viewerAnnotation);
this._pdf.annotationManager.selectAnnotation(viewerAnnotation);
this._annotationManager.redraw(viewerAnnotation);
this._annotationManager.select(viewerAnnotation);
}
async acceptResize($event: MouseEvent, annotation: AnnotationWrapper): Promise<void> {
@ -417,7 +422,7 @@ export class AnnotationActionsService {
const textAndPositions = await this._extractTextAndPositions(annotation.id);
const text = annotation.value === 'Rectangle' ? 'Rectangle' : annotation.isImage ? 'Image' : textAndPositions.text;
const data = { annotation, text };
this._dialogService.openDialog('resizeAnnotation', $event, data, async (result: { comment: string; updateDictionary: boolean }) => {
this._dialogService.openDialog('resizeAnnotation', $event, data, (result: { comment: string; updateDictionary: boolean }) => {
const resizeRequest: IResizeRequest = {
annotationId: annotation.id,
comment: result.comment,
@ -435,9 +440,9 @@ export class AnnotationActionsService {
annotationWrapper.resizing = false;
this._pdf.deleteAnnotations([annotationWrapper.id]);
await this._annotationDrawService.draw([annotationWrapper]);
this._pdf.annotationManager.deselectAllAnnotations();
this._annotationManager.delete(annotationWrapper);
await this._annotationDrawService.draw([annotationWrapper], this._state.dossierTemplateId, this._skippedService.hideSkipped);
this._annotationManager.deselect();
await this._fileDataService.annotationsChanged();
}
@ -461,8 +466,8 @@ export class AnnotationActionsService {
}
#generateRectangle(annotationWrapper: AnnotationWrapper) {
const annotation = new this._pdf.Annotations.RectangleAnnotation();
const pageHeight = this._pdf.documentViewer.getPageHeight(annotationWrapper.pageNumber);
const annotation = this._pdf.rectangle();
const pageHeight = this._documentViewer.getHeight(annotationWrapper.pageNumber);
const rectangle: IRectangle = annotationWrapper.positions[0];
annotation.PageNumber = annotationWrapper.pageNumber;
annotation.X = rectangle.topLeft.x;
@ -472,7 +477,12 @@ export class AnnotationActionsService {
annotation.ReadOnly = false;
annotation.Hidden = false;
annotation.disableRotationControl();
annotation.FillColor = this._annotationDrawService.getAndConvertColor(annotationWrapper.superType, annotationWrapper.type);
const dossierTemplateId = this._state.dossierTemplateId;
annotation.FillColor = this._annotationDrawService.getAndConvertColor(
annotationWrapper.superType,
dossierTemplateId,
annotationWrapper.type,
);
annotation.StrokeColor = annotation.FillColor;
annotation.Opacity = 0.6;
annotation.StrokeThickness = 1;
@ -527,18 +537,18 @@ export class AnnotationActionsService {
}
private async _extractTextAndPositions(annotationId: string) {
const viewerAnnotation = this._pdf.annotationManager.getAnnotationById(annotationId);
const viewerAnnotation = this._annotationManager.get(annotationId);
const document = await this._pdf.documentViewer.getDocument().getPDFDoc();
const document = await this._documentViewer.PDFDoc;
const page = await document.getPage(viewerAnnotation.getPageNumber());
if (viewerAnnotation instanceof this._pdf.Annotations.TextHighlightAnnotation) {
if (this._pdf.isTextHighlight(viewerAnnotation)) {
const words = [];
const rectangles: IRectangle[] = [];
for (const quad of viewerAnnotation.Quads) {
const rect = toPosition(
viewerAnnotation.getPageNumber(),
this._pdf.documentViewer.getPageHeight(viewerAnnotation.getPageNumber()),
this._translateQuads(viewerAnnotation.getPageNumber(), quad),
this._documentViewer.getHeight(viewerAnnotation.getPageNumber()),
this._pdf.translateQuad(viewerAnnotation.getPageNumber(), quad),
);
rectangles.push(rect);
@ -548,7 +558,7 @@ export class AnnotationActionsService {
*/
const percentHeightOffset = rect.height / 10;
const pdfNetRect = new this._pdf.instance.Core.PDFNet.Rect(
const pdfNetRect = new this._pdf.PDFNet.Rect(
rect.topLeft.x,
rect.topLeft.y + percentHeightOffset,
rect.topLeft.x + rect.width,
@ -565,7 +575,7 @@ export class AnnotationActionsService {
} else {
const rect = toPosition(
viewerAnnotation.getPageNumber(),
this._pdf.documentViewer.getPageHeight(viewerAnnotation.getPageNumber()),
this._documentViewer.getHeight(viewerAnnotation.getPageNumber()),
this._annotationDrawService.annotationToQuads(viewerAnnotation),
);
return {
@ -575,14 +585,9 @@ export class AnnotationActionsService {
}
}
private _translateQuads(page: number, quad: Quad): Quad {
const rotation = this._pdf.documentViewer.getCompleteRotation(page);
return translateQuads(page, rotation, quad);
}
private async _extractTextFromRect(page: Core.PDFNet.Page, rect: Core.PDFNet.Rect) {
const txt = await this._pdf.PDFNet.TextExtractor.create();
txt.begin(page, rect); // Read the page.
await txt.begin(page, rect); // Read the page.
const words: string[] = [];
// Extract words one by one.

View File

@ -3,6 +3,8 @@ import { Injectable } from '@angular/core';
import { EntitiesService, FilterService, ListingService, SearchService } from '@iqser/common-ui';
import { filter, tap } from 'rxjs/operators';
import { MultiSelectService } from './multi-select.service';
import { PdfViewer } from '../../pdf-viewer/services/pdf-viewer.service';
import { REDAnnotationManager } from '../../pdf-viewer/services/annotation-manager.service';
@Injectable()
export class AnnotationsListingService extends ListingService<AnnotationWrapper> {
@ -11,6 +13,8 @@ export class AnnotationsListingService extends ListingService<AnnotationWrapper>
protected readonly _searchService: SearchService<AnnotationWrapper>,
protected readonly _entitiesService: EntitiesService<AnnotationWrapper>,
private readonly _multiSelectService: MultiSelectService,
private readonly _pdf: PdfViewer,
private readonly _annotationManager: REDAnnotationManager,
) {
super(_filterService, _searchService, _entitiesService);
@ -21,4 +25,33 @@ export class AnnotationsListingService extends ListingService<AnnotationWrapper>
)
.subscribe();
}
selectAnnotations(annotations?: AnnotationWrapper[]) {
if (!annotations) {
return this._annotationManager.deselect();
}
const annotationsToSelect = this._multiSelectService.isActive ? [...this.selected, ...annotations] : annotations;
this.#selectAnnotations(annotationsToSelect);
}
#selectAnnotations(annotations: AnnotationWrapper[]) {
if (!annotations.length) {
return;
}
if (!this._multiSelectService.isActive) {
this._annotationManager.deselect();
}
const pageNumber = annotations[0].pageNumber;
if (pageNumber === this._pdf.currentPage) {
return this._annotationManager.jumpAndSelect(annotations);
}
this._pdf.navigateTo(pageNumber);
// wait for page to be loaded and to draw annotations
setTimeout(() => this._annotationManager.jumpAndSelect(annotations), 300);
}
}

View File

@ -0,0 +1,18 @@
import { Injectable } from '@angular/core';
import { CanDeactivate } from '@angular/router';
import { filter, map, Observable } from 'rxjs';
import { REDDocumentViewer } from '../../pdf-viewer/services/document-viewer.service';
@Injectable()
export class DocumentUnloadedGuard implements CanDeactivate<unknown> {
constructor(private readonly _documentViewer: REDDocumentViewer) {}
canDeactivate(): Observable<boolean> {
this._documentViewer.close();
return this._documentViewer.loaded$.pipe(
filter(loaded => !loaded),
map(() => true),
);
}
}

View File

@ -14,12 +14,10 @@ import { EntitiesService, shareLast, Toaster } from '@iqser/common-ui';
import { RedactionLogService } from '@services/files/redaction-log.service';
import { TextHighlightService } from '@services/files/text-highlight.service';
import { ViewModeService } from './view-mode.service';
import { Core } from '@pdftron/webviewer';
import dayjs from 'dayjs';
import { NGXLogger } from 'ngx-logger';
import { MultiSelectService } from './multi-select.service';
import { FilesService } from '@services/files/files.service';
import Annotation = Core.Annotations.Annotation;
const DELTA_VIEW_TIME = 10 * 60 * 1000; // 10 minutes;
@ -113,19 +111,6 @@ export class FileDataService extends EntitiesService<AnnotationWrapper> {
return firstValueFrom(redactionLog$.pipe(tap(redactionLog => this.#redactionLog$.next(redactionLog))));
}
updateHiddenAnnotations(viewerAnnotations: Annotation[], hidden: boolean) {
const annotationId = viewerAnnotations[0].Id;
if (hidden) {
this.hiddenAnnotations.add(annotationId);
} else {
this.hiddenAnnotations.delete(annotationId);
}
}
isHidden(annotationId: string) {
return this.hiddenAnnotations.has(annotationId);
}
#checkMissingTypes() {
if (this.missingTypes.size > 0) {
this._toaster.error(_('error.missing-types'), {

View File

@ -71,14 +71,16 @@ export class FilePreviewStateService {
}
get #blob$() {
const reloadBlob$ = this.#reloadBlob$.pipe(
withLatestFrom(this.file$),
map(([, file]) => file),
);
return merge(this.file$, reloadBlob$).pipe(
const file$ = this.file$.pipe(
startWith(undefined),
pairwise(),
filter(([oldFile, newFile]) => oldFile?.cacheIdentifier !== newFile.cacheIdentifier),
);
const reloadBlob$ = this.#reloadBlob$.pipe(
withLatestFrom(file$),
map(([, file]) => file),
);
return merge(file$, reloadBlob$).pipe(
switchMap(([oldFile, newFile]) => this.#downloadOriginalFile(newFile.cacheIdentifier, !!oldFile?.cacheIdentifier)),
);
}

View File

@ -15,18 +15,18 @@ export class MultiSelectService {
readonly inactive$: Observable<boolean>;
readonly #active$ = new BehaviorSubject(false);
readonly #enabled$ = new BehaviorSubject(true);
#enabled = true;
constructor(viewModeService: ViewModeService, state: FilePreviewStateService) {
[this.active$, this.inactive$] = boolFactory(this.#active$.asObservable());
this.enabled$ = combineLatest([viewModeService.viewMode$, state.isWritable$]).pipe(
map(([viewMode, isWritable]) => isWritable && ENABLED_MULTISELECT.includes(viewMode)),
tap(enabled => this.#enabled$.next(enabled)),
tap(enabled => (this.#enabled = enabled)),
);
}
get isEnabled() {
return this.#enabled$.value;
return this.#enabled;
}
get isActive() {

View File

@ -1,134 +0,0 @@
import { Injectable } from '@angular/core';
import { BehaviorSubject, firstValueFrom, of } from 'rxjs';
import { RotationType, RotationTypes } from '@red/domain';
import { FileManagementService } from '@services/files/file-management.service';
import { FilePreviewStateService } from './file-preview-state.service';
import { distinctUntilChanged, map, switchMap, tap, withLatestFrom } from 'rxjs/operators';
import { PdfViewer } from './pdf-viewer.service';
import { HeaderElements } from '../utils/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';
import { ViewerHeaderConfigService } from './viewer-header-config.service';
import { FilesService } from '@services/files/files.service';
const ACTION_BUTTONS = [HeaderElements.APPLY_ROTATION, HeaderElements.DISCARD_ROTATION];
const ONE_ROTATION_DEGREE = 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 _fileManagementService: FileManagementService,
private readonly _filesService: FilesService,
private readonly _headerConfigService: ViewerHeaderConfigService,
) {}
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(
withLatestFrom(this._screenState.file$),
switchMap(([, file]) => this._filesService.reload(dossierId, file)),
tap(() => this._loadingService.stop()),
),
);
}
discardRotation() {
const rotations = this.#rotations$.value;
for (const page of Object.keys(rotations)) {
const times = rotations[page] / ONE_ROTATION_DEGREE;
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._headerConfigService.enable(ACTION_BUTTONS);
}
#hideActionButtons() {
this._headerConfigService.disable(ACTION_BUTTONS);
}
}

View File

@ -0,0 +1,318 @@
import { ChangeDetectorRef, Inject, Injectable, NgZone } from '@angular/core';
import { IHeaderElement, IManualRedactionEntry } from '@red/domain';
import { Core } from '@pdftron/webviewer';
import { TranslateService } from '@ngx-translate/core';
import {
ManualRedactionEntryType,
ManualRedactionEntryTypes,
ManualRedactionEntryWrapper,
} from '../../../models/file/manual-redaction-entry.wrapper';
import { AnnotationWrapper } from '../../../models/file/annotation.wrapper';
import { AnnotationDrawService } from '../../pdf-viewer/services/annotation-draw.service';
import { AnnotationActionsService } from './annotation-actions.service';
import { UserPreferenceService } from '../../../services/user-preference.service';
import { BASE_HREF_FN, BaseHrefFn } from '../../../tokens';
import { shareDistinctLast } from '@iqser/common-ui';
import { toPosition } from '../utils/pdf-calculation.utils';
import { MultiSelectService } from './multi-select.service';
import { FilePreviewStateService } from './file-preview-state.service';
import { map, tap } from 'rxjs/operators';
import { HeaderElements, TextPopups } from '../utils/constants';
import { FileDataService } from './file-data.service';
import { ViewerHeaderService } from '../../pdf-viewer/services/viewer-header.service';
import { ManualRedactionService } from './manual-redaction.service';
import { PdfViewer } from '../../pdf-viewer/services/pdf-viewer.service';
import { REDAnnotationManager } from '../../pdf-viewer/services/annotation-manager.service';
import { ALLOWED_ACTIONS_WHEN_PAGE_EXCLUDED, HEADER_ITEMS_TO_TOGGLE, TEXT_POPUPS_TO_TOGGLE } from '../../pdf-viewer/utils/constants';
import { REDDocumentViewer } from '../../pdf-viewer/services/document-viewer.service';
import { combineLatest, Observable, Subject } from 'rxjs';
import { ViewModeService } from './view-mode.service';
import { PermissionsService } from '../../../services/permissions.service';
import { AnnotationsListingService } from './annotations-listing.service';
import Annotation = Core.Annotations.Annotation;
import Quad = Core.Math.Quad;
@Injectable()
export class PdfProxyService {
readonly annotationSelected$ = this.#annotationSelected$;
readonly manualAnnotationRequested$ = new Subject<ManualRedactionEntryWrapper>();
readonly pageChanged$ = this._pdf.pageChanged$.pipe(
tap(() => this._handleCustomActions()),
shareDistinctLast(),
);
canPerformActions = true;
instance = this._pdf.instance;
canPerformAnnotationActions$: Observable<boolean>;
readonly #visibilityOffIcon = this._convertPath('/assets/icons/general/visibility-off.svg');
readonly #visibilityIcon = this._convertPath('/assets/icons/general/visibility.svg');
readonly #falsePositiveIcon = this._convertPath('/assets/icons/general/pdftron-action-false-positive.svg');
readonly #addRedactionIcon = this._convertPath('/assets/icons/general/pdftron-action-add-redaction.svg');
readonly #addDictIcon = this._convertPath('/assets/icons/general/pdftron-action-add-dict.svg');
constructor(
@Inject(BASE_HREF_FN) private readonly _convertPath: BaseHrefFn,
private readonly _translateService: TranslateService,
private readonly _manualRedactionService: ManualRedactionService,
private readonly _ngZone: NgZone,
private readonly _userPreferenceService: UserPreferenceService,
private readonly _annotationDrawService: AnnotationDrawService,
private readonly _annotationActionsService: AnnotationActionsService,
private readonly _fileDataService: FileDataService,
private readonly _viewerHeaderService: ViewerHeaderService,
private readonly _viewModeService: ViewModeService,
private readonly _permissionsService: PermissionsService,
private readonly _documentViewer: REDDocumentViewer,
private readonly _annotationManager: REDAnnotationManager,
private readonly _pdf: PdfViewer,
private readonly _state: FilePreviewStateService,
private readonly _multiSelectService: MultiSelectService,
private readonly _listingService: AnnotationsListingService,
private readonly _changeDetectorRef: ChangeDetectorRef,
) {
this.canPerformAnnotationActions$ = this.#canPerformAnnotationActions$;
}
get #canPerformAnnotationActions$() {
const viewMode$ = this._viewModeService.viewMode$.pipe(tap(() => this.#deactivateMultiSelect()));
return combineLatest([this._state.file$, this._state.dossier$, viewMode$, this._pdf.compareMode$]).pipe(
map(
([file, dossier, viewMode]) =>
this._permissionsService.canPerformAnnotationActions(file, dossier) && viewMode === 'STANDARD',
),
tap(canPerformActions => (this.canPerformActions = canPerformActions)),
tap(() => this._handleCustomActions()),
shareDistinctLast(),
);
}
get #annotationSelected$() {
return this._annotationManager.annotationSelected$.pipe(
map(value => this.#processSelectedAnnotations(...value)),
tap(annotations => this.handleAnnotationSelected(annotations)),
);
}
handleAnnotationSelected(annotationIds: string[]) {
this._listingService.setSelected(annotationIds.map(id => this._fileDataService.find(id)).filter(ann => ann !== undefined));
this._changeDetectorRef.markForCheck();
}
loadViewer() {
this._configureElements();
this._configureTextPopup();
}
#deactivateMultiSelect() {
this._multiSelectService.deactivate();
this._annotationManager.deselect();
this.handleAnnotationSelected([]);
}
#processSelectedAnnotations(annotations: Annotation[], action) {
let nextAnnotations: Annotation[];
if (action === 'deselected') {
// Remove deselected annotations from selected list
nextAnnotations = this._annotationManager.selected.filter(ann => !annotations.some(a => a.Id === ann.Id));
this._pdf.disable(TextPopups.ADD_RECTANGLE);
return nextAnnotations.map(ann => ann.Id);
} else if (!this._multiSelectService.isEnabled) {
// Only choose the last selected annotation, to bypass viewer multi select
nextAnnotations = annotations;
const notSelected = this._fileDataService.all.filter(wrapper => !nextAnnotations.some(ann => ann.Id === wrapper.id));
this._annotationManager.deselect(notSelected);
} else {
// Get selected annotations from the manager, no intervention needed
nextAnnotations = this._annotationManager.selected;
}
this.#configureAnnotationSpecificActions(annotations);
if (!(annotations.length === 1 && annotations[0].ReadOnly)) {
this._pdf.enable(TextPopups.ADD_RECTANGLE);
} else {
this._pdf.disable(TextPopups.ADD_RECTANGLE);
}
return nextAnnotations.map(ann => ann.Id);
}
private _configureElements() {
const dossierTemplateId = this._state.dossierTemplateId;
const color = this._annotationDrawService.getAndConvertColor(dossierTemplateId, dossierTemplateId, 'manual');
this._documentViewer.setRectangleToolStyles(color);
}
#configureAnnotationSpecificActions(viewerAnnotations: Annotation[]) {
if (!this.canPerformActions) {
if (this.instance.UI.annotationPopup.getItems().length) {
this.instance.UI.annotationPopup.update([]);
}
return;
}
const annotationWrappers: AnnotationWrapper[] = viewerAnnotations.map(va => this._fileDataService.find(va.Id)).filter(va => !!va);
this.instance.UI.annotationPopup.update([]);
if (annotationWrappers.length === 0) {
this._configureRectangleAnnotationPopup(viewerAnnotations[0]);
return;
}
// Add hide action as last item
const allAnnotationsHaveImageAction = annotationWrappers.reduce((acc, next) => acc && next.isImage, true);
if (allAnnotationsHaveImageAction) {
const allAreVisible = viewerAnnotations.reduce((acc, next) => next.isVisible() && acc, true);
this.instance.UI.annotationPopup.add([
{
type: 'actionButton',
img: allAreVisible ? this.#visibilityOffIcon : this.#visibilityIcon,
title: this._translateService.instant(`annotation-actions.${allAreVisible ? 'hide' : 'show'}`),
onClick: () => {
this._ngZone.run(() => {
if (allAreVisible) {
this._annotationManager.hide(viewerAnnotations);
this._annotationManager.hidden.add(viewerAnnotations[0].Id);
} else {
this._annotationManager.show(viewerAnnotations);
this._annotationManager.hidden.delete(viewerAnnotations[0].Id);
}
this._annotationManager.deselect();
});
},
},
]);
}
const actions = this._annotationActionsService.getViewerAvailableActions(annotationWrappers);
this.instance.UI.annotationPopup.add(actions);
}
private _configureRectangleAnnotationPopup(annotation: Annotation) {
if (!this._pdf.isCompare || annotation.getPageNumber() % 2 === 1) {
this.instance.UI.annotationPopup.add([
{
type: 'actionButton',
dataElement: TextPopups.ADD_RECTANGLE,
img: this.#addRedactionIcon,
title: this.#getTitle(ManualRedactionEntryTypes.REDACTION),
onClick: () => this._addRectangleManualRedaction(),
},
]);
}
}
private _addRectangleManualRedaction() {
const activeAnnotation = this._annotationManager.selected[0];
const activePage = activeAnnotation.getPageNumber();
const quads = [this._annotationDrawService.annotationToQuads(activeAnnotation)];
const manualRedactionEntry = this._getManualRedaction({ [activePage]: quads });
this._cleanUpSelectionAndButtonState();
this.manualAnnotationRequested$.next({ manualRedactionEntry, type: 'REDACTION' });
}
private _cleanUpSelectionAndButtonState() {
this._viewerHeaderService.disable([HeaderElements.SHAPE_TOOL_GROUP_BUTTON]);
this._viewerHeaderService.enable([HeaderElements.SHAPE_TOOL_GROUP_BUTTON]);
}
private _configureTextPopup() {
const popups: IHeaderElement[] = [];
// Adding directly to the false-positive dict is only available in dev-mode
if (this._userPreferenceService.areDevFeaturesEnabled) {
popups.push({
type: 'actionButton',
dataElement: TextPopups.ADD_FALSE_POSITIVE,
img: this.#falsePositiveIcon,
title: this.#getTitle(ManualRedactionEntryTypes.FALSE_POSITIVE),
onClick: () => this._addManualRedactionOfType(ManualRedactionEntryTypes.FALSE_POSITIVE),
});
}
popups.push({
type: 'actionButton',
dataElement: TextPopups.ADD_REDACTION,
img: this.#addRedactionIcon,
title: this.#getTitle(ManualRedactionEntryTypes.REDACTION),
onClick: () => this._addManualRedactionOfType(ManualRedactionEntryTypes.REDACTION),
});
popups.push({
type: 'actionButton',
dataElement: TextPopups.ADD_DICTIONARY,
img: this.#addDictIcon,
title: this.#getTitle(ManualRedactionEntryTypes.DICTIONARY),
onClick: () => this._addManualRedactionOfType(ManualRedactionEntryTypes.DICTIONARY),
});
this._pdf.configureTextPopups(popups);
return this._handleCustomActions();
}
#getTitle(type: ManualRedactionEntryType) {
return this._translateService.instant(this._manualRedactionService.getTitle(type, this._state.dossier));
}
private _addManualRedactionOfType(type: ManualRedactionEntryType) {
const selectedQuads: Record<string, Quad[]> = this._pdf.documentViewer.getSelectedTextQuads();
const text = this._documentViewer.selectedText;
const manualRedactionEntry = this._getManualRedaction(selectedQuads, text, true);
this.manualAnnotationRequested$.next({ manualRedactionEntry, type });
}
private _handleCustomActions() {
const isCurrentPageExcluded = this._state.file.isPageExcluded(this._pdf.currentPage);
if (this.canPerformActions && !isCurrentPageExcluded) {
try {
this._pdf.instance.UI.enableTools(['AnnotationCreateRectangle']);
} catch (e) {
// happens
}
this._pdf.enable(TEXT_POPUPS_TO_TOGGLE);
this._viewerHeaderService.enable(HEADER_ITEMS_TO_TOGGLE);
if (this._documentViewer.selectedText.length > 2) {
this._pdf.enable([TextPopups.ADD_DICTIONARY, TextPopups.ADD_FALSE_POSITIVE]);
}
return;
}
let textPopupElementsToDisable = [...TEXT_POPUPS_TO_TOGGLE];
let headerElementsToDisable = [...HEADER_ITEMS_TO_TOGGLE];
if (isCurrentPageExcluded) {
textPopupElementsToDisable = textPopupElementsToDisable.filter(
element => !ALLOWED_ACTIONS_WHEN_PAGE_EXCLUDED.includes(element),
);
headerElementsToDisable = headerElementsToDisable.filter(element => !ALLOWED_ACTIONS_WHEN_PAGE_EXCLUDED.includes(element));
} else {
this._pdf.instance.UI.disableTools(['AnnotationCreateRectangle']);
}
this._pdf.disable(textPopupElementsToDisable);
this._viewerHeaderService.disable(headerElementsToDisable);
}
private _getManualRedaction(quads: Record<string, Quad[]>, text?: string, convertQuads = false): IManualRedactionEntry {
const entry: IManualRedactionEntry = { positions: [] };
for (const key of Object.keys(quads)) {
for (const quad of quads[key]) {
const page = parseInt(key, 10);
const pageHeight = this._documentViewer.getHeight(page);
entry.positions.push(toPosition(page, pageHeight, convertQuads ? this._pdf.translateQuad(page, quad) : quad));
}
}
entry.value = text;
entry.rectangle = !text;
return entry;
}
}

View File

@ -1,254 +0,0 @@
import { translateQuads } from '../../../utils';
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 { DISABLED_HOTKEYS } from '../utils/constants';
import { Observable, Subject } from 'rxjs';
import { NGXLogger } from 'ngx-logger';
import { map, tap } from 'rxjs/operators';
import { ListingService, shareDistinctLast, shareLast } from '@iqser/common-ui';
import { MultiSelectService } from './multi-select.service';
import { ActivatedRoute } from '@angular/router';
import Annotation = Core.Annotations.Annotation;
@Injectable()
export class PdfViewer {
instance?: WebViewerInstance;
readonly documentLoaded$: Observable<boolean>;
readonly currentPage$ = this._activatedRoute.queryParamMap.pipe(
map(params => Number(params.get('page') ?? '1')),
shareDistinctLast(),
);
readonly #documentLoaded$ = new Subject<boolean>();
constructor(
@Inject(BASE_HREF) private readonly _baseHref: string,
private readonly _viewModeService: ViewModeService,
private readonly _activatedRoute: ActivatedRoute,
private readonly _multiSelectService: MultiSelectService,
private readonly _listingService: ListingService<AnnotationWrapper>,
private readonly _logger: NGXLogger,
) {
this.documentLoaded$ = this.#documentLoaded$.asObservable().pipe(
tap(() => this._logger.debug('[PDF] Loaded')),
tap(() => this.#setCurrentPage()),
shareLast(),
);
}
get documentViewer() {
return this.instance?.Core.documentViewer;
}
get annotationManager() {
return this.instance?.Core.annotationManager;
}
get UI() {
return this.instance.UI;
}
get Annotations() {
return this.instance.Core.Annotations;
}
get PDFNet(): typeof Core.PDFNet {
return this.instance.Core.PDFNet;
}
get hasAnnotations() {
return this.annotationManager?.getAnnotationsList().length > 0 ?? false;
}
get paginationOffset() {
return this._viewModeService.isCompare ? 2 : 1;
}
get currentPage() {
return this._viewModeService.isCompare ? Math.ceil(this._currentInternalPage / 2) : this._currentInternalPage;
}
get totalPages() {
return this._viewModeService.isCompare ? Math.ceil(this.pageCount / 2) : this.pageCount;
}
get pageCount() {
try {
return this.instance?.Core.documentViewer?.getPageCount() ?? 1;
} catch {
// might throw Error: getPageCount was called before the 'documentLoaded' event
return 1;
}
}
private get _currentInternalPage() {
return this.instance?.Core.documentViewer?.getCurrentPage() ?? 1;
}
async lockDocument() {
const document = await this.documentViewer.getDocument()?.getPDFDoc();
if (!document) {
return false;
}
await document.lock();
this._logger.debug('[PDF] Locked');
return true;
}
hideAnnotations(annotations: Annotation[]): void {
this.annotationManager.hideAnnotations(annotations);
}
showAnnotations(annotations: Annotation[]): void {
this.annotationManager.showAnnotations(annotations);
}
getAnnotations(predicate?: (value) => boolean) {
const annotations = this.annotationManager?.getAnnotationsList() ?? [];
return predicate ? annotations.filter(predicate) : annotations;
}
getAnnotationsById(ids: readonly string[]) {
if (this.annotationManager) {
return ids.map(id => this.annotationManager.getAnnotationById(id)).filter(a => !!a);
}
return [];
}
emitDocumentLoaded() {
this.deleteAnnotations();
this.#documentLoaded$.next(true);
}
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,
);
return this.instance;
}
isCurrentPageExcluded(file: File) {
const currentPage = this.currentPage;
return !!file?.excludedPages?.includes(currentPage);
}
navigateToPage(pageNumber: string | number) {
const parsedNumber = typeof pageNumber === 'string' ? parseInt(pageNumber, 10) : pageNumber;
this._navigateToPage(this.paginationOffset === 2 ? parsedNumber * this.paginationOffset - 1 : parsedNumber);
}
navigatePreviousPage() {
if (this._currentInternalPage > 1) {
this._navigateToPage(Math.max(this._currentInternalPage - this.paginationOffset, 1));
}
}
navigateNextPage() {
if (this._currentInternalPage < this.pageCount) {
this._navigateToPage(Math.min(this._currentInternalPage + this.paginationOffset, this.pageCount));
}
}
disableHotkeys(): void {
DISABLED_HOTKEYS.forEach(key => this.instance.UI.hotkeys.off(key));
}
translateQuad(page: number, quad: Core.Math.Quad) {
const rotation = this.documentViewer.getCompleteRotation(page);
return translateQuads(page, rotation, quad);
}
deselectAllAnnotations() {
this.annotationManager?.deselectAllAnnotations();
}
selectAnnotations(annotations?: AnnotationWrapper[]) {
if (!annotations) {
return this.deselectAllAnnotations();
}
const annotationsToSelect = this._multiSelectService.isActive ? [...this._listingService.selected, ...annotations] : annotations;
this.#selectAnnotations(annotationsToSelect);
}
deleteAnnotations(annotationsIds?: readonly string[]) {
let annotations: Annotation[];
if (!annotationsIds) {
annotations = this.getAnnotations();
} else {
annotations = this.getAnnotationsById(annotationsIds);
}
try {
this.annotationManager.deleteAnnotations(annotations, {
force: true,
});
} catch (error) {
console.log('Error while deleting annotations: ', error);
}
}
deselectAnnotations(annotations: AnnotationWrapper[]) {
const ann = this.getAnnotationsById(annotations.map(a => a.id));
this.annotationManager.deselectAnnotations(ann);
}
#selectAnnotations(annotations: AnnotationWrapper[] = []) {
const filteredAnnotationsIds = annotations.filter(a => !!a).map(a => a.id);
if (!filteredAnnotationsIds.length) {
return;
}
if (!this._multiSelectService.isActive) {
this.deselectAllAnnotations();
}
const pageNumber = annotations[0].pageNumber;
if (pageNumber === this.currentPage) {
return this.#jumpAndSelectAnnotations(filteredAnnotationsIds);
}
this.navigateToPage(pageNumber);
// wait for page to be loaded and to draw annotations
setTimeout(() => this.#jumpAndSelectAnnotations(filteredAnnotationsIds), 300);
}
#jumpAndSelectAnnotations(annotationIds: readonly string[]) {
const annotationsFromViewer = this.getAnnotationsById(annotationIds);
this.annotationManager.jumpToAnnotation(annotationsFromViewer[0]);
this.annotationManager.selectAnnotations(annotationsFromViewer);
}
private _navigateToPage(pageNumber: number) {
if (this._currentInternalPage !== pageNumber) {
this.documentViewer.displayPageLocation(pageNumber, 0, 0);
}
}
#convertPath(path: string) {
return `${this._baseHref}${path}`;
}
#setCurrentPage() {
const currentDocPage = this._activatedRoute.snapshot.queryParamMap.get('page');
this.documentViewer.setCurrentPage(Number(currentDocPage ?? '1'));
}
}

View File

@ -1,15 +1,15 @@
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { skip, tap } from 'rxjs/operators';
import { shareDistinctLast } from '@iqser/common-ui';
import { PdfViewer } from './pdf-viewer.service';
import { bool, shareDistinctLast } from '@iqser/common-ui';
import { REDAnnotationManager } from '../../pdf-viewer/services/annotation-manager.service';
@Injectable()
export class SkippedService {
readonly hideSkipped$: Observable<boolean>;
readonly #hideSkipped$ = new BehaviorSubject(false);
constructor(private readonly _pdf: PdfViewer) {
constructor(private readonly _annotationManager: REDAnnotationManager) {
this.hideSkipped$ = this.#hideSkipped$.pipe(
tap(hideSkipped => this._handleIgnoreAnnotationsDrawing(hideSkipped)),
shareDistinctLast(),
@ -28,11 +28,11 @@ export class SkippedService {
}
private _handleIgnoreAnnotationsDrawing(hideSkipped: boolean): void {
const ignored = this._pdf.getAnnotations(a => a.getCustomData('skipped'));
const ignored = this._annotationManager.get(a => bool(a.getCustomData('skipped')));
if (hideSkipped) {
this._pdf.hideAnnotations(ignored);
this._annotationManager.hide(ignored);
} else {
this._pdf.showAnnotations(ignored);
this._annotationManager.show(ignored);
}
}
}

View File

@ -1,6 +1,5 @@
import { Injectable } from '@angular/core';
import { clearStamps, stampPDFPage } from '../../../utils';
import { PdfViewer } from './pdf-viewer.service';
import { FilePreviewStateService } from './file-preview-state.service';
import { NGXLogger } from 'ngx-logger';
import { ViewModeService } from './view-mode.service';
@ -8,12 +7,15 @@ import { TranslateService } from '@ngx-translate/core';
import { Core } from '@pdftron/webviewer';
import { firstValueFrom } from 'rxjs';
import { WatermarkService } from '@services/entity-services/watermark.service';
import { PdfViewer } from '../../pdf-viewer/services/pdf-viewer.service';
import { REDDocumentViewer } from '../../pdf-viewer/services/document-viewer.service';
import PDFNet = Core.PDFNet;
@Injectable()
export class StampService {
constructor(
private readonly _pdf: PdfViewer,
private readonly _documentViewer: REDDocumentViewer,
private readonly _state: FilePreviewStateService,
private readonly _logger: NGXLogger,
private readonly _viewModeService: ViewModeService,
@ -22,7 +24,7 @@ export class StampService {
) {}
async stampPDF(): Promise<void> {
const pdfDoc = await this._pdf.documentViewer.getDocument()?.getPDFDoc();
const pdfDoc = await this._documentViewer.PDFDoc;
if (!pdfDoc) {
return;
}
@ -33,7 +35,7 @@ export class StampService {
try {
await clearStamps(pdfDoc, this._pdf.PDFNet, allPages);
} catch (e) {
this._logger.error('Error clearing stamps: ', e);
console.error('Error clearing stamps: ', e);
return;
}
@ -46,8 +48,7 @@ export class StampService {
await this._stampExcludedPages(pdfDoc, file.excludedPages);
}
this._pdf.documentViewer.refreshAll();
this._pdf.documentViewer.updateView([this._pdf.currentPage], this._pdf.currentPage);
this._documentViewer.refreshAndUpdateView();
}
private async _stampExcludedPages(document: PDFNet.PDFDoc, excludedPages: number[]): Promise<void> {

View File

@ -1,49 +0,0 @@
import { Inject, Injectable } from '@angular/core';
import { PdfViewer } from './pdf-viewer.service';
import { UserPreferenceService } from '@services/user-preference.service';
import { HeaderElements } from '../utils/constants';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { TranslateService } from '@ngx-translate/core';
import { BASE_HREF } from '../../../tokens';
@Injectable()
export class TooltipsService {
constructor(
@Inject(BASE_HREF) private readonly _baseHref: string,
private readonly _pdfViewer: PdfViewer,
private readonly _userPreferenceService: UserPreferenceService,
private readonly _translateService: TranslateService,
) {}
get toggleTooltipsBtnTitle(): string {
return this._translateService.instant(_('pdf-viewer.toggle-tooltips'), {
active: this._userPreferenceService.getFilePreviewTooltipsPreference(),
});
}
get toggleTooltipsBtnIcon(): string {
return this._convertPath(
this._userPreferenceService.getFilePreviewTooltipsPreference()
? '/assets/icons/general/pdftron-action-enable-tooltips.svg'
: '/assets/icons/general/pdftron-action-disable-tooltips.svg',
);
}
updateTooltipsVisibility(): void {
const current = this._userPreferenceService.getFilePreviewTooltipsPreference();
this._pdfViewer.instance.UI.setAnnotationContentOverlayHandler(() => (current ? undefined : false));
}
async toggleTooltips(): Promise<void> {
await this._userPreferenceService.toggleFilePreviewTooltipsPreference();
this.updateTooltipsVisibility();
this._pdfViewer.instance.UI.updateElement(HeaderElements.TOGGLE_TOOLTIPS, {
title: this.toggleTooltipsBtnTitle,
img: this.toggleTooltipsBtnIcon,
});
}
private _convertPath(path: string): string {
return this._baseHref + path;
}
}

View File

@ -7,17 +7,14 @@ import { shareDistinctLast } from '@iqser/common-ui';
@Injectable()
export class ViewModeService {
readonly viewMode$: Observable<ViewMode>;
readonly compareMode$: Observable<boolean>;
readonly isRedacted$: Observable<boolean>;
readonly isStandard$: Observable<boolean>;
readonly isDelta$: Observable<boolean>;
private readonly _viewMode$ = new BehaviorSubject<ViewMode>('STANDARD');
private readonly _compareMode$ = new BehaviorSubject<boolean>(false);
readonly #viewMode$ = new BehaviorSubject<ViewMode>('STANDARD');
constructor() {
this.viewMode$ = this._viewMode$.asObservable();
this.compareMode$ = this._compareMode$.asObservable();
this.viewMode$ = this.#viewMode$.asObservable();
this.isRedacted$ = this._is('REDACTED');
this.isStandard$ = this._is('STANDARD');
this.isDelta$ = this._is('DELTA');
@ -28,35 +25,27 @@ export class ViewModeService {
}
get viewMode() {
return this._viewMode$.value;
return this.#viewMode$.value;
}
set viewMode(mode: ViewMode) {
this._viewMode$.next(mode);
this.#viewMode$.next(mode);
}
get isStandard() {
return this._viewMode$.value === 'STANDARD';
return this.#viewMode$.value === 'STANDARD';
}
get isDelta() {
return this._viewMode$.value === 'DELTA';
return this.#viewMode$.value === 'DELTA';
}
get isRedacted() {
return this._viewMode$.value === 'REDACTED';
return this.#viewMode$.value === 'REDACTED';
}
get isTextHighlights() {
return this._viewMode$.value === 'TEXT_HIGHLIGHTS';
}
get isCompare() {
return this._compareMode$.value;
}
set compareMode(compareMode: boolean) {
this._compareMode$.next(compareMode);
return this.#viewMode$.value === 'TEXT_HIGHLIGHTS';
}
switchToStandard() {
@ -76,7 +65,7 @@ export class ViewModeService {
}
private _switchTo(mode: ViewMode) {
this._viewMode$.next(mode);
this.#viewMode$.next(mode);
}
private _is(mode: ViewMode) {

View File

@ -1,8 +1,7 @@
import { stampPDFPage } from '../../../utils';
import { Core, WebViewerInstance } from '@pdftron/webviewer';
import { File } from '@red/domain';
import { Core } from '@pdftron/webviewer';
const processPage = async (
export const processPage = async (
pageNumber: number,
document1: Core.PDFNet.PDFDoc,
document2: Core.PDFNet.PDFDoc,
@ -24,39 +23,3 @@ const processPage = async (
await mergedDocument.getPageCount(),
]);
};
export const loadCompareDocumentWrapper = async (
currentDocument: Core.PDFNet.PDFDoc,
compareDocument: Core.PDFNet.PDFDoc,
mergedDocument: Core.PDFNet.PDFDoc,
instance: WebViewerInstance,
file: File,
setCompareViewMode: () => void,
navigateToPage: () => void,
pdfNet: typeof Core.PDFNet,
) => {
try {
const maxPageCount = Math.max(await currentDocument.getPageCount(), await compareDocument.getPageCount());
for (let idx = 1; idx <= maxPageCount; idx++) {
await processPage(idx, currentDocument, compareDocument, mergedDocument, pdfNet);
await processPage(idx, compareDocument, currentDocument, mergedDocument, pdfNet);
}
const buffer = await mergedDocument.saveMemoryBuffer(pdfNet.SDFDoc.SaveOptions.e_linearized);
const mergedDocumentBuffer = new Blob([buffer], {
type: 'application/pdf',
});
setCompareViewMode();
instance.UI.loadDocument(mergedDocumentBuffer, {
filename: file?.filename ?? 'document.pdf',
});
navigateToPage();
} catch (e) {
console.error(e);
}
};

View File

@ -11,8 +11,6 @@ export const ActionsHelpModeKeys = {
'hint-image': 'picture',
} as const;
export const ALLOWED_KEYBOARD_SHORTCUTS: List = ['+', '-', 'p', 'r', 'Escape'] as const;
export const ALL_HOTKEYS: List = ['Escape', 'F', 'f', 'ArrowUp', 'ArrowDown'] as const;
export type HeaderElementType =
@ -42,36 +40,3 @@ export const TextPopups = {
ADD_RECTANGLE: 'add-rectangle',
ADD_FALSE_POSITIVE: 'add-false-positive',
} as const;
export const DISABLED_HOTKEYS = [
'CTRL+SHIFT+EQUAL',
'COMMAND+SHIFT+EQUAL',
'CTRL+SHIFT+MINUS',
'COMMAND+SHIFT+MINUS',
'CTRL+V',
'COMMAND+V',
'CTRL+Y',
'COMMAND+Y',
'CTRL+O',
'COMMAND+O',
'CTRL+P',
'COMMAND+P',
'SPACE',
'UP',
'DOWN',
'R',
'P',
'A',
'C',
'E',
'I',
'L',
'N',
'O',
'T',
'S',
'G',
'H',
'K',
'U',
] as const;

View File

@ -1,18 +1,16 @@
import { IRectangle } from '@red/domain';
import { Core } from '@pdftron/webviewer';
import Quad = Core.Math.Quad;
export const toPosition = (
page: number,
pageHeight: number,
selectedQuad: { x1: number; x2: number; x3: number; x4: number; y4: number; y2: number },
): IRectangle => {
const height = selectedQuad.y2 - selectedQuad.y4;
export const toPosition = (page: number, pageHeight: number, { x1, x2, x3, x4, y2, y4 }: Quad): IRectangle => {
const height = y2 - y4;
return {
page: page,
page,
topLeft: {
x: Math.min(selectedQuad.x3, selectedQuad.x4, selectedQuad.x2, selectedQuad.x1),
y: pageHeight - (selectedQuad.y4 + height),
x: Math.min(x3, x4, x2, x1),
y: pageHeight - (y4 + height),
},
height: height,
width: Math.max(4, Math.abs(selectedQuad.x3 - selectedQuad.x4), Math.abs(selectedQuad.x3 - selectedQuad.x1)),
height,
width: Math.max(4, Math.abs(x3 - x4), Math.abs(x3 - x1)),
};
};

View File

@ -0,0 +1 @@
<input #input (input)="upload($event.target['files'])" accept="application/pdf" id="compareFileInput" type="file" />

View File

@ -0,0 +1,6 @@
:host {
position: absolute;
width: 0;
height: 0;
visibility: hidden;
}

View File

@ -0,0 +1,137 @@
import { Component, ElementRef, ViewChild } from '@angular/core';
import { HeaderElements } from '../../../file-preview/utils/constants';
import { ConfirmationDialogInput, ConfirmOptions, LoadingService } from '@iqser/common-ui';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { FilesMapService } from '@services/files/files-map.service';
import { SharedDialogService } from '@shared/services/dialog.service';
import { PdfViewer } from '../../services/pdf-viewer.service';
import { ViewerHeaderService } from '../../services/viewer-header.service';
import { REDDocumentViewer } from '../../services/document-viewer.service';
import { Core } from '@pdftron/webviewer';
import { firstValueFrom } from 'rxjs';
import { MatDialogRef } from '@angular/material/dialog';
import { processPage } from '../../../file-preview/utils/compare-mode.utils';
import { NGXLogger } from 'ngx-logger';
import PDFDoc = Core.PDFNet.PDFDoc;
@Component({
selector: 'redaction-compare-file-input',
templateUrl: './compare-file-input.component.html',
styleUrls: ['./compare-file-input.component.scss'],
})
export class CompareFileInputComponent {
@ViewChild('input', { static: true }) private readonly _input: ElementRef;
constructor(
private readonly _pdf: PdfViewer,
private readonly _logger: NGXLogger,
private readonly _loadingService: LoadingService,
private readonly _documentViewer: REDDocumentViewer,
private readonly _filesMapService: FilesMapService,
private readonly _dialogService: SharedDialogService,
private readonly _viewerHeaderService: ViewerHeaderService,
) {}
get #filename() {
const { dossierId, fileId } = this._pdf;
if (!dossierId || !fileId) {
this._logger.error('No dossier id or file id when uploading compare file!');
return 'document.pdf';
}
return this._filesMapService.get(dossierId, fileId)?.filename ?? 'document.pdf';
}
upload(files: FileList) {
const fileToCompare = files[0];
this._input.nativeElement.value = null;
if (!fileToCompare) {
console.error('No file to compare!');
return;
}
const fileReader = new FileReader();
fileReader.onload = () => this.#createDocumentsAndCompare(fileReader.result as ArrayBuffer, fileToCompare.name);
fileReader.readAsArrayBuffer(fileToCompare);
}
async #createDocumentsAndCompare(buffer: ArrayBuffer, fileName: string) {
const currentBlob = await this._documentViewer.blob();
this._documentViewer.close();
const pdfNet = this._pdf.PDFNet;
const compareDocument = await pdfNet.PDFDoc.createFromBuffer(buffer);
const currentDocument = await pdfNet.PDFDoc.createFromBuffer(await currentBlob.arrayBuffer());
const currentDocumentPageCount = await currentDocument.getPageCount();
const compareDocumentPageCount = await compareDocument.getPageCount();
if (currentDocumentPageCount !== compareDocumentPageCount) {
const result = await this.#askForConfirmation(fileName, currentDocumentPageCount, compareDocumentPageCount);
if (result !== ConfirmOptions.CONFIRM) {
return;
}
}
await this.#loadCompareDocument(currentDocument, compareDocument);
}
#askForConfirmation(fileName: string, currentDocumentPageCount: number, compareDocumentPageCount: number) {
const ref: MatDialogRef<unknown, ConfirmOptions> = this._dialogService.openDialog(
'confirm',
null,
new ConfirmationDialogInput({
title: _('confirmation-dialog.compare-file.title'),
question: _('confirmation-dialog.compare-file.question'),
translateParams: {
fileName,
currentDocumentPageCount,
compareDocumentPageCount,
},
}),
);
return firstValueFrom(ref.afterClosed());
}
async #loadCompareDocument(current: PDFDoc, compare: PDFDoc) {
this._loadingService.start();
await this.#mergeDocuments(current, compare);
this._viewerHeaderService.disable([HeaderElements.COMPARE_BUTTON]);
this._viewerHeaderService.enable([HeaderElements.CLOSE_COMPARE_BUTTON]);
this._loadingService.stop();
}
async #mergeDocuments(current: PDFDoc, compare: PDFDoc) {
const pdfNet = this._pdf.PDFNet;
const merged = await pdfNet.PDFDoc.create();
try {
const maxPageCount = Math.max(await current.getPageCount(), await compare.getPageCount());
for (let idx = 1; idx <= maxPageCount; idx++) {
await processPage(idx, current, compare, merged, pdfNet);
await processPage(idx, compare, current, merged, pdfNet);
}
const buffer = await merged.saveMemoryBuffer(pdfNet.SDFDoc.SaveOptions.e_linearized);
const mergedDocumentBuffer = new Blob([buffer], {
type: 'application/pdf',
});
this._pdf.openCompareMode();
this._pdf.instance.UI.loadDocument(mergedDocumentBuffer, {
filename: this.#filename,
});
this._pdf.navigateTo(1);
} catch (e) {
console.error(e);
}
}
}

View File

@ -0,0 +1,27 @@
<div [style.visibility]="(documentViewer.loaded$ | async) ? 'visible' : 'hidden'" class="pagination noselect">
<div (click)="pdf.navigatePreviousPage()">
<mat-icon class="chevron-icon" svgIcon="red:nav-prev"></mat-icon>
</div>
<div>
<input
#pageInput
(change)="pdf.navigateTo(pageInput.value)"
[max]="pdf.totalPages$ | async"
[value]="pdf.currentPage$ | async"
class="page-number-input"
min="1"
type="number"
/>
</div>
<div class="separator">/</div>
<div>
{{ pdf.totalPages$ | async }}
</div>
<div (click)="pdf.navigateNextPage()">
<mat-icon class="chevron-icon" svgIcon="red:nav-next"></mat-icon>
</div>
</div>

View File

@ -1,30 +1,8 @@
.page {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
}
.viewer {
height: 100%;
}
.searching {
position: absolute;
top: 9px;
right: 60px;
display: flex;
align-items: center;
mat-spinner {
margin-left: 15px;
}
}
.pagination {
z-index: 1;
position: absolute;
bottom: 20px;
left: 50%;
left: calc(50% - (var(--workload-width) / 2));
transform: translate(-50%);
background: var(--iqser-white);
color: var(--iqser-grey-7);

View File

@ -0,0 +1,12 @@
import { Component } from '@angular/core';
import { PdfViewer } from '../../services/pdf-viewer.service';
import { REDDocumentViewer } from '../../services/document-viewer.service';
@Component({
selector: 'redaction-paginator',
templateUrl: './paginator.component.html',
styleUrls: ['./paginator.component.scss'],
})
export class PaginatorComponent {
constructor(readonly pdf: PdfViewer, readonly documentViewer: REDDocumentViewer) {}
}

View File

@ -0,0 +1,5 @@
<div id="viewer"></div>
<redaction-compare-file-input></redaction-compare-file-input>
<redaction-paginator></redaction-paginator>

View File

@ -0,0 +1,12 @@
:host {
--workload-width: 350px;
--header-height: 111px;
}
div {
width: calc(100% - var(--workload-width));
height: calc(100% - var(--header-height));
bottom: 0;
left: 0;
position: absolute;
}

View File

@ -0,0 +1,8 @@
import { Component } from '@angular/core';
@Component({
selector: 'redaction-pdf-viewer',
templateUrl: './pdf-viewer.component.html',
styleUrls: ['./pdf-viewer.component.scss'],
})
export class PdfViewerComponent {}

View File

@ -0,0 +1,31 @@
import { NgModule } from '@angular/core';
import { PdfViewerComponent } from './pdf-viewer.component';
import { CommonModule } from '@angular/common';
import { CompareFileInputComponent } from './components/compare-file-input/compare-file-input.component';
import { PageRotationService } from './services/page-rotation.service';
import { REDAnnotationManager } from './services/annotation-manager.service';
import { PdfViewer } from './services/pdf-viewer.service';
import { TooltipsService } from './services/tooltips.service';
import { ViewerHeaderService } from './services/viewer-header.service';
import { PaginatorComponent } from './components/paginator/paginator.component';
import { MatIconModule } from '@angular/material/icon';
import { AnnotationDrawService } from './services/annotation-draw.service';
import { REDDocumentViewer } from './services/document-viewer.service';
import { WebViewerLoadedGuard } from './services/webviewer-loaded.guard';
@NgModule({
declarations: [PdfViewerComponent, CompareFileInputComponent, PaginatorComponent],
exports: [PdfViewerComponent],
imports: [CommonModule, MatIconModule],
providers: [
PdfViewer,
REDDocumentViewer,
REDAnnotationManager,
PageRotationService,
TooltipsService,
ViewerHeaderService,
AnnotationDrawService,
WebViewerLoadedGuard,
],
})
export class PdfViewerModule {}

View File

@ -1,20 +1,19 @@
import { Injectable } from '@angular/core';
import { Core } from '@pdftron/webviewer';
import { hexToRgb } from '../../../utils';
import { AnnotationWrapper } from '@models/file/annotation.wrapper';
import { UserPreferenceService } from '@services/user-preference.service';
import { RedactionLogService } from '@services/files/redaction-log.service';
import { environment } from '@environments/environment';
import { AnnotationWrapper } from '../../../models/file/annotation.wrapper';
import { UserPreferenceService } from '../../../services/user-preference.service';
import { RedactionLogService } from '../../../services/files/redaction-log.service';
import { IRectangle, ISectionGrid, ISectionRectangle } from '@red/domain';
import { SkippedService } from './skipped.service';
import { firstValueFrom } from 'rxjs';
import { DictionariesMapService } from '@services/entity-services/dictionaries-map.service';
import { DictionariesMapService } from '../../../services/entity-services/dictionaries-map.service';
import { SuperTypes } from '../../../models/file/super-types';
import { PdfViewer } from './pdf-viewer.service';
import { FilePreviewStateService } from './file-preview-state.service';
import { ViewModeService } from './view-mode.service';
import { FileDataService } from './file-data.service';
import { SuperTypes } from '@models/file/super-types';
import { ActivatedRoute } from '@angular/router';
import { REDAnnotationManager } from './annotation-manager.service';
import { List } from '@iqser/common-ui';
import { REDDocumentViewer } from './document-viewer.service';
import Annotation = Core.Annotations.Annotation;
import Quad = Core.Math.Quad;
@ -27,45 +26,26 @@ export class AnnotationDrawService {
private readonly _dictionariesMapService: DictionariesMapService,
private readonly _redactionLogService: RedactionLogService,
private readonly _userPreferenceService: UserPreferenceService,
private readonly _skippedService: SkippedService,
private readonly _activatedRoute: ActivatedRoute,
private readonly _annotationManager: REDAnnotationManager,
private readonly _pdf: PdfViewer,
private readonly _state: FilePreviewStateService,
private readonly _viewModeService: ViewModeService,
private readonly _fileDataService: FileDataService,
private readonly _documentViewer: REDDocumentViewer,
) {}
draw(annotations: readonly AnnotationWrapper[]) {
const licenseKey = environment.licenseKey ? atob(environment.licenseKey) : null;
return this._pdf.PDFNet.runWithCleanup(() => this._draw(annotations), licenseKey);
}
getColor(superType: string, dictionary?: string) {
let color: string;
switch (superType) {
case SuperTypes.Hint:
case SuperTypes.Redaction:
color = this._dictionariesMapService.getDictionaryColor(dictionary, this._state.dossierTemplateId);
break;
case SuperTypes.Recommendation:
color = this._dictionariesMapService.getDictionaryColor(dictionary, this._state.dossierTemplateId, true);
break;
case SuperTypes.Skipped:
color = this._dictionariesMapService.getDictionaryColor(superType, this._state.dossierTemplateId);
break;
default:
color = this._dictionariesMapService.getDictionaryColor(superType, this._state.dossierTemplateId);
break;
async draw(annotations: List<AnnotationWrapper>, dossierTemplateId: string, hideSkipped: boolean) {
try {
await this._draw(annotations, dossierTemplateId, hideSkipped);
} catch (e) {
console.log(e);
}
return color;
}
getAndConvertColor(superType: string, dictionary?: string) {
return this.convertColor(this.getColor(superType, dictionary));
getAndConvertColor(superType: string, dossierTemplateId: string, dictionary?: string) {
return this.convertColor(this.#getColor(superType, dossierTemplateId, dictionary));
}
convertColor(hexColor: string) {
const rgbColor = hexToRgb(hexColor);
return new this._pdf.Annotations.Color(rgbColor.r, rgbColor.g, rgbColor.b);
return this._pdf.color(hexToRgb(hexColor));
}
annotationToQuads(annotation: Annotation) {
@ -81,41 +61,57 @@ export class AnnotationDrawService {
const x4 = annotation.getRect().x1;
const y4 = annotation.getRect().y1;
return new this._pdf.instance.Core.Math.Quad(x1, y1, x2, y2, x3, y3, x4, y4);
return this._pdf.quad(x1, y1, x2, y2, x3, y3, x4, y4);
}
private async _draw(annotationWrappers: readonly AnnotationWrapper[]) {
const annotations = annotationWrappers.map(annotation => this._computeAnnotation(annotation)).filter(a => !!a);
this._pdf.annotationManager.addAnnotations(annotations, { imported: true });
await this._pdf.annotationManager.drawAnnotationsFromList(annotations);
#getColor(superType: string, dossierTemplateId: string, dictionary?: string) {
let color: string;
switch (superType) {
case SuperTypes.Hint:
case SuperTypes.Redaction:
color = this._dictionariesMapService.getDictionaryColor(dictionary, dossierTemplateId);
break;
case SuperTypes.Recommendation:
color = this._dictionariesMapService.getDictionaryColor(dictionary, dossierTemplateId, true);
break;
case SuperTypes.Skipped:
color = this._dictionariesMapService.getDictionaryColor(superType, dossierTemplateId);
break;
default:
color = this._dictionariesMapService.getDictionaryColor(superType, dossierTemplateId);
break;
}
return color;
}
private async _draw(annotationWrappers: List<AnnotationWrapper>, dossierTemplateId: string, hideSkipped: boolean) {
const annotations = annotationWrappers
.map(annotation => this._computeAnnotation(annotation, dossierTemplateId, hideSkipped))
.filter(a => !!a);
await this._annotationManager.add(annotations);
if (this._userPreferenceService.areDevFeaturesEnabled) {
const { dossierId, fileId } = this._state;
const { dossierId, fileId } = this._pdf;
const sectionsGrid$ = this._redactionLogService.getSectionGrid(dossierId, fileId);
const sectionsGrid = await firstValueFrom(sectionsGrid$).catch(() => ({ rectanglesPerPage: {} }));
await this._drawSections(sectionsGrid);
await this._drawSections(sectionsGrid, dossierTemplateId);
}
}
private async _drawSections(sectionGrid: ISectionGrid) {
private async _drawSections(sectionGrid: ISectionGrid, dossierTemplateId: string) {
const sections: Core.Annotations.RectangleAnnotation[] = [];
for (const page of Object.keys(sectionGrid.rectanglesPerPage)) {
const sectionRectangles = sectionGrid.rectanglesPerPage[page];
sectionRectangles.forEach(sectionRectangle => {
sections.push(this._computeSection(parseInt(page, 10), sectionRectangle));
// sectionRectangle.tableCells?.forEach(cell =>{
// sections.push(this.computeSection(activeViewer, parseInt(page, 10), cell));
// })
sections.push(this._computeSection(dossierTemplateId, parseInt(page, 10), sectionRectangle));
});
}
const annotationManager = this._pdf.annotationManager;
annotationManager.addAnnotations(sections, { imported: true });
await annotationManager.drawAnnotationsFromList(sections);
await this._annotationManager.add(sections);
}
private _computeSection(pageNumber: number, sectionRectangle: ISectionRectangle) {
const rectangleAnnot = new this._pdf.Annotations.RectangleAnnotation();
const pageHeight = this._pdf.documentViewer.getPageHeight(pageNumber);
private _computeSection(dossierTemplateId: string, pageNumber: number, sectionRectangle: ISectionRectangle) {
const rectangleAnnot = this._pdf.rectangle();
const pageHeight = this._documentViewer.getHeight(pageNumber);
const rectangle: IRectangle = {
topLeft: sectionRectangle.topLeft,
page: pageNumber,
@ -128,22 +124,22 @@ export class AnnotationDrawService {
rectangleAnnot.Width = rectangle.width + 2;
rectangleAnnot.Height = rectangle.height + 2;
rectangleAnnot.ReadOnly = true;
rectangleAnnot.StrokeColor = this.getAndConvertColor('analysis', 'analysis');
rectangleAnnot.StrokeColor = this.getAndConvertColor('analysis', dossierTemplateId, 'analysis');
rectangleAnnot.StrokeThickness = 1;
return rectangleAnnot;
}
private _computeAnnotation(annotationWrapper: AnnotationWrapper) {
const pageNumber = this._viewModeService.isCompare ? annotationWrapper.pageNumber * 2 - 1 : annotationWrapper.pageNumber;
private _computeAnnotation(annotationWrapper: AnnotationWrapper, dossierTemplateId: string, hideSkipped: boolean) {
const pageNumber = this._pdf.isCompare ? annotationWrapper.pageNumber * 2 - 1 : annotationWrapper.pageNumber;
if (pageNumber > this._pdf.pageCount) {
// skip imported annotations from files that have more pages than the current one
return;
}
if (annotationWrapper.superType === SuperTypes.TextHighlight) {
const rectangleAnnot = new this._pdf.Annotations.RectangleAnnotation();
const pageHeight = this._pdf.documentViewer.getPageHeight(pageNumber);
const rectangleAnnot = this._pdf.rectangle();
const pageHeight = this._documentViewer.getHeight(pageNumber);
const rectangle: IRectangle = annotationWrapper.positions[0];
rectangleAnnot.PageNumber = pageNumber;
rectangleAnnot.X = rectangle.topLeft.x;
@ -158,34 +154,37 @@ export class AnnotationDrawService {
return rectangleAnnot;
}
const annotation = new this._pdf.Annotations.TextHighlightAnnotation();
const annotation = this._pdf.textHighlight();
annotation.Quads = this._rectanglesToQuads(annotationWrapper.positions, pageNumber);
annotation.Opacity = annotationWrapper.isChangeLogRemoved ? DEFAULT_REMOVED_ANNOTATION_OPACITY : DEFAULT_TEXT_ANNOTATION_OPACITY;
annotation.setContents(annotationWrapper.content);
annotation.PageNumber = pageNumber;
annotation.StrokeColor = this.getAndConvertColor(annotationWrapper.superType, annotationWrapper.type);
annotation.StrokeColor = this.getAndConvertColor(annotationWrapper.superType, dossierTemplateId, annotationWrapper.type);
annotation.Id = annotationWrapper.id;
annotation.ReadOnly = true;
annotation.Hidden =
annotationWrapper.isChangeLogRemoved ||
(this._skippedService.hideSkipped && annotationWrapper.isSkipped) ||
(hideSkipped && annotationWrapper.isSkipped) ||
annotationWrapper.isOCR ||
this._fileDataService.isHidden(annotationWrapper.annotationId);
this._annotationManager.isHidden(annotationWrapper.annotationId);
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('redaction', 'preview')));
annotation.setCustomData('annotationColor', String(this.getColor(annotationWrapper.superType, annotationWrapper.type)));
annotation.setCustomData('redactionColor', String(this.#getColor('redaction', dossierTemplateId, 'preview')));
annotation.setCustomData(
'annotationColor',
String(this.#getColor(annotationWrapper.superType, dossierTemplateId, annotationWrapper.type)),
);
return annotation;
}
private _rectanglesToQuads(positions: IRectangle[], pageNumber: number): Quad[] {
const pageHeight = this._pdf.documentViewer.getPageHeight(pageNumber);
const pageHeight = this._documentViewer.getHeight(pageNumber);
return positions.map(p => this._rectangleToQuad(p, pageHeight));
}
@ -202,6 +201,6 @@ export class AnnotationDrawService {
const x4 = rectangle.topLeft.x;
const y4 = pageHeight - rectangle.topLeft.y;
return new this._pdf.instance.Core.Math.Quad(x1, y1, x2, y2, x3, y3, x4, y4);
return this._pdf.quad(x1, y1, x2, y2, x3, y3, x4, y4);
}
}

View File

@ -0,0 +1,129 @@
import { Injectable } from '@angular/core';
import { Core } from '@pdftron/webviewer';
import type { List } from '@iqser/common-ui';
import { AnnotationPredicate, DeleteAnnotationsOptions } from '../utils/types';
import { AnnotationWrapper } from '@models/file/annotation.wrapper';
import { fromEvent, Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
import { asList, getId, isStringOrWrapper } from '../utils/functions';
import AnnotationManager = Core.AnnotationManager;
import Annotation = Core.Annotations.Annotation;
@Injectable()
export class REDAnnotationManager {
annotationSelected$: Observable<[Annotation[], string]>;
readonly hidden = new Set<string>();
#manager: AnnotationManager;
get selected() {
return this.#manager.getSelectedAnnotations();
}
get annotations() {
return this.#get();
}
get #annotationSelected$() {
const onSelect$ = fromEvent<[Annotation[], string]>(this.#manager, 'annotationSelected');
return onSelect$.pipe(tap(value => console.log('Annotation selected: ', value)));
}
init(annotationManager: AnnotationManager) {
this.#manager = annotationManager;
this.annotationSelected$ = this.#annotationSelected$;
this.#autoSelectRectangleAfterCreation();
}
isHidden(annotationId: string) {
return this.hidden.has(annotationId);
}
delete(annotations?: List | List<AnnotationWrapper> | string | AnnotationWrapper) {
const items = isStringOrWrapper(annotations) ? [this.get(annotations)] : this.get(annotations);
const options: DeleteAnnotationsOptions = { force: true };
this.#manager.deleteAnnotations(items, options);
}
get(annotation: AnnotationWrapper | string): Annotation;
get(annotations: List | List<AnnotationWrapper>): Annotation[];
get(predicate?: (value: Annotation) => boolean): Annotation[];
get(argument?: AnnotationPredicate | List<AnnotationWrapper> | List | AnnotationWrapper | string): Annotation | Annotation[] {
if (isStringOrWrapper(argument)) {
return this.#getById(argument);
}
const isList = argument instanceof Array;
return isList ? this.#getByIds(argument) : this.#get(argument);
}
deselect(annotation: string | AnnotationWrapper);
deselect(annotations?: List | List<AnnotationWrapper>);
deselect(argument?: string | AnnotationWrapper | List | List<AnnotationWrapper>) {
if (!argument) {
return this.#manager.deselectAllAnnotations();
}
const ann = isStringOrWrapper(argument) ? [this.#getById(argument)] : this.#getByIds(argument);
this.#manager.deselectAnnotations(ann);
}
hide(annotations: Annotation[]): void {
this.#manager.hideAnnotations(annotations);
}
show(annotations: Annotation[]): void {
this.#manager.showAnnotations(annotations);
}
jumpAndSelect(annotations: List | List<AnnotationWrapper>) {
const annotationsFromViewer = this.get(annotations);
this.#manager.jumpToAnnotation(annotationsFromViewer[0]);
this.#manager.selectAnnotations(annotationsFromViewer);
}
select(annotations: Annotation | Annotation[]) {
this.#manager.selectAnnotations(asList(annotations));
}
redraw(annotations: Annotation | Annotation[]) {
annotations = asList(annotations);
annotations.forEach(annotation => this.#manager.redrawAnnotation(annotation));
}
add(annotations: Annotation | Annotation[]) {
try {
annotations = asList(annotations);
this.#manager.addAnnotations(annotations, { imported: true });
return this.#manager.drawAnnotationsFromList(annotations);
} catch (e) {
console.log(e);
}
}
#getById(annotation: AnnotationWrapper | string) {
return this.#manager.getAnnotationById(getId(annotation));
}
#autoSelectRectangleAfterCreation() {
this.#manager.addEventListener('annotationChanged', (annotations: Annotation[]) => {
// when a rectangle is drawn,
// it returns one annotation with tool name 'AnnotationCreateRectangle;
// this will auto select rectangle after drawing
if (annotations.length === 1 && annotations[0].ToolName === 'AnnotationCreateRectangle') {
this.#manager.selectAnnotations(annotations);
annotations[0].disableRotationControl();
}
});
}
#getByIds(annotations: List | List<AnnotationWrapper>) {
return annotations.map((item: string | AnnotationWrapper) => this.#getById(item)).filter(a => !!a);
}
#get(predicate?: AnnotationPredicate) {
const annotations = this.#manager.getAnnotationsList();
return predicate ? annotations.filter(predicate) : annotations;
}
}

View File

@ -0,0 +1,179 @@
import { Injectable } from '@angular/core';
import { Core } from '@pdftron/webviewer';
import { NGXLogger } from 'ngx-logger';
import { fromEvent, merge, Observable } from 'rxjs';
import { debounceTime, filter, map, tap } from 'rxjs/operators';
import { ActivatedRoute } from '@angular/router';
import { PdfViewer } from './pdf-viewer.service';
import { UserPreferenceService } from '@services/user-preference.service';
import { log, shareLast } from '@iqser/common-ui';
import { stopAndPrevent, stopAndPreventIfNotAllowed } from '../utils/functions';
import { RotationType, RotationTypes } from '@red/domain';
import DocumentViewer = Core.DocumentViewer;
import Color = Core.Annotations.Color;
import Quad = Core.Math.Quad;
@Injectable()
export class REDDocumentViewer {
loaded$: Observable<boolean>;
pageComplete$: Observable<boolean>;
keyUp$: Observable<KeyboardEvent>;
textSelected$: Observable<string>;
selectedText = '';
#document: DocumentViewer;
constructor(
private readonly _logger: NGXLogger,
private readonly _userPreferenceService: UserPreferenceService,
private readonly _pdf: PdfViewer,
private readonly _activatedRoute: ActivatedRoute,
) {}
get PDFDoc() {
return this.document?.getPDFDoc();
}
get document() {
return this.#document.getDocument();
}
get #documentUnloaded$() {
const event$ = fromEvent(this.#document, 'documentUnloaded');
const toBool$ = event$.pipe(map(() => false));
return toBool$.pipe(tap(() => this._logger.info('[PDF] Document unloaded')));
}
get #documentLoaded$() {
const event$ = fromEvent(this.#document, 'documentLoaded');
const toBool$ = event$.pipe(map(() => true));
return toBool$.pipe(
tap(() => this.#setCurrentPage()),
tap(() => this.#setInitialDisplayMode()),
tap(() => this.updateTooltipsVisibility()),
tap(() => this._logger.info('[PDF] Document loaded')),
);
}
get #keyUp$() {
return fromEvent<KeyboardEvent>(this.#document, 'keyUp').pipe(
tap(stopAndPreventIfNotAllowed),
filter($event => ($event.target as HTMLElement)?.tagName?.toLowerCase() !== 'input'),
filter($event => $event.key.startsWith('Arrow') || $event.key === 'f'),
tap(stopAndPrevent),
log('[PDF] Keyboard shortcut'),
);
}
get #pageComplete$() {
return fromEvent(this.#document, 'pageComplete').pipe(debounceTime(300));
}
get #textSelected$(): Observable<string> {
return fromEvent<[Quad, string, number]>(this.#document, 'textSelected').pipe(
tap(([, selectedText]) => (this.selectedText = selectedText)),
tap(([, , pageNumber]) => {
if (this._pdf.isCompare && pageNumber % 2 === 0) {
this._pdf.disable('textPopup');
} else {
this._pdf.enable('textPopup');
}
}),
map(([, selectedText]) => selectedText),
);
}
get #loaded$() {
return merge(this.#documentUnloaded$, this.#documentLoaded$).pipe(shareLast());
}
close() {
this._logger.info('[PDF] Closing document');
this.#document.closeDocument();
this._pdf.closeCompareMode();
}
updateTooltipsVisibility(): void {
const current = this._userPreferenceService.getFilePreviewTooltipsPreference();
this._pdf.instance.UI.setAnnotationContentOverlayHandler(() => (current ? undefined : false));
}
init(document: DocumentViewer) {
this.#document = document;
this.loaded$ = this.#loaded$;
this.pageComplete$ = this.#pageComplete$.pipe(shareLast());
this.keyUp$ = this.#keyUp$;
this.textSelected$ = this.#textSelected$;
}
async lock() {
const document = await this.PDFDoc;
if (!document) {
return false;
}
await document.lock();
this._logger.info('[PDF] Locked');
return true;
}
async blob() {
const data = await this.document.getFileData();
return new Blob([new Uint8Array(data)], { type: 'application/pdf' });
}
setRectangleToolStyles(color: Color) {
this.#document.getTool('AnnotationCreateRectangle').setStyles({
StrokeThickness: 2,
StrokeColor: color,
FillColor: color,
Opacity: 0.6,
});
}
getHeight(page: number) {
try {
return this.#document.getPageHeight(page);
} catch {
// might throw Error: getPageHeight was called before the 'documentLoaded' event
return 0;
}
}
refreshAndUpdateView() {
this.#document.refreshAll();
// do not adjust this page if is compare mode
const currentPage = this.#document.getCurrentPage();
this.#document.updateView([currentPage], currentPage);
}
resetRotation(pages: number | number[] | string | string[]) {
if (!(pages instanceof Array)) {
pages = [Number(pages)];
}
pages.forEach(page => this.#document.setRotation(0, Number(page)));
}
rotate(rotation: RotationType, page = this._pdf.currentPage) {
if (rotation === RotationTypes.LEFT) {
this.#document.rotateCounterClockwise(page);
} else {
this.#document.rotateClockwise(page);
}
}
#setCurrentPage() {
const currentDocPage = this._activatedRoute.snapshot.queryParamMap.get('page');
this.#document.setCurrentPage(Number(currentDocPage ?? '1'), false);
}
#setInitialDisplayMode() {
this._pdf.instance.UI.setFitMode('FitPage');
const displayModeManager = this.#document.getDisplayModeManager();
const instanceDisplayMode = displayModeManager.getDisplayMode();
instanceDisplayMode.mode = this._pdf.isCompare ? 'Facing' : 'Single';
displayModeManager.setDisplayMode(instanceDisplayMode);
}
}

View File

@ -0,0 +1,108 @@
import { Injectable, Injector } from '@angular/core';
import { BehaviorSubject, firstValueFrom, of } from 'rxjs';
import { RotationType } from '@red/domain';
import { FileManagementService } from '../../../services/files/file-management.service';
import { distinctUntilChanged, map, switchMap, tap } from 'rxjs/operators';
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';
import { FilesService } from '../../../services/files/files.service';
import { PdfViewer } from './pdf-viewer.service';
import { FilesMapService } from '../../../services/files/files-map.service';
import { NGXLogger } from 'ngx-logger';
import { REDDocumentViewer } from './document-viewer.service';
@Injectable()
export class PageRotationService {
readonly #rotations$ = new BehaviorSubject<Record<number, number>>({});
constructor(
private readonly _pdf: PdfViewer,
private readonly _loadingService: LoadingService,
private readonly _logger: NGXLogger,
private readonly _injector: Injector,
private readonly _fileManagementService: FileManagementService,
private readonly _filesService: FilesService,
private readonly _filesMapService: FilesMapService,
private readonly _documentViewer: REDDocumentViewer,
) {}
get hasRotations() {
return Object.values(this.#rotations$.value).filter(v => !!v).length > 0;
}
isRotated$(page: number) {
return this.#rotations$.pipe(
map(rotations => !!rotations[page]),
distinctUntilChanged(),
);
}
applyRotation() {
this._loadingService.start();
const pages = this.#rotations$.value;
const { dossierId, fileId } = this._pdf;
if (!dossierId || !fileId) {
this._loadingService.stop();
return this._logger.error('No dossier id or file id while applying rotations: ', dossierId, fileId);
}
const file = this._filesMapService.get(dossierId, fileId);
if (!file) {
this._loadingService.stop();
return this._logger.error('Cannot find file: ', dossierId, fileId);
}
const request$ = this._fileManagementService.rotatePage({ pages }, dossierId, fileId);
this.clearRotations();
const reloaded$ = request$.pipe(switchMap(() => this._filesService.reload(dossierId, file)));
return firstValueFrom(reloaded$.pipe(tap(() => this._loadingService.stop())));
}
discardRotation() {
this._documentViewer.resetRotation(Object.keys(this.#rotations$.value));
this.clearRotations();
}
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 });
this._documentViewer.rotate(rotation);
}
clearRotations() {
this.#rotations$.next({});
}
showConfirmationDialogIfHasRotations() {
return this.hasRotations ? this.#showConfirmationDialog() : of(false);
}
#showConfirmationDialog() {
const ref = this._injector.get(MatDialog).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'),
}),
});
const closed$ = ref.afterClosed().pipe(map((option: ConfirmOptions) => option === ConfirmOptions.CONFIRM));
return closed$.pipe(tap(apply => (apply ? this.applyRotation() : this.discardRotation())));
}
}

View File

@ -0,0 +1,283 @@
import { Inject, Injectable, Injector } from '@angular/core';
import WebViewer, { Core, WebViewerInstance, WebViewerOptions } from '@pdftron/webviewer';
import { environment } from '@environments/environment';
import { BASE_HREF_FN, BaseHrefFn } from '../../../tokens';
import { File, IHeaderElement } from '@red/domain';
import { ErrorService, shareDistinctLast } from '@iqser/common-ui';
import { ActivatedRoute } from '@angular/router';
import { map, startWith, tap } from 'rxjs/operators';
import { BehaviorSubject, combineLatest, fromEvent, Observable } from 'rxjs';
import { ConfigService } from '@services/config.service';
import { NGXLogger } from 'ngx-logger';
import { DISABLED_HOTKEYS, DOCUMENT_LOADING_ERROR, SEARCH_OPTIONS, USELESS_ELEMENTS } from '../utils/constants';
import { Rgb } from '../utils/types';
import { asList } from '../utils/functions';
import { REDAnnotationManager } from './annotation-manager.service';
import { TranslateService } from '@ngx-translate/core';
import TextTool = Core.Tools.TextTool;
import Annotation = Core.Annotations.Annotation;
import TextHighlightAnnotation = Core.Annotations.TextHighlightAnnotation;
import DocumentViewer = Core.DocumentViewer;
import Quad = Core.Math.Quad;
@Injectable()
export class PdfViewer {
readonly currentPage$ = this._activatedRoute.queryParamMap.pipe(
map(params => Number(params.get('page') ?? '1')),
shareDistinctLast(),
);
documentViewer: DocumentViewer;
fileId: string;
dossierId: string;
pageChanged$: Observable<number>;
compareMode$: Observable<boolean>;
totalPages$: Observable<number>;
#instance: WebViewerInstance;
readonly #compareMode$ = new BehaviorSubject(false);
readonly #searchButton: IHeaderElement = {
type: 'actionButton',
img: this._convertPath('/assets/icons/general/pdftron-action-search.svg'),
title: this._translateService.instant('pdf-viewer.text-popup.actions.search'),
onClick: () => {
this.#instance.UI.openElements(['searchPanel']);
setTimeout(() => this.#searchForSelectedText(), 250);
},
};
constructor(
private readonly _logger: NGXLogger,
private readonly _injector: Injector,
private readonly _activatedRoute: ActivatedRoute,
private readonly _translateService: TranslateService,
private readonly _annotationManager: REDAnnotationManager,
@Inject(BASE_HREF_FN) private readonly _convertPath: BaseHrefFn,
) {}
get instance() {
return this.#instance;
}
get PDFNet(): typeof Core.PDFNet {
return this.#instance.Core.PDFNet;
}
get isCompare() {
return this.#compareMode$.value;
}
get pageCount() {
try {
return this.#instance.Core.documentViewer.getPageCount();
} catch {
// might throw Error: getPageCount was called before the 'documentLoaded' event
return 1;
}
}
get currentPage() {
return this.#adjustPage(this.#currentInternalPage);
}
get #totalPages$() {
const layoutChanged$ = fromEvent(this.documentViewer, 'layoutChanged').pipe(startWith(''));
const pageCount$ = layoutChanged$.pipe(map(() => this.pageCount));
const docChanged$ = combineLatest([pageCount$, this.compareMode$]);
return docChanged$.pipe(map(([pageCount]) => this.#adjustPage(pageCount)));
}
get #paginationOffset() {
return this.isCompare ? 2 : 1;
}
get #currentInternalPage() {
return this.documentViewer.getCurrentPage();
}
get #pageChanged$() {
const page$ = fromEvent<number>(this.documentViewer, 'pageNumberUpdated');
return page$.pipe(
tap(() => this._annotationManager.deselect()),
map(page => this.#adjustPage(page)),
);
}
navigateTo(page: string | number) {
const parsedNumber = typeof page === 'string' ? parseInt(page, 10) : page;
const paginationOffset = this.#paginationOffset;
this.#navigateTo(paginationOffset === 2 ? parsedNumber * paginationOffset - 1 : parsedNumber);
}
navigatePreviousPage() {
if (this.#currentInternalPage > 1) {
this.#navigateTo(Math.max(this.#currentInternalPage - this.#paginationOffset, 1));
}
}
navigateNextPage() {
const pageCount = this.pageCount;
if (this.#currentInternalPage < pageCount) {
this.#navigateTo(Math.min(this.#currentInternalPage + this.#paginationOffset, pageCount));
}
}
async init(htmlElement: HTMLElement) {
this.#instance = await this.#getInstance(htmlElement);
await this.PDFNet.initialize(environment.licenseKey ? window.atob(environment.licenseKey) : null);
this._logger.info('[PDF] Initialized');
this.documentViewer = this.#instance.Core.documentViewer;
this.compareMode$ = this.#compareMode$.asObservable();
this.pageChanged$ = this.#pageChanged$.pipe(shareDistinctLast());
this.totalPages$ = this.#totalPages$.pipe(shareDistinctLast());
this.#setSelectionMode();
this.#configureElements();
this.#disableHotkeys();
this.#clearSearchResultsWhenVisibilityChanged();
return this.#instance;
}
enable(dataElements: string[] | string) {
this.#instance.UI.enableElements(asList(dataElements));
}
disable(dataElements: string[] | string) {
this.#instance.UI.disableElements(asList(dataElements));
}
openCompareMode() {
this._logger.info('[PDF] Open compare mode');
this.#compareMode$.next(true);
}
closeCompareMode() {
this._logger.info('[PDF] Close compare mode');
this.#compareMode$.next(false);
}
async loadDocument(blob: Blob, file: File) {
const onError = () => {
this._injector.get(ErrorService).set(DOCUMENT_LOADING_ERROR);
this._logger.error('[PDF] Error while loading document');
// this.stateService.reloadBlob();
};
const document = await this.PDFNet.PDFDoc.createFromBuffer(await blob.arrayBuffer());
await document.flattenAnnotations(false);
this.fileId = file.fileId;
this.dossierId = file.dossierId;
this.#instance.UI.loadDocument(document, { filename: file?.filename + '.pdf' ?? 'document.pdf', onError });
}
quad(x1: number, y1: number, x2: number, y2: number, x3: number, y3: number, x4: number, y4: number) {
return new this.#instance.Core.Math.Quad(x1, y1, x2, y2, x3, y3, x4, y4);
}
translateQuad(page: number, quad: Quad): Quad {
const rotationsEnum = this.#instance.Core.PageRotation;
switch (this.documentViewer.getCompleteRotation(page)) {
case rotationsEnum.E_90:
return this.quad(quad.x2, quad.y2, quad.x3, quad.y3, quad.x4, quad.y4, quad.x1, quad.y1);
case rotationsEnum.E_180:
return this.quad(quad.x3, quad.y3, quad.x4, quad.y4, quad.x1, quad.y1, quad.x2, quad.y2);
case rotationsEnum.E_270:
return this.quad(quad.x4, quad.y4, quad.x1, quad.y1, quad.x2, quad.y2, quad.x3, quad.y3);
case rotationsEnum.E_0:
default:
return quad;
}
}
color(rgb: Rgb) {
return new this.#instance.Core.Annotations.Color(rgb.r, rgb.g, rgb.b);
}
rectangle() {
return new this.#instance.Core.Annotations.RectangleAnnotation();
}
textHighlight() {
return new this.#instance.Core.Annotations.TextHighlightAnnotation();
}
isTextHighlight(annotation: Annotation): annotation is TextHighlightAnnotation {
return annotation instanceof this.#instance.Core.Annotations.TextHighlightAnnotation;
}
configureTextPopups(popups: IHeaderElement[]) {
this.#instance.UI.textPopup.update([]);
this.#instance.UI.textPopup.add([...popups, this.#searchButton]);
}
#adjustPage(page: number) {
if (this.isCompare) {
if (page % 2 === 1) {
page = page + 1;
}
return page / 2;
}
return page;
}
#searchForSelectedText() {
this.#instance.UI.searchTextFull(this.documentViewer.getSelectedText(), SEARCH_OPTIONS);
}
#clearSearchResultsWhenVisibilityChanged() {
const iframeWindow = this.#instance.UI.iframeWindow;
iframeWindow.addEventListener('visibilityChanged', (event: any) => {
if (event.detail.element !== 'searchPanel') {
return;
}
const inputElement = iframeWindow.document.getElementById('SearchPanel__input') as HTMLInputElement;
setTimeout(() => (inputElement.value = ''), 0);
if (!event.detail.isVisible) {
this.documentViewer.clearSearchResults();
}
});
}
#navigateTo(pageNumber: number) {
if (this.#currentInternalPage !== pageNumber) {
this.documentViewer.displayPageLocation(pageNumber, 0, 0);
}
}
#disableHotkeys(): void {
DISABLED_HOTKEYS.forEach(key => this.#instance.UI.hotkeys.off(key));
}
#configureElements() {
this.#instance.UI.disableElements(USELESS_ELEMENTS);
}
#setSelectionMode(): void {
const textTool = this.#instance.Core.Tools.TextTool as unknown as TextTool;
const configService = this._injector.get(ConfigService);
textTool.SELECTION_MODE = configService.values.SELECTION_MODE;
}
#getInstance(htmlElement: HTMLElement) {
const options: WebViewerOptions = {
licenseKey: environment.licenseKey ? window.atob(environment.licenseKey) : null,
fullAPI: true,
path: this._convertPath('/assets/wv-resources'),
css: this._convertPath('/assets/pdftron/stylesheet.css'),
backendType: 'ems',
};
return WebViewer(options, htmlElement);
}
}

View File

@ -0,0 +1,42 @@
import { Inject, Injectable } from '@angular/core';
import { UserPreferenceService } from '../../../services/user-preference.service';
import { HeaderElements } from '../../file-preview/utils/constants';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { TranslateService } from '@ngx-translate/core';
import { BASE_HREF_FN, BaseHrefFn } from '../../../tokens';
import { PdfViewer } from './pdf-viewer.service';
import { REDDocumentViewer } from './document-viewer.service';
@Injectable()
export class TooltipsService {
readonly #enableIcon = this._convertPath('/assets/icons/general/pdftron-action-enable-tooltips.svg');
readonly #disableIcon = this._convertPath('/assets/icons/general/pdftron-action-disable-tooltips.svg');
constructor(
@Inject(BASE_HREF_FN) private readonly _convertPath: BaseHrefFn,
private readonly _pdf: PdfViewer,
private readonly _documentViewer: REDDocumentViewer,
private readonly _userPreferenceService: UserPreferenceService,
private readonly _translateService: TranslateService,
) {}
get toggleTooltipsBtnTitle(): string {
return this._translateService.instant(_('pdf-viewer.toggle-tooltips'), {
active: this._userPreferenceService.getFilePreviewTooltipsPreference(),
});
}
get toggleTooltipsBtnIcon(): string {
const tooltipsDisabled = this._userPreferenceService.getFilePreviewTooltipsPreference();
return tooltipsDisabled ? this.#enableIcon : this.#disableIcon;
}
async toggleTooltips(): Promise<void> {
await this._userPreferenceService.toggleFilePreviewTooltipsPreference();
this._documentViewer.updateTooltipsVisibility();
this._pdf.instance.UI.updateElement(HeaderElements.TOGGLE_TOOLTIPS, {
title: this.toggleTooltipsBtnTitle,
img: this.toggleTooltipsBtnIcon,
});
}
}

View File

@ -1,22 +1,23 @@
import { ElementRef, Inject, Injectable, Injector } from '@angular/core';
import { Inject, Injectable, Injector } from '@angular/core';
import { IHeaderElement, RotationTypes } from '@red/domain';
import { HeaderElements, HeaderElementType } from '../utils/constants';
import { HeaderElements, HeaderElementType } from '../../file-preview/utils/constants';
import { TranslateService } from '@ngx-translate/core';
import { BASE_HREF } from '../../../tokens';
import { PdfViewer } from './pdf-viewer.service';
import { BASE_HREF_FN, BaseHrefFn } from '../../../tokens';
import { TooltipsService } from './tooltips.service';
import { environment } from '@environments/environment';
import { ViewModeService } from './view-mode.service';
import { FilePreviewStateService } from './file-preview-state.service';
import { PageRotationService } from './page-rotation.service';
import { PdfViewer } from './pdf-viewer.service';
import { ROTATION_ACTION_BUTTONS } from '../utils/constants';
import { FilesMapService } from '@services/files/files-map.service';
import { REDDocumentViewer } from './document-viewer.service';
const divider: IHeaderElement = {
type: 'divider',
};
@Injectable()
export class ViewerHeaderConfigService {
private _divider: IHeaderElement = {
type: 'divider',
};
private _buttons: Map<HeaderElementType, IHeaderElement>;
private _config: Map<HeaderElementType, boolean> = new Map<HeaderElementType, boolean>([
export class ViewerHeaderService {
#buttons: Map<HeaderElementType, IHeaderElement>;
readonly #config = new Map<HeaderElementType, boolean>([
[HeaderElements.SHAPE_TOOL_GROUP_BUTTON, true],
[HeaderElements.TOGGLE_TOOLTIPS, true],
[HeaderElements.COMPARE_BUTTON, true],
@ -26,16 +27,16 @@ export class ViewerHeaderConfigService {
[HeaderElements.APPLY_ROTATION, false],
[HeaderElements.DISCARD_ROTATION, false],
]);
private _rotationService: PageRotationService;
#docBeforeCompare: Blob;
constructor(
@Inject(BASE_HREF) private readonly _baseHref: string,
@Inject(BASE_HREF_FN) private readonly _convertPath: BaseHrefFn,
private readonly _injector: Injector,
private readonly _translateService: TranslateService,
private readonly _pdfViewer: PdfViewer,
private readonly _pdf: PdfViewer,
private readonly _documentViewer: REDDocumentViewer,
private readonly _rotationService: PageRotationService,
private readonly _tooltipsService: TooltipsService,
private readonly _viewModeService: ViewModeService,
private readonly _stateService: FilePreviewStateService,
) {}
private get _rectangle(): IHeaderElement {
@ -68,9 +69,7 @@ export class ViewerHeaderConfigService {
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();
},
onClick: () => this._closeCompareMode(),
};
}
@ -80,7 +79,10 @@ export class ViewerHeaderConfigService {
element: HeaderElements.ROTATE_LEFT_BUTTON,
dataElement: HeaderElements.ROTATE_LEFT_BUTTON,
img: this._convertPath('/assets/icons/general/rotate-left.svg'),
onClick: () => this._rotationService.addRotation(RotationTypes.LEFT),
onClick: () => {
this._rotationService.addRotation(RotationTypes.LEFT);
this.#toggleRotationActionButtons();
},
};
}
@ -100,6 +102,7 @@ export class ViewerHeaderConfigService {
`;
paragraph.addEventListener('click', async () => {
await this._rotationService.applyRotation();
this.disable(ROTATION_ACTION_BUTTONS);
});
return paragraph;
},
@ -120,7 +123,10 @@ export class ViewerHeaderConfigService {
cursor: pointer;
opacity: 0.7;
`;
paragraph.addEventListener('click', () => this._rotationService.discardRotation());
paragraph.addEventListener('click', () => {
this._rotationService.discardRotation();
this.disable(ROTATION_ACTION_BUTTONS);
});
return paragraph;
},
};
@ -132,25 +138,40 @@ export class ViewerHeaderConfigService {
element: HeaderElements.ROTATE_RIGHT_BUTTON,
dataElement: HeaderElements.ROTATE_RIGHT_BUTTON,
img: this._convertPath('/assets/icons/general/rotate-right.svg'),
onClick: () => this._rotationService.addRotation(RotationTypes.RIGHT),
onClick: () => {
this._rotationService.addRotation(RotationTypes.RIGHT);
this.#toggleRotationActionButtons();
},
};
}
initialize(compareFileInput: ElementRef): void {
this._rotationService = this._injector.get<PageRotationService>(PageRotationService);
private get _compare(): IHeaderElement {
return {
type: 'actionButton',
element: HeaderElements.COMPARE_BUTTON,
dataElement: HeaderElements.COMPARE_BUTTON,
img: this._convertPath('/assets/icons/general/pdftron-action-compare.svg'),
title: 'Compare',
onClick: async () => {
document.getElementById('compareFileInput').click();
this.#docBeforeCompare = await this._documentViewer.blob();
},
};
}
this._buttons = new Map([
init(): void {
this.#buttons = new Map([
[HeaderElements.SHAPE_TOOL_GROUP_BUTTON, this._rectangle],
[HeaderElements.ROTATE_LEFT_BUTTON, this._rotateLeft],
[HeaderElements.ROTATE_RIGHT_BUTTON, this._rotateRight],
[HeaderElements.APPLY_ROTATION, this._applyRotation],
[HeaderElements.DISCARD_ROTATION, this._discardRotation],
[HeaderElements.TOGGLE_TOOLTIPS, this._toggleTooltips],
[HeaderElements.COMPARE_BUTTON, this._compare(compareFileInput)],
[HeaderElements.COMPARE_BUTTON, this._compare],
[HeaderElements.CLOSE_COMPARE_BUTTON, this._closeCompare],
]);
this._updateElements();
this.updateElements();
}
enable(elements: HeaderElementType[]): void {
@ -161,43 +182,8 @@ export class ViewerHeaderConfigService {
this._updateState(elements, false);
}
private async _closeCompareMode() {
this._viewModeService.compareMode = false;
const pdfNet = this._pdfViewer.instance.Core.PDFNet;
await pdfNet.initialize(environment.licenseKey ? atob(environment.licenseKey) : null);
const blob = await this._stateService.blob;
const currentDocument = await pdfNet.PDFDoc.createFromBuffer(await blob.arrayBuffer());
const filename = this._stateService.file.filename ?? 'document.pdf';
this._pdfViewer.instance.UI.loadDocument(currentDocument, { filename });
this.disable([HeaderElements.CLOSE_COMPARE_BUTTON]);
this.enable([HeaderElements.COMPARE_BUTTON]);
this._pdfViewer.navigateToPage(1);
}
private _compare(compareFileInput: ElementRef): IHeaderElement {
return {
type: 'actionButton',
element: HeaderElements.COMPARE_BUTTON,
dataElement: HeaderElements.COMPARE_BUTTON,
img: this._convertPath('/assets/icons/general/pdftron-action-compare.svg'),
title: 'Compare',
onClick: () => compareFileInput.nativeElement.click(),
};
}
private _pushGroup(items: IHeaderElement[], group: HeaderElementType[]): void {
const enabledItems = group.filter(item => this._isEnabled(item));
if (enabledItems.length) {
items.push(this._divider);
enabledItems.forEach(item => items.push(this._buttons.get(item)));
}
}
private _updateElements(): void {
this._pdfViewer.instance.UI.setHeaderItems(header => {
updateElements(): void {
this._pdf.instance.UI.setHeaderItems(header => {
const enabledItems: IHeaderElement[] = [];
const groups: HeaderElementType[][] = [
[HeaderElements.COMPARE_BUTTON, HeaderElements.CLOSE_COMPARE_BUTTON],
@ -215,16 +201,46 @@ export class ViewerHeaderConfigService {
});
}
resetCompareButtons() {
this.disable([HeaderElements.CLOSE_COMPARE_BUTTON]);
this.enable([HeaderElements.COMPARE_BUTTON]);
}
#toggleRotationActionButtons() {
if (this._rotationService.hasRotations) {
this.enable(ROTATION_ACTION_BUTTONS);
} else {
this.disable(ROTATION_ACTION_BUTTONS);
}
}
private _closeCompareMode() {
this._pdf.closeCompareMode();
const { dossierId, fileId } = this._pdf;
const file = this._injector.get(FilesMapService).get(dossierId, fileId);
const filename = file.filename ?? 'document.pdf';
this._pdf.instance.UI.loadDocument(this.#docBeforeCompare, { filename });
this.resetCompareButtons();
this._pdf.navigateTo(1);
}
private _pushGroup(items: IHeaderElement[], group: HeaderElementType[]): void {
const enabledItems = group.filter(item => this._isEnabled(item));
if (enabledItems.length) {
items.push(divider);
enabledItems.forEach(item => items.push(this.#buttons.get(item)));
}
}
private _updateState(elements: HeaderElementType[], value: boolean): void {
elements.forEach(element => this._config.set(element, value));
this._updateElements();
elements.forEach(element => this.#config.set(element, value));
this.updateElements();
}
private _isEnabled(key: HeaderElementType): boolean {
return this._config.get(key);
}
private _convertPath(path: string): string {
return this._baseHref + path;
return this.#config.get(key);
}
}

View File

@ -0,0 +1,33 @@
import { Injectable } from '@angular/core';
import { CanActivate } from '@angular/router';
import { REDDocumentViewer } from './document-viewer.service';
import { PdfViewer } from './pdf-viewer.service';
import { REDAnnotationManager } from './annotation-manager.service';
import { ViewerHeaderService } from './viewer-header.service';
import { LoadingService } from '@iqser/common-ui';
@Injectable()
export class WebViewerLoadedGuard implements CanActivate {
constructor(
private readonly _documentViewer: REDDocumentViewer,
private readonly _pdf: PdfViewer,
private readonly _annotationManager: REDAnnotationManager,
private readonly _viewerHeaderService: ViewerHeaderService,
private readonly _loadingService: LoadingService,
) {}
async canActivate() {
if (this._pdf.instance) {
return true;
}
this._loadingService.start();
const instance = await this._pdf.init(document.getElementById('viewer'));
this._annotationManager.init(instance.Core.annotationManager);
this._documentViewer.init(instance.Core.documentViewer);
this._viewerHeaderService.init();
return true;
}
}

View File

@ -0,0 +1,88 @@
import { CustomError, List } from '@iqser/common-ui';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { HeaderElements, TextPopups } from '../../file-preview/utils/constants';
export const ROTATION_ACTION_BUTTONS = [HeaderElements.APPLY_ROTATION, HeaderElements.DISCARD_ROTATION];
export const TEXT_POPUPS_TO_TOGGLE = [TextPopups.ADD_REDACTION, TextPopups.ADD_RECTANGLE, TextPopups.ADD_FALSE_POSITIVE];
export const HEADER_ITEMS_TO_TOGGLE = [
HeaderElements.SHAPE_TOOL_GROUP_BUTTON,
HeaderElements.ROTATE_LEFT_BUTTON,
HeaderElements.ROTATE_RIGHT_BUTTON,
];
export const ALLOWED_ACTIONS_WHEN_PAGE_EXCLUDED: string[] = [
TextPopups.ADD_RECTANGLE,
TextPopups.ADD_REDACTION,
HeaderElements.SHAPE_TOOL_GROUP_BUTTON,
];
export const ALLOWED_KEYBOARD_SHORTCUTS: List = ['+', '-', 'p', 'r', 'Escape'] as const;
export const DOCUMENT_LOADING_ERROR = new CustomError(_('error.file-preview.label'), _('error.file-preview.action'), 'iqser:refresh');
export const SEARCH_OPTIONS = {
caseSensitive: true, // match case
wholeWord: true, // match whole words only
wildcard: false, // allow using '*' as a wildcard value
regex: false, // string is treated as a regular expression
searchUp: false, // search from the end of the document upwards
ambientString: true, // return ambient string as part of the result
};
export const USELESS_ELEMENTS = [
'pageNavOverlay',
'menuButton',
'selectToolButton',
'textHighlightToolButton',
'textUnderlineToolButton',
'textSquigglyToolButton',
'textStrikeoutToolButton',
'viewControlsButton',
'contextMenuPopup',
'linkButton',
'toggleNotesButton',
'notesPanel',
'thumbnailControl',
'documentControl',
'ribbons',
'toolsHeader',
'rotateClockwiseButton',
'rotateCounterClockwiseButton',
'annotationStyleEditButton',
'annotationGroupButton',
];
export const DISABLED_HOTKEYS = [
'CTRL+SHIFT+EQUAL',
'COMMAND+SHIFT+EQUAL',
'CTRL+SHIFT+MINUS',
'COMMAND+SHIFT+MINUS',
'CTRL+V',
'COMMAND+V',
'CTRL+Y',
'COMMAND+Y',
'CTRL+O',
'COMMAND+O',
'CTRL+P',
'COMMAND+P',
'SPACE',
'UP',
'DOWN',
'R',
'P',
'A',
'C',
'E',
'I',
'L',
'N',
'O',
'T',
'S',
'G',
'H',
'K',
'U',
] as const;

View File

@ -0,0 +1,48 @@
import { List } from '@iqser/common-ui';
import { AnnotationWrapper } from '../../../models/file/annotation.wrapper';
import { ALLOWED_KEYBOARD_SHORTCUTS } from './constants';
export function stopAndPrevent<T extends Event>($event: T) {
$event.preventDefault();
$event.stopPropagation();
}
export function stopAndPreventIfNotAllowed($event: KeyboardEvent) {
if (!ALLOWED_KEYBOARD_SHORTCUTS.includes($event.key)) {
stopAndPrevent($event);
}
}
export function getId(item: string | AnnotationWrapper) {
return typeof item === 'string' ? item : item.annotationId;
}
export function getIds(items?: List | List<AnnotationWrapper>): List | undefined {
return items?.map(getId);
}
export function isStringOrWrapper(value: unknown): value is string | AnnotationWrapper {
return typeof value === 'string' || value instanceof AnnotationWrapper;
}
export function asList<T>(items: T[] | T): T[];
export function asList(items: string[] | string): string[];
export function asList(items: AnnotationWrapper[] | AnnotationWrapper): AnnotationWrapper[];
export function asList<T>(
items: string[] | string | AnnotationWrapper[] | AnnotationWrapper | T | T[],
): string[] | AnnotationWrapper[] | T[] {
if (typeof items === 'string') {
return [items];
}
if (items instanceof AnnotationWrapper) {
return [items];
}
const isArray = items instanceof Array;
if (!isArray) {
return [items];
}
return items;
}

View File

@ -0,0 +1,17 @@
import { Core } from '@pdftron/webviewer';
import Annotation = Core.Annotations.Annotation;
export interface Rgb {
readonly r: number;
readonly g: number;
readonly b: number;
}
export interface DeleteAnnotationsOptions {
readonly imported?: boolean;
readonly force?: boolean;
readonly isUndoRedo?: boolean;
readonly source?: string;
}
export type AnnotationPredicate = (value: Annotation) => boolean;

View File

@ -1,4 +1,14 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, HostBinding, Input, OnChanges, Optional, ViewChild } from '@angular/core';
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
HostBinding,
Injector,
Input,
OnChanges,
Optional,
ViewChild,
} from '@angular/core';
import { PermissionsService } from '@services/permissions.service';
import { Action, ActionTypes, Dossier, File } from '@red/domain';
import { DossiersDialogService } from '../../services/dossiers-dialog.service';
@ -23,8 +33,10 @@ import { ExcludedPagesService } from '../../../file-preview/services/excluded-pa
import { DocumentInfoService } from '../../../file-preview/services/document-info.service';
import { ExpandableFileActionsComponent } from '@shared/components/expandable-file-actions/expandable-file-actions.component';
import { firstValueFrom, Observable } from 'rxjs';
import { PageRotationService } from '../../../file-preview/services/page-rotation.service';
import { PageRotationService } from '../../../pdf-viewer/services/page-rotation.service';
import { FileAssignService } from '../../services/file-assign.service';
import { ViewerHeaderService } from '../../../pdf-viewer/services/viewer-header.service';
import { ROTATION_ACTION_BUTTONS } from '../../../pdf-viewer/utils/constants';
@Component({
selector: 'redaction-file-actions [file] [type] [dossier]',
@ -34,7 +46,7 @@ import { FileAssignService } from '../../services/file-assign.service';
})
export class FileActionsComponent implements OnChanges {
readonly circleButtonTypes = CircleButtonTypes;
readonly currentUser = this._userService.currentUser;
readonly currentUser;
@Input() file: File;
@Input() dossier: Dossier;
@ -75,23 +87,25 @@ export class FileActionsComponent implements OnChanges {
private readonly _expandableActionsComponent: ExpandableFileActionsComponent;
constructor(
@Optional() private readonly _excludedPagesService: ExcludedPagesService,
@Optional() private readonly _documentInfoService: DocumentInfoService,
@Optional() private readonly _pageRotationService: PageRotationService,
private readonly _permissionsService: PermissionsService,
private readonly _activeDossiersService: ActiveDossiersService,
userService: UserService,
private readonly _injector: Injector,
private readonly _filesService: FilesService,
private readonly _changeRef: ChangeDetectorRef,
private readonly _loadingService: LoadingService,
private readonly _dialogService: DossiersDialogService,
private readonly _fileAssignService: FileAssignService,
private readonly _loadingService: LoadingService,
private readonly _fileManagementService: FileManagementService,
private readonly _filesService: FilesService,
private readonly _userService: UserService,
private readonly _toaster: Toaster,
private readonly _userPreferenceService: UserPreferenceService,
private readonly _reanalysisService: ReanalysisService,
private readonly _router: Router,
private readonly _changeRef: ChangeDetectorRef,
) {}
private readonly _permissionsService: PermissionsService,
private readonly _pageRotationService: PageRotationService,
private readonly _viewerHeaderService: ViewerHeaderService,
private readonly _activeDossiersService: ActiveDossiersService,
private readonly _fileManagementService: FileManagementService,
private readonly _userPreferenceService: UserPreferenceService,
@Optional() private readonly _documentInfoService: DocumentInfoService,
@Optional() private readonly _excludedPagesService: ExcludedPagesService,
) {
this.currentUser = userService.currentUser;
}
@HostBinding('class.keep-visible')
get expanded() {
@ -309,9 +323,9 @@ export class FileActionsComponent implements OnChanges {
try {
const dossier = this._activeDossiersService.find(this.file.dossierId);
await firstValueFrom(this._fileManagementService.delete([this.file], this.file.dossierId));
await this._router.navigate([dossier.routerLink]);
await this._injector.get(Router).navigate([dossier.routerLink]);
} catch (error) {
this._toaster.error(_('error.http.generic'), { params: error });
this._injector.get(Toaster).error(_('error.http.generic'), { params: error });
}
this._loadingService.stop();
},
@ -360,9 +374,12 @@ export class FileActionsComponent implements OnChanges {
return;
}
}
if (this._pageRotationService) {
await firstValueFrom(this._pageRotationService.showConfirmationDialogIfHasRotations());
}
const pageRotationService = this._injector.get(PageRotationService);
await firstValueFrom(pageRotationService.showConfirmationDialogIfHasRotations());
const viewerHeaderService = this._injector.get(ViewerHeaderService);
viewerHeaderService.disable(ROTATION_ACTION_BUTTONS);
this._loadingService.start();
await firstValueFrom(this._reanalysisService.ocrFiles([this.file], this.file.dossierId));
this._loadingService.stop();

View File

@ -28,7 +28,11 @@ export class FileAssignService {
async assignToMe(files: File[]): Promise<unknown> {
const assignReq = async () => {
this._loadingService.start();
await firstValueFrom(this._filesService.setAssignee(files, files[0].dossierId, this.currentUser.id));
if (files[0].isNew) {
await this._makeAssignFileRequest(this.currentUser.id, 'UNDER_REVIEW', files);
} else {
await firstValueFrom(this._filesService.setAssignee(files, files[0].dossierId, this.currentUser.id));
}
this._loadingService.stop();
};
if (atLeastOneAssignee(files)) {

View File

@ -1,13 +1,17 @@
import { Injectable } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { AddDossierDialogComponent } from '../dialogs/add-dossier-dialog/add-dossier-dialog.component';
import { DialogConfig, DialogService } from '@iqser/common-ui';
import { ConfirmationDialogComponent, DialogConfig, DialogService } from '@iqser/common-ui';
type DialogType = 'addDossier';
type DialogType = 'addDossier' | 'confirm';
@Injectable()
export class SharedDialogService extends DialogService<DialogType> {
protected readonly _config: DialogConfig<DialogType> = {
confirm: {
component: ConfirmationDialogComponent,
dialogConfig: { disableClose: false },
},
addDossier: {
component: AddDossierDialogComponent,
dialogConfig: { width: '900px', autoFocus: true },

View File

@ -114,9 +114,11 @@ export class BreadcrumbsService {
}
private _addDossierTemplateDropdown(params: Record<string, string>): void {
const breadcrumbs = this._dashboardStatsService.all
.filter(dt => !dt.isEmpty)
.map(dt => this._dossierTemplateBreadcrumb({ dossierTemplateId: dt.id }));
const breadcrumbs = [this._dashboardBreadcrumb].concat(
this._dashboardStatsService.all
.filter(dt => !dt.isEmpty)
.map(dt => this._dossierTemplateBreadcrumb({ dossierTemplateId: dt.id })),
);
const activeOption = breadcrumbs.find(b => b.options.routerLink[1] === params[DOSSIER_TEMPLATE_ID]);

View File

@ -3,7 +3,7 @@ import { HttpClient } from '@angular/common/http';
import { Title } from '@angular/platform-browser';
import packageInfo from '../../../../../package.json';
import envConfig from '../../assets/config/config.json';
import { CacheApiService, wipeCaches } from '@red/cache';
import { CacheApiService, wipeAllCaches, wipeCaches } from '@red/cache';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
import { AppConfig } from '@red/domain';
@ -28,15 +28,6 @@ export class ConfigService {
return this._values;
}
loadAppConfig(): Observable<any> {
return this._httpClient.get('/app-config').pipe(
tap(config => {
console.log('[REDACTION] Loaded config: ', config);
this._values = { ...this._values, ...config };
}),
);
}
loadLocalConfig(): Observable<any> {
return this._httpClient.get<any>('/assets/config/config.json').pipe(
tap(config => {
@ -56,7 +47,7 @@ export class ConfigService {
console.log('[REDACTION] Last app version: ', lastVersion, ' current version ', version);
if (lastVersion !== version) {
console.warn('[REDACTION] Version-mismatch - wiping caches!');
await wipeCaches();
await wipeAllCaches();
}
await this._cacheApiService.cacheValue('FRONTEND_APP_VERSION', version);
});

View File

@ -4,13 +4,13 @@ import { Dossier, File, IDossier, IFile, TrashDossier, TrashFile, TrashItem } fr
import { catchError, switchMap, take, tap } from 'rxjs/operators';
import { forkJoin, map, Observable, of } from 'rxjs';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { ConfigService } from '../config.service';
import { PermissionsService } from '../permissions.service';
import { ActiveDossiersService } from '../dossiers/active-dossiers.service';
import { UserService } from '../user.service';
import { flatMap } from 'lodash-es';
import { DossierStatsService } from '../dossiers/dossier-stats.service';
import { FilesService } from '../files/files.service';
import { SystemPreferencesService } from '@services/system-preferences.service';
@Injectable({
providedIn: 'root',
@ -19,7 +19,7 @@ export class TrashService extends EntitiesService<TrashItem> {
constructor(
protected readonly _injector: Injector,
private readonly _toaster: Toaster,
private readonly _configService: ConfigService,
private readonly _systemPreferencesService: SystemPreferencesService,
private readonly _permissionsService: PermissionsService,
private readonly _activeDossiersService: ActiveDossiersService,
private readonly _userService: UserService,
@ -64,7 +64,7 @@ export class TrashService extends EntitiesService<TrashItem> {
dossier =>
new TrashDossier(
dossier,
this._configService.values.softDeleteCleanupTime,
this._systemPreferencesService.values.softDeleteCleanupTime,
this._permissionsService.canRestoreDossier(dossier),
this._permissionsService.canHardDeleteDossier(dossier),
),
@ -82,7 +82,7 @@ export class TrashService extends EntitiesService<TrashItem> {
return new TrashFile(
file,
dossier.dossierTemplateId,
this._configService.values.softDeleteCleanupTime,
this._systemPreferencesService.values.softDeleteCleanupTime,
this._permissionsService.canRestoreFile(file, dossier),
this._permissionsService.canHardDeleteFile(file, dossier),
);

View File

@ -8,13 +8,14 @@ export class LoggerRulesService extends NGXLoggerRulesService {
if (message && typeof message === 'string') {
const matches = message.match('\\[(.*?)\\]');
const firstMatch = matches[1]?.toUpperCase();
if (matches && matches.length > 0 && config.features[firstMatch]) {
if (matches && matches.length > 0) {
const firstMatch = matches[1]?.toUpperCase();
const featureConfig = config.features[firstMatch];
if (!featureConfig.enabled || (featureConfig.level && featureConfig?.level < config.level)) {
return false;
if (featureConfig) {
if (!featureConfig.enabled || (featureConfig.level && featureConfig.level < config.level)) {
return false;
}
}
}
}

View File

@ -294,9 +294,12 @@ export class PermissionsService {
return dossier.isActive && file.isUnderReview && this.isDossierMember(dossier);
}
/** UNDER_APPROVAL => UNDER_REVIEW */
/** UNDER_APPROVAL => UNDER_REVIEW OR NEW => UNDER_REVIEW */
private _canSetUnderReview(file: File, dossier: Dossier): boolean {
return dossier.isActive && file.isUnderApproval && this.isAssigneeOrApprover(file, dossier);
return (
dossier.isActive &&
((file.isUnderApproval && this.isAssigneeOrApprover(file, dossier)) || (file.isNew && this.isDossierMember(dossier)))
);
}
/** UNDER_APPROVAL => APPROVED */

View File

@ -0,0 +1,29 @@
import { Injectable, Injector } from '@angular/core';
import { GenericService } from '@iqser/common-ui';
import { SystemPreferences } from '@red/domain';
import { Observable, switchMap } from 'rxjs';
import { tap } from 'rxjs/operators';
@Injectable({
providedIn: 'root',
})
export class SystemPreferencesService extends GenericService<SystemPreferences> {
values: SystemPreferences;
constructor(protected readonly _injector: Injector) {
super(_injector, 'app-config');
}
loadPreferences(): Observable<SystemPreferences> {
return this.get<SystemPreferences>().pipe(
tap((config: SystemPreferences) => {
console.log('[REDACTION] Loaded config: ', config);
this.values = config;
}),
);
}
update(value: SystemPreferences): Observable<SystemPreferences> {
return this._post(value).pipe(switchMap(() => this.loadPreferences()));
}
}

View File

@ -1,7 +1,9 @@
import { InjectionToken } from '@angular/core';
export const BASE_HREF: InjectionToken<string> = new InjectionToken<string>('BASE_HREF');
export const DOSSIER_ID: InjectionToken<string> = new InjectionToken<string>('DOSSIER_ID');
export const BASE_HREF = new InjectionToken<string>('BASE_HREF');
export type BaseHrefFn = (path: string) => string;
export const BASE_HREF_FN = new InjectionToken<BaseHrefFn>('Convert path function');
export const DOSSIER_ID = new InjectionToken<string>('DOSSIER_ID');
export const ACTIVE_DOSSIERS_SERVICE = new InjectionToken<string>('Active dossiers service');
export const ARCHIVED_DOSSIERS_SERVICE = new InjectionToken<string>('Archived dossiers service');

View File

@ -0,0 +1,22 @@
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { SystemPreferences } from '@red/domain';
import { KeysOf } from '@iqser/common-ui';
export const systemPreferencesTranslations: Record<'label' | 'placeholder', Record<KeysOf<SystemPreferences>, string>> = {
label: {
softDeleteCleanupTime: _('general-config-screen.system-preferences.labels.soft-delete-cleanup-time'),
downloadCleanupDownloadFilesHours: _('general-config-screen.system-preferences.labels.download-cleanup-download-files-hours'),
downloadCleanupNotDownloadFilesHours: _(
'general-config-screen.system-preferences.labels.download-cleanup-not-download-files-hours',
),
removeDigitalSignaturesOnUpload: _('general-config-screen.system-preferences.labels.remove-digital-signature-on-upload'),
},
placeholder: {
softDeleteCleanupTime: _('general-config-screen.system-preferences.placeholders.soft-delete-cleanup-time'),
downloadCleanupDownloadFilesHours: _('general-config-screen.system-preferences.placeholders.download-cleanup-download-files-hours'),
downloadCleanupNotDownloadFilesHours: _(
'general-config-screen.system-preferences.placeholders.download-cleanup-not-download-files-hours',
),
removeDigitalSignaturesOnUpload: _('general-config-screen.system-preferences.placeholders.remove-digital-signature-on-upload'),
},
} as const;

View File

@ -8,6 +8,7 @@ import { LanguageService } from '@i18n/language.service';
import { UserPreferenceService } from '@services/user-preference.service';
import { UserService } from '@services/user.service';
import { FeaturesService } from '@services/features.service';
import { SystemPreferencesService } from '@services/system-preferences.service';
function lastDossierTemplateRedirect(baseHref: string, userPreferenceService: UserPreferenceService) {
const url = window.location.href.split('/').filter(s => s.length > 0);
@ -22,6 +23,7 @@ export function configurationInitializer(
keycloakService: KeycloakService,
title: Title,
configService: ConfigService,
systemPreferencesService: SystemPreferencesService,
featuresService: FeaturesService,
generalSettingsService: GeneralSettingsService,
languageService: LanguageService,
@ -39,7 +41,7 @@ export function configurationInitializer(
switchMap(user => (!user.hasAnyREDRoles ? throwError('Not user has no red roles') : of({}))),
mergeMap(() => generalSettingsService.getGeneralConfigurations()),
tap(configuration => configService.updateDisplayName(configuration.displayName)),
switchMap(() => configService.loadAppConfig()),
switchMap(() => systemPreferencesService.loadPreferences()),
switchMap(() => userPreferenceService.reload()),
catchError(e => {
console.log('[Redaction] Initialization error:', e);

View File

@ -11,5 +11,4 @@ export * from './functions';
export * from './global-error-handler.service';
export * from './missing-translations-handler';
export * from './page-stamper';
export * from './pdf-coordinates';
export * from './pruning-translation-loader';

View File

@ -1,55 +0,0 @@
import { Core } from '@pdftron/webviewer';
import Quad = Core.Math.Quad;
enum PageRotation {
E_0 = 0,
E_90 = 1,
E_180 = 2,
E_270 = 3,
}
export function translateQuads(page: number, rotation: number, quad: Quad): Quad {
let result;
switch (rotation) {
case PageRotation.E_90:
result = {
x1: quad.x2,
x2: quad.x3,
x3: quad.x4,
x4: quad.x1,
y1: quad.y2,
y2: quad.y3,
y3: quad.y4,
y4: quad.y1,
};
break;
case PageRotation.E_180:
result = {
x1: quad.x3,
x2: quad.x4,
x3: quad.x1,
x4: quad.x2,
y1: quad.y3,
y2: quad.y4,
y3: quad.y1,
y4: quad.y2,
};
break;
case PageRotation.E_270:
result = {
x1: quad.x4,
x2: quad.x1,
x3: quad.x2,
x4: quad.x3,
y1: quad.y4,
y2: quad.y1,
y3: quad.y2,
y4: quad.y3,
};
break;
case PageRotation.E_0:
default:
result = quad;
}
return result;
}

View File

@ -1468,6 +1468,19 @@
"title": "Allgemeine Einstellungen"
},
"subtitle": "SMTP (Simple Mail Transfer Protocol) ermöglicht es Ihnen, Ihre E-Mails über die angegebenen Servereinstellungen zu versenden.",
"system-preferences": {
"labels": {
"download-cleanup-download-files-hours": "",
"download-cleanup-not-download-files-hours": "",
"soft-delete-cleanup-time": ""
},
"placeholders": {
"download-cleanup-download-files-hours": "",
"download-cleanup-not-download-files-hours": "",
"soft-delete-cleanup-time": ""
},
"title": ""
},
"test": {
"error": "Die Test-E-Mail konnte nicht gesendet werden! Bitte überprüfen Sie die E-Mail-Adresse.",
"success": "Die Test-E-Mail wurde erfolgreich versendet!"
@ -1915,6 +1928,7 @@
"top-bar": {
"navigation-items": {
"back": "Zurück",
"back-to-dashboard": "",
"dashboard": "",
"my-account": {
"children": {

View File

@ -955,7 +955,7 @@
},
"dossier-template-stats": {
"active-dossiers": "Active {count, plural, one{Dossier} other{Dossiers}}",
"analyzed-pages": "<strong>{count}</strong> {count, plural, one{Page} other {Pages}} Analysed",
"analyzed-pages": "<strong>{count}</strong> {count, plural, one{Page} other {Pages}} analyzed",
"archived-dossiers": "<strong>{count}</strong> {count, plural, one{Dossier} other {Dossiers}} in Archive",
"deleted-dossiers": "<strong>{count}</strong> {count, plural, one{Dossier} other {Dossiers}} in Trash",
"total-documents": "Total Documents",
@ -1467,6 +1467,21 @@
"title": "General Configurations"
},
"subtitle": "SMTP (Simple Mail Transfer Protocol) enables you to send your emails through the specified server settings.",
"system-preferences": {
"labels": {
"download-cleanup-download-files-hours": "Cleanup time for downloaded files (hours)",
"download-cleanup-not-download-files-hours": "Cleanup time for not downloaded files (hours)",
"soft-delete-cleanup-time": "Soft delete cleanup time (hours)",
"remove-digital-signature-on-upload": "Remove Digital Signature on Upload"
},
"placeholders": {
"download-cleanup-download-files-hours": "(hours)",
"download-cleanup-not-download-files-hours": "(hours)",
"soft-delete-cleanup-time": "(hours)",
"remove-digital-signature-on-upload": "True / False"
},
"title": "System Preferences"
},
"test": {
"error": "Test email could not be sent! Please revise the email address.",
"success": "Test email was sent successfully!"
@ -1550,7 +1565,7 @@
"table-header": "{length} {length, plural, one{justification} other{justifications}}"
},
"license-info-screen": {
"analyzed-pages": "Analyzed Pages",
"analyzed-pages": "Analyzed pages",
"backend-version": "Backend Application Version",
"chart": {
"cumulative": "Cumulative Pages",
@ -1914,6 +1929,7 @@
"top-bar": {
"navigation-items": {
"back": "Back",
"back-to-dashboard": "Back to Dashboard",
"dashboard": "Dashboard",
"my-account": {
"children": {

View File

@ -4,5 +4,6 @@
"outDir": "../../dist/out-tsc",
"allowSyntheticDefaultImports": true
},
"files": ["src/main.ts", "src/polyfills.ts"]
"files": ["src/main.ts", "src/polyfills.ts"],
"exclude": ["jest.config.ts"]
}

View File

@ -6,5 +6,5 @@
"types": ["jest", "node"]
},
"files": ["src/test-setup.ts"],
"include": ["**/*.spec.ts", "**/*.d.ts"]
"include": ["**/*.spec.ts", "**/*.d.ts", "jest.config.ts"]
}

View File

@ -1,3 +1,3 @@
const { getJestProjects } = require('@nrwl/jest');
module.exports = { projects: getJestProjects() };
export default { projects: getJestProjects() };

View File

@ -1,11 +1,11 @@
const nxPreset = require('@nrwl/jest/preset');
const nxPreset = require('@nrwl/jest/preset').default;
module.exports = {
...nxPreset,
testMatch: ['**/+(*.)+(spec|test).+(ts|js)?(x)'],
transform: {
'^.+\\.(ts|js|html)$': 'ts-jest'
'^.+\\.(ts|js|html)$': 'ts-jest',
},
resolver: '@nrwl/jest/plugins/resolver',
moduleFileExtensions: ['ts', 'js', 'html'],
coverageReporters: ['html']
coverageReporters: ['html'],
};

@ -1 +1 @@
Subproject commit 1b146037facaa4d3e5a662fea64f67c0dd8b0110
Subproject commit ea3c8b7f32be0e23c0b47a50bfda524d0b58107d

View File

@ -1,4 +1,5 @@
module.exports = {
/* eslint-disable */
export default {
preset: '../../jest.preset.js',
setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],
globals: {

View File

@ -19,6 +19,13 @@ export const DYNAMIC_CACHES = [
},
];
export async function wipeAllCaches() {
const keys = await caches.keys();
for (const cache of keys) {
await caches.delete(cache);
}
}
export async function wipeCaches(logoutDependant: boolean = false) {
await caches.delete(APP_LEVEL_CACHE);
for (const cache of DYNAMIC_CACHES) {

View File

@ -14,6 +14,6 @@
"strictMetadataEmit": true,
"enableResourceInlining": true
},
"exclude": ["src/test-setup.ts", "**/*.spec.ts"],
"exclude": ["src/test-setup.ts", "**/*.spec.ts", "jest.config.ts"],
"include": ["**/*.ts"]
}

View File

@ -6,5 +6,5 @@
"types": ["jest", "node"]
},
"files": ["src/test-setup.ts"],
"include": ["**/*.spec.ts", "**/*.d.ts"]
"include": ["**/*.spec.ts", "**/*.d.ts", "jest.config.ts"]
}

View File

@ -1,4 +1,5 @@
module.exports = {
/* eslint-disable */
export default {
displayName: 'red-domain',
preset: '../../jest.preset.js',
setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],

View File

@ -154,4 +154,8 @@ export class File extends Entity<IFile> implements IFile {
const routerPath = this.dossierArchived ? ARCHIVE_ROUTE : DOSSIERS_ROUTE;
return this.canBeOpened ? `/main/${this.dossierTemplateId}/${routerPath}/${this.dossierId}/file/${this.fileId}` : undefined;
}
isPageExcluded(page: number): boolean {
return this.excludedPages.includes(page);
}
}

View File

@ -20,7 +20,4 @@ export interface AppConfig {
RECENT_PERIOD_IN_HOURS: number;
SELECTION_MODE: string;
MANUAL_BASE_URL: string;
softDeleteCleanupTime?: number;
downloadCleanupDownloadFilesHours?: number;
downloadCleanupNotDownloadFilesHours?: number;
}

View File

@ -12,3 +12,4 @@ export * from './logger-config';
export * from './admin-side-nav-types';
export * from './charts';
export * from './app-config';
export * from './system-preferences';

View File

@ -0,0 +1,6 @@
export interface SystemPreferences {
softDeleteCleanupTime: number;
downloadCleanupDownloadFilesHours: number;
downloadCleanupNotDownloadFilesHours: number;
removeDigitalSignaturesOnUpload: boolean;
}

View File

@ -10,6 +10,6 @@
"types": [],
"lib": ["dom", "es2018"]
},
"exclude": ["src/test-setup.ts", "**/*.spec.ts"],
"exclude": ["src/test-setup.ts", "**/*.spec.ts", "jest.config.ts"],
"include": ["**/*.ts"]
}

Some files were not shown because too many files have changed in this diff Show More