Merge branch 'master' into VM/RED-3982
This commit is contained in:
commit
88a3625f80
@ -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"]
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
module.exports = {
|
||||
/* eslint-disable */
|
||||
export default {
|
||||
preset: '../../jest.preset.js',
|
||||
setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],
|
||||
globals: {
|
||||
@ -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/**/*.*"]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -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],
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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,
|
||||
) {}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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,
|
||||
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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>
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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',
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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)));
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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();
|
||||
});
|
||||
|
||||
@ -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>
|
||||
@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
];
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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]);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 {}
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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'), {
|
||||
|
||||
@ -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)),
|
||||
);
|
||||
}
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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'));
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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> {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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) {
|
||||
|
||||
@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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)),
|
||||
};
|
||||
};
|
||||
|
||||
@ -0,0 +1 @@
|
||||
<input #input (input)="upload($event.target['files'])" accept="application/pdf" id="compareFileInput" type="file" />
|
||||
@ -0,0 +1,6 @@
|
||||
:host {
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
@ -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);
|
||||
@ -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) {}
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
<div id="viewer"></div>
|
||||
|
||||
<redaction-compare-file-input></redaction-compare-file-input>
|
||||
|
||||
<redaction-paginator></redaction-paginator>
|
||||
@ -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;
|
||||
}
|
||||
@ -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 {}
|
||||
31
apps/red-ui/src/app/modules/pdf-viewer/pdf-viewer.module.ts
Normal file
31
apps/red-ui/src/app/modules/pdf-viewer/pdf-viewer.module.ts
Normal 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 {}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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())));
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
88
apps/red-ui/src/app/modules/pdf-viewer/utils/constants.ts
Normal file
88
apps/red-ui/src/app/modules/pdf-viewer/utils/constants.ts
Normal 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;
|
||||
48
apps/red-ui/src/app/modules/pdf-viewer/utils/functions.ts
Normal file
48
apps/red-ui/src/app/modules/pdf-viewer/utils/functions.ts
Normal 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;
|
||||
}
|
||||
17
apps/red-ui/src/app/modules/pdf-viewer/utils/types.ts
Normal file
17
apps/red-ui/src/app/modules/pdf-viewer/utils/types.ts
Normal 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;
|
||||
@ -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();
|
||||
|
||||
@ -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)) {
|
||||
|
||||
@ -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 },
|
||||
|
||||
@ -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]);
|
||||
|
||||
|
||||
@ -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);
|
||||
});
|
||||
|
||||
@ -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),
|
||||
);
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 */
|
||||
|
||||
29
apps/red-ui/src/app/services/system-preferences.service.ts
Normal file
29
apps/red-ui/src/app/services/system-preferences.service.ts
Normal 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()));
|
||||
}
|
||||
}
|
||||
@ -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');
|
||||
|
||||
@ -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;
|
||||
@ -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);
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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": {
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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"]
|
||||
}
|
||||
|
||||
@ -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"]
|
||||
}
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
const { getJestProjects } = require('@nrwl/jest');
|
||||
|
||||
module.exports = { projects: getJestProjects() };
|
||||
export default { projects: getJestProjects() };
|
||||
@ -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
|
||||
@ -1,4 +1,5 @@
|
||||
module.exports = {
|
||||
/* eslint-disable */
|
||||
export default {
|
||||
preset: '../../jest.preset.js',
|
||||
setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],
|
||||
globals: {
|
||||
@ -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) {
|
||||
|
||||
@ -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"]
|
||||
}
|
||||
|
||||
@ -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"]
|
||||
}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
module.exports = {
|
||||
/* eslint-disable */
|
||||
export default {
|
||||
displayName: 'red-domain',
|
||||
preset: '../../jest.preset.js',
|
||||
setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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';
|
||||
|
||||
6
libs/red-domain/src/lib/shared/system-preferences.ts
Normal file
6
libs/red-domain/src/lib/shared/system-preferences.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export interface SystemPreferences {
|
||||
softDeleteCleanupTime: number;
|
||||
downloadCleanupDownloadFilesHours: number;
|
||||
downloadCleanupNotDownloadFilesHours: number;
|
||||
removeDigitalSignaturesOnUpload: boolean;
|
||||
}
|
||||
@ -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
Loading…
x
Reference in New Issue
Block a user