diff --git a/apps/red-ui/src/app/components/notifications/notifications.component.ts b/apps/red-ui/src/app/components/notifications/notifications.component.ts index 01a4289f4..49f816095 100644 --- a/apps/red-ui/src/app/components/notifications/notifications.component.ts +++ b/apps/red-ui/src/app/components/notifications/notifications.component.ts @@ -6,9 +6,9 @@ import { UserService } from '@services/user.service'; import { DossiersService } from '@services/entity-services/dossiers.service'; import { NotificationsService } from '@services/notifications.service'; import { Notification } from '@red/domain'; -import { distinctUntilChanged, map, shareReplay } from 'rxjs/operators'; +import { distinctUntilChanged, map } from 'rxjs/operators'; import { BehaviorSubject, Observable } from 'rxjs'; -import { List } from '@iqser/common-ui'; +import { List, shareLast } from '@iqser/common-ui'; interface NotificationsGroup { date: string; @@ -25,7 +25,7 @@ export class NotificationsComponent implements OnInit { notifications$: Observable; hasUnreadNotifications$: Observable; groupedNotifications$: Observable; - private _notifications$ = new BehaviorSubject([]); + private _notifications$ = new BehaviorSubject([]); constructor( private readonly _translateService: TranslateService, @@ -35,12 +35,12 @@ export class NotificationsComponent implements OnInit { private readonly _dossiersService: DossiersService, private readonly _datePipe: DatePipe, ) { - this.notifications$ = this._notifications$.asObservable(); + this.notifications$ = this._notifications$.asObservable().pipe(shareLast()); this.groupedNotifications$ = this.notifications$.pipe(map(notifications => this._groupNotifications(notifications))); this.hasUnreadNotifications$ = this.notifications$.pipe( map(notifications => notifications.filter(n => !n.readDate).length > 0), distinctUntilChanged(), - shareReplay(1), + shareLast(), ); } diff --git a/apps/red-ui/src/app/models/file/file-data.model.ts b/apps/red-ui/src/app/models/file/file-data.model.ts index 4a82bd0a4..6407c15fd 100644 --- a/apps/red-ui/src/app/models/file/file-data.model.ts +++ b/apps/red-ui/src/app/models/file/file-data.model.ts @@ -24,7 +24,7 @@ export class FileDataModel { const entries: RedactionLogEntryWrapper[] = this._convertData(); let allAnnotations = entries .map(entry => AnnotationWrapper.fromData(entry)) - .filter(ann => !this.file.excludedPages.includes(ann.pageNumber)); + .filter(ann => ann.manual || !this.file.excludedPages.includes(ann.pageNumber)); if (!areDevFeaturesEnabled) { allAnnotations = allAnnotations.filter(annotation => !annotation.isFalsePositive); diff --git a/apps/red-ui/src/app/models/file/manual-redaction-entry.wrapper.ts b/apps/red-ui/src/app/models/file/manual-redaction-entry.wrapper.ts index 39714983c..79504dea3 100644 --- a/apps/red-ui/src/app/models/file/manual-redaction-entry.wrapper.ts +++ b/apps/red-ui/src/app/models/file/manual-redaction-entry.wrapper.ts @@ -1,10 +1,18 @@ import { IManualRedactionEntry } from '@red/domain'; +export const ManualRedactionEntryTypes = { + DICTIONARY: 'DICTIONARY', + REDACTION: 'REDACTION', + FALSE_POSITIVE: 'FALSE_POSITIVE', +} as const; + +export type ManualRedactionEntryType = keyof typeof ManualRedactionEntryTypes; + export class ManualRedactionEntryWrapper { constructor( readonly quads: any, readonly manualRedactionEntry: IManualRedactionEntry, - readonly type: 'DICTIONARY' | 'REDACTION' | 'FALSE_POSITIVE', + readonly type: ManualRedactionEntryType, readonly annotationType: 'TEXT' | 'RECTANGLE' = 'TEXT', readonly rectId?: string, ) {} diff --git a/apps/red-ui/src/app/modules/dossier/components/file-workload/file-workload.component.html b/apps/red-ui/src/app/modules/dossier/components/file-workload/file-workload.component.html index ad93dcfe9..7ee814bf5 100644 --- a/apps/red-ui/src/app/modules/dossier/components/file-workload/file-workload.component.html +++ b/apps/red-ui/src/app/modules/dossier/components/file-workload/file-workload.component.html @@ -88,6 +88,7 @@ [activeSelection]="pageHasSelection(pageNumber)" [active]="pageNumber === activeViewerPage" [number]="pageNumber" + [showDottedIcon]="hasOnlyManualRedactionsAndNotExcluded(pageNumber)" [viewedPages]="fileData?.viewedPages" > @@ -104,11 +105,21 @@
-
- - {{ activeViewerPage }} - - {{ activeAnnotations?.length || 0 }} - +
+ + + + + {{ activeViewerPage }} - + {{ activeAnnotations?.length || 0 }} + +
@@ -141,13 +152,9 @@ [verticalPadding]="40" icon="iqser:document" > - + {{ 'file-preview.tabs.annotations.page-is' | translate }} - . @@ -173,11 +180,11 @@ diff --git a/apps/red-ui/src/app/modules/dossier/components/file-workload/file-workload.component.scss b/apps/red-ui/src/app/modules/dossier/components/file-workload/file-workload.component.scss index 7636f6bec..46fc452ab 100644 --- a/apps/red-ui/src/app/modules/dossier/components/file-workload/file-workload.component.scss +++ b/apps/red-ui/src/app/modules/dossier/components/file-workload/file-workload.component.scss @@ -167,3 +167,11 @@ } } } + +.padding-left-0 { + padding-left: 0 !important; +} + +::ng-deep .page-separator iqser-circle-button mat-icon { + color: var(--iqser-primary); +} diff --git a/apps/red-ui/src/app/modules/dossier/components/file-workload/file-workload.component.ts b/apps/red-ui/src/app/modules/dossier/components/file-workload/file-workload.component.ts index abeb97c36..6f7328ee1 100644 --- a/apps/red-ui/src/app/modules/dossier/components/file-workload/file-workload.component.ts +++ b/apps/red-ui/src/app/modules/dossier/components/file-workload/file-workload.component.ts @@ -90,6 +90,10 @@ export class FileWorkloadComponent { return !this._permissionsService.canPerformAnnotationActions(); } + get isExcluded(): boolean { + return this.fileData?.file?.excludedPages?.includes(this.activeViewerPage); + } + private get _firstSelectedAnnotation() { return this.selectedAnnotations?.length ? this.selectedAnnotations[0] : null; } @@ -114,6 +118,11 @@ export class FileWorkloadComponent { } } + hasOnlyManualRedactionsAndNotExcluded(pageNumber: number): boolean { + const hasOnlyManualRedactions = this.displayedAnnotations.get(pageNumber).every(annotation => annotation.manual); + return hasOnlyManualRedactions && this.fileData.file.excludedPages.includes(pageNumber); + } + pageHasSelection(page: number) { return this.multiSelectActive && !!this.selectedAnnotations?.find(a => a.pageNumber === page); } @@ -225,6 +234,10 @@ export class FileWorkloadComponent { this.selectPage.emit(this._nextPageWithAnnotations()); } + viewExcludePages(): void { + this.actionPerformed.emit('view-exclude-pages'); + } + private _filterAnnotations( annotations: AnnotationWrapper[], primary: INestedFilter[], diff --git a/apps/red-ui/src/app/modules/dossier/components/page-indicator/page-indicator.component.html b/apps/red-ui/src/app/modules/dossier/components/page-indicator/page-indicator.component.html index c7db169f4..420ce5f46 100644 --- a/apps/red-ui/src/app/modules/dossier/components/page-indicator/page-indicator.component.html +++ b/apps/red-ui/src/app/modules/dossier/components/page-indicator/page-indicator.component.html @@ -6,7 +6,7 @@ [id]="'quick-nav-page-' + number" class="page-wrapper" > - +
{{ number }}
diff --git a/apps/red-ui/src/app/modules/dossier/components/page-indicator/page-indicator.component.ts b/apps/red-ui/src/app/modules/dossier/components/page-indicator/page-indicator.component.ts index 29ea3b674..b99b2bad1 100644 --- a/apps/red-ui/src/app/modules/dossier/components/page-indicator/page-indicator.component.ts +++ b/apps/red-ui/src/app/modules/dossier/components/page-indicator/page-indicator.component.ts @@ -2,18 +2,19 @@ import { Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output, S import { AppStateService } from '@state/app-state.service'; import { PermissionsService } from '@services/permissions.service'; import { ConfigService } from '@services/config.service'; -import { Subscription } from 'rxjs'; import { DossiersService } from '@services/entity-services/dossiers.service'; import { ViewedPagesService } from '../../shared/services/viewed-pages.service'; import { IViewedPage } from '@red/domain'; +import { AutoUnsubscribe } from '@iqser/common-ui'; @Component({ selector: 'redaction-page-indicator', templateUrl: './page-indicator.component.html', styleUrls: ['./page-indicator.component.scss'], }) -export class PageIndicatorComponent implements OnChanges, OnInit, OnDestroy { +export class PageIndicatorComponent extends AutoUnsubscribe implements OnChanges, OnInit, OnDestroy { @Input() active: boolean; + @Input() showDottedIcon = false; @Input() number: number; @Input() viewedPages: IViewedPage[]; @Input() activeSelection = false; @@ -22,7 +23,6 @@ export class PageIndicatorComponent implements OnChanges, OnInit, OnDestroy { pageReadTimeout: number = null; canMarkPagesAsViewed: boolean; - private _subscription: Subscription; constructor( private readonly _viewedPagesService: ViewedPagesService, @@ -30,7 +30,9 @@ export class PageIndicatorComponent implements OnChanges, OnInit, OnDestroy { private readonly _dossiersService: DossiersService, private readonly _configService: ConfigService, private readonly _permissionService: PermissionsService, - ) {} + ) { + super(); + } get activePage() { return this.viewedPages?.find(p => p.page === this.number); @@ -46,7 +48,7 @@ export class PageIndicatorComponent implements OnChanges, OnInit, OnDestroy { } ngOnInit(): void { - this._subscription = this._appStateService.fileChanged$.subscribe(() => { + this.addSubscription = this._appStateService.fileChanged$.subscribe(() => { if (this.canMarkPagesAsViewed !== this._permissionService.canMarkPagesAsViewed()) { this.canMarkPagesAsViewed = this._permissionService.canMarkPagesAsViewed(); this._handlePageRead(); @@ -70,24 +72,21 @@ export class PageIndicatorComponent implements OnChanges, OnInit, OnDestroy { } } - ngOnDestroy(): void { - if (this._subscription) { - this._subscription.unsubscribe(); - } - } - private _handlePageRead() { - if (this.canMarkPagesAsViewed) { - if (this.pageReadTimeout) { - clearTimeout(this.pageReadTimeout); - } - if (this.active && !this.read) { - this.pageReadTimeout = window.setTimeout(() => { - if (this.active && !this.read) { - this._markPageRead(); - } - }, this._configService.values.AUTO_READ_TIME * 1000); - } + if (!this.canMarkPagesAsViewed) { + return; + } + + if (this.pageReadTimeout) { + clearTimeout(this.pageReadTimeout); + } + + if (this.active && !this.read) { + this.pageReadTimeout = window.setTimeout(() => { + if (this.active && !this.read) { + this._markPageRead(); + } + }, this._configService.values.AUTO_READ_TIME * 1000); } } @@ -109,7 +108,7 @@ export class PageIndicatorComponent implements OnChanges, OnInit, OnDestroy { // } private _markPageRead() { - this._viewedPagesService + this.addSubscription = this._viewedPagesService .addPage({ page: this.number }, this._dossiersService.activeDossierId, this._appStateService.activeFileId) .subscribe(() => { if (this.activePage) { @@ -121,7 +120,7 @@ export class PageIndicatorComponent implements OnChanges, OnInit, OnDestroy { } private _markPageUnread() { - this._viewedPagesService + this.addSubscription = this._viewedPagesService .removePage(this._dossiersService.activeDossierId, this._appStateService.activeFileId, this.number) .subscribe(() => { this.viewedPages?.splice( diff --git a/apps/red-ui/src/app/modules/dossier/components/pdf-viewer/pdf-viewer.component.ts b/apps/red-ui/src/app/modules/dossier/components/pdf-viewer/pdf-viewer.component.ts index 4cd21ba90..e460b7e02 100644 --- a/apps/red-ui/src/app/modules/dossier/components/pdf-viewer/pdf-viewer.component.ts +++ b/apps/red-ui/src/app/modules/dossier/components/pdf-viewer/pdf-viewer.component.ts @@ -14,7 +14,11 @@ import { import { File, IManualRedactionEntry, ViewMode } from '@red/domain'; import WebViewer, { Core, WebViewerInstance } from '@pdftron/webviewer'; import { TranslateService } from '@ngx-translate/core'; -import { ManualRedactionEntryWrapper } from '@models/file/manual-redaction-entry.wrapper'; +import { + ManualRedactionEntryType, + ManualRedactionEntryTypes, + ManualRedactionEntryWrapper, +} from '@models/file/manual-redaction-entry.wrapper'; import { AnnotationWrapper } from '@models/file/annotation.wrapper'; import { ManualAnnotationService } from '../../services/manual-annotation.service'; import { environment } from '@environments/environment'; @@ -33,6 +37,20 @@ import Tools = Core.Tools; import TextTool = Tools.TextTool; import Annotation = Core.Annotations.Annotation; +const ALLOWED_KEYBOARD_SHORTCUTS = ['+', '-', 'p', 'r', 'Escape'] as const; +const dataElements = { + ADD_REDACTION: 'add-redaction', + ADD_DICTIONARY: 'add-dictionary', + ADD_RECTANGLE: 'add-rectangle', + ADD_FALSE_POSITIVE: 'add-false-positive', + SHAPE_TOOL_GROUP_BUTTON: 'shapeToolGroupButton', + RECTANGLE_TOOL_DIVIDER: 'rectangleToolDivider', + ANNOTATION_POPUP: 'annotationPopup', + COMPARE_BUTTON: 'compareButton', + CLOSE_COMPARE_BUTTON: 'closeCompareButton', + COMPARE_TOOL_DIVIDER: 'compareToolDivider', +} as const; + @Component({ selector: 'redaction-pdf-viewer', templateUrl: './pdf-viewer.component.html', @@ -55,10 +73,11 @@ export class PdfViewerComponent implements OnInit, OnChanges { @ViewChild('viewer', { static: true }) viewer: ElementRef; @ViewChild('compareFileInput', { static: true }) compareFileInput: ElementRef; instance: WebViewerInstance; + documentViewer: Core.DocumentViewer; + annotationManager: Core.AnnotationManager; utils: PdfViewerUtils; private _selectedText = ''; private _firstPageChange = true; - private readonly _allowedKeyboardShortcuts = ['+', '-', 'p', 'r', 'Escape']; constructor( @Inject(BASE_HREF) private readonly _baseHref: string, @@ -86,96 +105,90 @@ export class PdfViewerComponent implements OnInit, OnChanges { } async ngOnInit() { - this._documentLoaded = this._documentLoaded.bind(this); + this._setReadyAndInitialState = this._setReadyAndInitialState.bind(this); await this._loadViewer(); } ngOnChanges(changes: SimpleChanges): void { - if (this.instance) { - if (changes.fileData) { - this._loadDocument(); - } - if (changes.canPerformActions) { - this._handleCustomActions(); - } - if (changes.multiSelectActive) { - this.utils.multiSelectActive = this.multiSelectActive; - } + if (!this.instance) { + return; } - } - setInitialViewerState() { - // viewer init - this.instance.UI.setFitMode('FitPage'); + if (changes.fileData) { + this._loadDocument(); + } - const instanceDisplayMode = this.instance.Core.documentViewer.getDisplayModeManager().getDisplayMode(); - instanceDisplayMode.mode = this.viewMode === 'STANDARD' ? 'Single' : 'Facing'; - this.instance.Core.documentViewer.getDisplayModeManager().setDisplayMode(instanceDisplayMode); + if (changes.canPerformActions) { + this._handleCustomActions(); + } + + if (changes.multiSelectActive) { + this.utils.multiSelectActive = this.multiSelectActive; + } } uploadFile(files: any) { const fileToCompare = files[0]; this.compareFileInput.nativeElement.value = null; - const fileReader = new FileReader(); - - if (fileToCompare) { - fileReader.onload = async () => { - const pdfData = fileReader.result; - const pdfNet = this.instance.Core.PDFNet; - - await pdfNet.initialize(environment.licenseKey ? atob(environment.licenseKey) : null); - - const mergedDocument = await pdfNet.PDFDoc.create(); - const compareDocument = await pdfNet.PDFDoc.createFromBuffer(pdfData); - const currentDocument = await pdfNet.PDFDoc.createFromBuffer(await this.fileData.arrayBuffer()); - - const currentDocumentPageCount = await currentDocument.getPageCount(); - const compareDocumentPageCount = await compareDocument.getPageCount(); - - const loadCompareDocument = async () => { - this._loadingService.start(); - this.utils.ready = false; - await loadCompareDocumentWrapper( - currentDocumentPageCount, - compareDocumentPageCount, - currentDocument, - compareDocument, - mergedDocument, - this.instance, - this.file, - () => { - this.viewMode = 'COMPARE'; - }, - () => { - this.utils.navigateToPage(1); - }, - this.instance.Core.PDFNet, - ); - this._loadingService.stop(); - }; - - 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(); - } - }; + 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 currentDocument = await pdfNet.PDFDoc.createFromBuffer(await this.fileData.arrayBuffer()); + + const loadCompareDocument = async () => { + this._loadingService.start(); + this.utils.ready = false; + const mergedDocument = await pdfNet.PDFDoc.create(); + await loadCompareDocumentWrapper( + currentDocument, + compareDocument, + mergedDocument, + this.instance, + this.file, + () => { + this.viewMode = 'COMPARE'; + }, + () => { + this.utils.navigateToPage(1); + }, + this.instance.Core.PDFNet, + ); + 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); } @@ -187,11 +200,18 @@ export class PdfViewerComponent implements OnInit, OnChanges { this.instance.UI.loadDocument(currentDocument, { filename: this.file ? this.file.filename : 'document.pdf', }); - this.instance.UI.disableElements(['closeCompareButton']); - this.instance.UI.enableElements(['compareButton']); + this.instance.UI.disableElements([dataElements.CLOSE_COMPARE_BUTTON]); + this.instance.UI.enableElements([dataElements.COMPARE_BUTTON]); this.utils.navigateToPage(1); } + private _setInitialDisplayMode() { + this.instance.UI.setFitMode('FitPage'); + const instanceDisplayMode = this.documentViewer.getDisplayModeManager().getDisplayMode(); + instanceDisplayMode.mode = this.viewMode === 'STANDARD' ? 'Single' : 'Facing'; + this.documentViewer.getDisplayModeManager().setDisplayMode(instanceDisplayMode); + } + private _convertPath(path: string): string { return this._baseHref + path; } @@ -208,6 +228,8 @@ export class PdfViewerComponent implements OnInit, OnChanges { this.viewer.nativeElement, ); + this.documentViewer = this.instance.Core.documentViewer; + this.annotationManager = this.instance.Core.annotationManager; this.utils = new PdfViewerUtils(this.instance, this.viewMode, this.multiSelectActive); this._setSelectionMode(); @@ -215,8 +237,8 @@ export class PdfViewerComponent implements OnInit, OnChanges { this.utils.disableHotkeys(); this._configureTextPopup(); - this.instance.Core.annotationManager.on('annotationSelected', (annotations, action) => { - this.annotationSelected.emit(this.instance.Core.annotationManager.getSelectedAnnotations().map(ann => ann.Id)); + this.annotationManager.on('annotationSelected', (annotations, action) => { + this.annotationSelected.emit(this.annotationManager.getSelectedAnnotations().map(ann => ann.Id)); if (action === 'deselected') { this._toggleRectangleAnnotationAction(true); } else { @@ -225,16 +247,16 @@ export class PdfViewerComponent implements OnInit, OnChanges { } }); - this.instance.Core.annotationManager.on('annotationChanged', annotations => { + this.annotationManager.on('annotationChanged', annotations => { // 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.instance.Core.annotationManager.selectAnnotations(annotations); + this.annotationManager.selectAnnotations(annotations); } }); - this.instance.Core.documentViewer.on('pageNumberUpdated', pageNumber => { + this.documentViewer.on('pageNumberUpdated', pageNumber => { if (this.shouldDeselectAnnotationsOnPageChange) { this.utils.deselectAllAnnotations(); } @@ -250,9 +272,9 @@ export class PdfViewerComponent implements OnInit, OnChanges { this._handleCustomActions(); }); - this.instance.Core.documentViewer.on('documentLoaded', this._documentLoaded); + this.documentViewer.on('documentLoaded', this._setReadyAndInitialState); - this.instance.Core.documentViewer.on('keyUp', $event => { + this.documentViewer.on('keyUp', $event => { // arrows and full-screen if ($event.target?.tagName?.toLowerCase() !== 'input') { if ($event.key.startsWith('Arrow') || $event.key === 'f') { @@ -264,18 +286,20 @@ export class PdfViewerComponent implements OnInit, OnChanges { } } - if (this._allowedKeyboardShortcuts.indexOf($event.key) < 0) { + if (ALLOWED_KEYBOARD_SHORTCUTS.indexOf($event.key) < 0) { $event.preventDefault(); $event.stopPropagation(); } }); - this.instance.Core.documentViewer.on('textSelected', (quads, selectedText) => { + this.documentViewer.on('textSelected', (quads, selectedText) => { this._selectedText = selectedText; - if (selectedText.length > 2 && this.canPerformActions) { - this.instance.UI.enableElements(['add-dictionary', 'add-false-positive']); + const textActions = [dataElements.ADD_DICTIONARY, dataElements.ADD_FALSE_POSITIVE]; + + if (selectedText.length > 2 && this.canPerformActions && !this.utils.isCurrentPageExcluded) { + this.instance.UI.enableElements(textActions); } else { - this.instance.UI.disableElements(['add-dictionary', 'add-false-positive']); + this.instance.UI.disableElements(textActions); } }); @@ -286,7 +310,7 @@ export class PdfViewerComponent implements OnInit, OnChanges { inputElement.value = ''; }, 0); if (!event.detail.isVisible) { - this.instance.Core.documentViewer.clearSearchResults(); + this.documentViewer.clearSearchResults(); } } }); @@ -295,15 +319,15 @@ export class PdfViewerComponent implements OnInit, OnChanges { } private _setSelectionMode(): void { - const textTool = ( this.instance.Core.Tools.TextTool) as TextTool; + const textTool = this.instance.Core.Tools.TextTool as unknown as TextTool; textTool.SELECTION_MODE = this._configService.values.SELECTION_MODE; } - private _toggleRectangleAnnotationAction(readonly: boolean) { + private _toggleRectangleAnnotationAction(readonly = false) { if (!readonly) { - this.instance.UI.enableElements(['add-rectangle']); + this.instance.UI.enableElements([dataElements.ADD_RECTANGLE]); } else { - this.instance.UI.disableElements(['add-rectangle']); + this.instance.UI.disableElements([dataElements.ADD_RECTANGLE]); } } @@ -331,50 +355,59 @@ export class PdfViewerComponent implements OnInit, OnChanges { 'annotationGroupButton', ]); - this.instance.UI.setHeaderItems(header => { - const originalHeaderItems = header.getItems(); - originalHeaderItems.splice(8, 0, { + const headerItems = [ + { type: 'divider', - dataElement: 'rectangleToolDivider', - }); - originalHeaderItems.splice(9, 0, { + dataElement: dataElements.RECTANGLE_TOOL_DIVIDER, + }, + { type: 'toolGroupButton', toolGroup: 'rectangleTools', - dataElement: 'shapeToolGroupButton', + dataElement: dataElements.SHAPE_TOOL_GROUP_BUTTON, img: this._convertPath('/assets/icons/general/rectangle.svg'), title: 'annotation.rectangle', - }); + }, + ]; + + this.instance.UI.setHeaderItems(header => { + const originalHeaderItems = header.getItems(); + originalHeaderItems.splice(8, 0, ...headerItems); + if (this._userPreferenceService.areDevFeaturesEnabled) { - originalHeaderItems.splice(11, 0, { - type: 'actionButton', - element: 'compare', - dataElement: 'compareButton', - img: this._convertPath('/assets/icons/general/pdftron-action-compare.svg'), - title: 'Compare', - onClick: () => { - this.compareFileInput.nativeElement.click(); + const devHeaderItems = [ + { + type: 'actionButton', + element: 'compare', + dataElement: dataElements.COMPARE_BUTTON, + img: this._convertPath('/assets/icons/general/pdftron-action-compare.svg'), + title: 'Compare', + onClick: () => { + this.compareFileInput.nativeElement.click(); + }, }, - }); - originalHeaderItems.splice(11, 0, { - type: 'actionButton', - element: 'closeCompare', - dataElement: 'closeCompareButton', - img: this._convertPath('/assets/icons/general/pdftron-action-close-compare.svg'), - title: 'Leave Compare Mode', - onClick: () => { - this.closeCompareMode(); + { + type: 'actionButton', + element: 'closeCompare', + dataElement: dataElements.CLOSE_COMPARE_BUTTON, + img: this._convertPath('/assets/icons/general/pdftron-action-close-compare.svg'), + title: 'Leave Compare Mode', + onClick: async () => { + await this.closeCompareMode(); + }, }, - }); - originalHeaderItems.splice(13, 0, { - type: 'divider', - dataElement: 'compareToolDivider', - }); + { + type: 'divider', + dataElement: dataElements.COMPARE_TOOL_DIVIDER, + }, + ]; + + originalHeaderItems.splice(11, 0, ...devHeaderItems); } }); - this.instance.UI.disableElements(['closeCompareButton']); + this.instance.UI.disableElements([dataElements.CLOSE_COMPARE_BUTTON]); - this.instance.Core.documentViewer.getTool('AnnotationCreateRectangle').setStyles(() => ({ + this.documentViewer.getTool('AnnotationCreateRectangle').setStyles(() => ({ StrokeThickness: 2, StrokeColor: this._annotationDrawService.getColor(this.instance, 'manual'), FillColor: this._annotationDrawService.getColor(this.instance, 'manual'), @@ -410,11 +443,11 @@ export class PdfViewerComponent implements OnInit, OnChanges { onClick: () => { this._ngZone.run(() => { if (allAreVisible) { - this.instance.Core.annotationManager.hideAnnotations(viewerAnnotations); + this.annotationManager.hideAnnotations(viewerAnnotations); } else { - this.instance.Core.annotationManager.showAnnotations(viewerAnnotations); + this.annotationManager.showAnnotations(viewerAnnotations); } - this.instance.Core.annotationManager.deselectAllAnnotations(); + this.annotationManager.deselectAllAnnotations(); this._annotationActionsService.updateHiddenAnnotation(this.annotations, viewerAnnotations, allAreVisible); }); }, @@ -428,38 +461,43 @@ export class PdfViewerComponent implements OnInit, OnChanges { } private _configureRectangleAnnotationPopup() { - this.instance.UI.annotationPopup.add({ - type: 'actionButton', - dataElement: 'add-rectangle', - img: this._convertPath('/assets/icons/general/pdftron-action-add-redaction.svg'), - title: this._translateService.instant(this._manualAnnotationService.getTitle('REDACTION')), - onClick: () => { - const selectedAnnotations = this.instance.Core.annotationManager.getSelectedAnnotations(); - const activeAnnotation = selectedAnnotations[0]; - const activePage = selectedAnnotations[0].getPageNumber(); - const quad = this._annotationDrawService.annotationToQuads(activeAnnotation, this.instance); - const quadsObject = {}; - quadsObject[activePage] = [quad]; - const mre = this._getManualRedactionEntry(quadsObject, 'Rectangle'); - // cleanup selection and button state - this.utils.deselectAllAnnotations(); - this.instance.UI.disableElements(['shapeToolGroupButton', 'rectangleToolDivider']); - this.instance.UI.enableElements(['shapeToolGroupButton', 'rectangleToolDivider']); - // dispatch event - this.manualAnnotationRequested.emit( - new ManualRedactionEntryWrapper([quad], mre, 'REDACTION', 'RECTANGLE', activeAnnotation.Id), - ); + this.instance.UI.annotationPopup.add([ + { + type: 'actionButton', + dataElement: dataElements.ADD_RECTANGLE, + img: this._convertPath('/assets/icons/general/pdftron-action-add-redaction.svg'), + title: this._translateService.instant(this._manualAnnotationService.getTitle('REDACTION')), + onClick: () => this._addRectangleManualRedaction(), }, - }); + ]); + } + + private _addRectangleManualRedaction() { + const activeAnnotation = this.annotationManager.getSelectedAnnotations()[0]; + const activePage = activeAnnotation.getPageNumber(); + const quads = [this._annotationDrawService.annotationToQuads(activeAnnotation, this.instance)]; + const manualRedaction = this._getManualRedaction({ [activePage]: quads }, 'Rectangle'); + this._cleanUpSelectionAndButtonState(); + + this.manualAnnotationRequested.emit( + new ManualRedactionEntryWrapper(quads, manualRedaction, 'REDACTION', 'RECTANGLE', activeAnnotation.Id), + ); + } + + private _cleanUpSelectionAndButtonState() { + const rectangleElements = [dataElements.SHAPE_TOOL_GROUP_BUTTON, dataElements.RECTANGLE_TOOL_DIVIDER]; + this.utils.deselectAllAnnotations(); + this.instance.UI.disableElements(rectangleElements); + this.instance.UI.enableElements(rectangleElements); } private _configureTextPopup() { - this.instance.UI.textPopup.add({ + 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.instance.Core.documentViewer.getSelectedText(); + const text = this.documentViewer.getSelectedText(); const searchOptions = { caseSensitive: true, // match case wholeWord: true, // match whole words only @@ -469,117 +507,121 @@ export class PdfViewerComponent implements OnInit, OnChanges { ambientString: true, // return ambient string as part of the result }; this.instance.UI.openElements(['searchPanel']); - setTimeout(() => { - this.instance.UI.searchTextFull(text, searchOptions); - }, 250); + setTimeout(() => this.instance.UI.searchTextFull(text, searchOptions), 250); }, - }); + }; + + this.instance.UI.textPopup.add([searchButton]); // Adding directly to the false-positive dict is only available in dev-mode if (this._userPreferenceService.areDevFeaturesEnabled) { - this.instance.UI.textPopup.add({ - type: 'actionButton', - dataElement: 'add-false-positive', - img: this._convertPath('/assets/icons/general/pdftron-action-false-positive.svg'), - title: this._translateService.instant(this._manualAnnotationService.getTitle('FALSE_POSITIVE')), - onClick: () => { - const selectedQuads = this.instance.Core.documentViewer.getSelectedTextQuads(); - const text = this.instance.Core.documentViewer.getSelectedText(); - const mre = this._getManualRedactionEntry(selectedQuads, text, true); - this.manualAnnotationRequested.emit( - new ManualRedactionEntryWrapper(this.instance.Core.documentViewer.getSelectedTextQuads(), mre, 'FALSE_POSITIVE'), - ); + this.instance.UI.textPopup.add([ + { + type: 'actionButton', + dataElement: 'add-false-positive', + img: this._convertPath('/assets/icons/general/pdftron-action-false-positive.svg'), + title: this._translateService.instant(this._manualAnnotationService.getTitle(ManualRedactionEntryTypes.FALSE_POSITIVE)), + onClick: () => this._addManualRedactionOfType(ManualRedactionEntryTypes.FALSE_POSITIVE), }, - }); + ]); } - this.instance.UI.textPopup.add({ - type: 'actionButton', - dataElement: 'add-dictionary', - img: this._convertPath('/assets/icons/general/pdftron-action-add-dict.svg'), - title: this._translateService.instant(this._manualAnnotationService.getTitle('DICTIONARY')), - onClick: () => { - const selectedQuads = this.instance.Core.documentViewer.getSelectedTextQuads(); - const text = this.instance.Core.documentViewer.getSelectedText(); - const mre = this._getManualRedactionEntry(selectedQuads, text, true); - this.manualAnnotationRequested.emit( - new ManualRedactionEntryWrapper(this.instance.Core.documentViewer.getSelectedTextQuads(), mre, 'DICTIONARY'), - ); + this.instance.UI.textPopup.add([ + { + type: 'actionButton', + dataElement: dataElements.ADD_REDACTION, + img: this._convertPath('/assets/icons/general/pdftron-action-add-redaction.svg'), + title: this._translateService.instant(this._manualAnnotationService.getTitle(ManualRedactionEntryTypes.REDACTION)), + onClick: () => this._addManualRedactionOfType(ManualRedactionEntryTypes.REDACTION), }, - }); + { + type: 'actionButton', + dataElement: dataElements.ADD_DICTIONARY, + img: this._convertPath('/assets/icons/general/pdftron-action-add-dict.svg'), + title: this._translateService.instant(this._manualAnnotationService.getTitle(ManualRedactionEntryTypes.DICTIONARY)), + onClick: () => this._addManualRedactionOfType(ManualRedactionEntryTypes.DICTIONARY), + }, + ]); - this.instance.UI.textPopup.add({ - type: 'actionButton', - dataElement: 'add-redaction', - img: this._convertPath('/assets/icons/general/pdftron-action-add-redaction.svg'), - title: this._translateService.instant(this._manualAnnotationService.getTitle('REDACTION')), - onClick: () => { - const selectedQuads = this.instance.Core.documentViewer.getSelectedTextQuads(); - const text = this.instance.Core.documentViewer.getSelectedText(); - const mre = this._getManualRedactionEntry(selectedQuads, text, true); - this.manualAnnotationRequested.emit( - new ManualRedactionEntryWrapper(this.instance.Core.documentViewer.getSelectedTextQuads(), mre, 'REDACTION'), - ); - }, - }); this._handleCustomActions(); } + private _addManualRedactionOfType(type: ManualRedactionEntryType) { + const selectedQuads = this.documentViewer.getSelectedTextQuads(); + const text = this.documentViewer.getSelectedText(); + const manualRedaction = this._getManualRedaction(selectedQuads, text, true); + this.manualAnnotationRequested.emit(new ManualRedactionEntryWrapper(selectedQuads, manualRedaction, type)); + } + private _handleCustomActions() { this.instance.UI.setToolMode('AnnotationEdit'); + const { ANNOTATION_POPUP, ADD_RECTANGLE, ADD_REDACTION, SHAPE_TOOL_GROUP_BUTTON } = dataElements; + const elements = [ + ADD_REDACTION, + ADD_RECTANGLE, + 'add-false-positive', + SHAPE_TOOL_GROUP_BUTTON, + 'rectangleToolDivider', + ANNOTATION_POPUP, + ]; + if (this.canPerformActions && !this.utils.isCurrentPageExcluded) { this.instance.UI.enableTools(['AnnotationCreateRectangle']); - this.instance.UI.enableElements([ - 'add-redaction', - 'add-rectangle', - 'add-false-positive', - 'shapeToolGroupButton', - 'rectangleToolDivider', - 'annotationPopup', - ]); + this.instance.UI.enableElements(elements); + if (this._selectedText.length > 2) { - this.instance.UI.enableElements(['add-dictionary', 'add-false-positive']); + this.instance.UI.enableElements([dataElements.ADD_DICTIONARY, dataElements.ADD_FALSE_POSITIVE]); } + + return; + } + + let elementsToDisable = [...elements, ADD_RECTANGLE]; + + if (this.utils.isCurrentPageExcluded) { + const allowedActionsWhenPageExcluded: string[] = [ANNOTATION_POPUP, ADD_RECTANGLE, ADD_REDACTION, SHAPE_TOOL_GROUP_BUTTON]; + elementsToDisable = elementsToDisable.filter(element => !allowedActionsWhenPageExcluded.includes(element)); } else { this.instance.UI.disableTools(['AnnotationCreateRectangle']); - this.instance.UI.disableElements([ - 'add-redaction', - 'add-dictionary', - 'add-false-positive', - 'add-rectangle', - 'shapeToolGroupButton', - 'rectangleToolDivider', - 'annotationPopup', - ]); } + + this.instance.UI.disableElements(elementsToDisable); } - private _getManualRedactionEntry(quads: any, text: string, convertQuads: boolean = false): IManualRedactionEntry { + private _getManualRedaction( + quads: Readonly>, + 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); entry.positions.push(this.utils.toPosition(page, convertQuads ? this.utils.translateQuads(page, quad) : quad)); } } + entry.value = text; return entry; } private _loadDocument() { - if (this.fileData) { - this.instance.UI.loadDocument(this.fileData, { - filename: this.file ? this.file.filename : 'document.pdf', - }); + if (!this.fileData) { + return; } + + this.instance.UI.loadDocument(this.fileData, { + filename: this.file ? this.file.filename : 'document.pdf', + }); } - private _documentLoaded(): void { + private _setReadyAndInitialState(): void { this._ngZone.run(() => { this.utils.ready = true; this._firstPageChange = true; this.viewerReady.emit(this.instance); - this.setInitialViewerState(); + this._setInitialDisplayMode(); }); } } diff --git a/apps/red-ui/src/app/modules/dossier/utils/compare-mode.utils.ts b/apps/red-ui/src/app/modules/dossier/utils/compare-mode.utils.ts index 3074b808b..06876a51b 100644 --- a/apps/red-ui/src/app/modules/dossier/utils/compare-mode.utils.ts +++ b/apps/red-ui/src/app/modules/dossier/utils/compare-mode.utils.ts @@ -1,34 +1,42 @@ import { stampPDFPage } from '@utils/page-stamper'; +import { Core, WebViewerInstance } from '@pdftron/webviewer'; +import { File } from '@red/domain'; -const processPage = async (pageNumber, document1, document2, mergedDocument, pdfNet) => { +const processPage = async ( + pageNumber: number, + document1: Core.PDFNet.PDFDoc, + document2: Core.PDFNet.PDFDoc, + mergedDocument: Core.PDFNet.PDFDoc, + pdfNet: typeof Core.PDFNet, +) => { const document1PageCount = await document1.getPageCount(); + if (document1PageCount >= pageNumber) { await mergedDocument.insertPages(pageNumber * 2, document1, pageNumber, pageNumber, pdfNet.PDFDoc.InsertFlag.e_none); - } else { - const pageToCopy = await document2.getPage(pageNumber); - const blankPage = await mergedDocument.pageCreate(await pageToCopy.getCropBox()); - await blankPage.setRotation(await pageToCopy.getRotation()); - await mergedDocument.pagePushBack(blankPage); - await stampPDFPage(mergedDocument, pdfNet, '<< Compare Placeholder Page >>', 20, 'courier', 'DIAGONAL', 33, '#ffb83b', [ - await mergedDocument.getPageCount(), - ]); + return; } + + const pageToCopy = await document2.getPage(pageNumber); + const blankPage = await mergedDocument.pageCreate(await pageToCopy.getCropBox()); + await blankPage.setRotation(await pageToCopy.getRotation()); + await mergedDocument.pagePushBack(blankPage); + await stampPDFPage(mergedDocument, pdfNet, '<< Compare Placeholder Page >>', 20, 'courier', 'DIAGONAL', 33, '#ffb83b', [ + await mergedDocument.getPageCount(), + ]); }; export const loadCompareDocumentWrapper = async ( - currentDocumentPageCount, - compareDocumentPageCount, - currentDocument, - compareDocument, - mergedDocument, - instance, - file, + currentDocument: Core.PDFNet.PDFDoc, + compareDocument: Core.PDFNet.PDFDoc, + mergedDocument: Core.PDFNet.PDFDoc, + instance: WebViewerInstance, + file: File, setCompareViewMode: () => void, navigateToPage: () => void, - pdfNet: any, + pdfNet: typeof Core.PDFNet, ) => { try { - const maxPageCount = Math.max(currentDocumentPageCount, compareDocumentPageCount); + const maxPageCount = Math.max(await currentDocument.getPageCount(), await compareDocument.getPageCount()); for (let idx = 1; idx <= maxPageCount; idx++) { await processPage(idx, currentDocument, compareDocument, mergedDocument, pdfNet); @@ -43,11 +51,11 @@ export const loadCompareDocumentWrapper = async ( setCompareViewMode(); - instance.loadDocument(mergedDocumentBuffer, { + instance.UI.loadDocument(mergedDocumentBuffer, { filename: file?.filename ?? 'document.pdf', }); - instance.disableElements(['compareButton']); - instance.enableElements(['closeCompareButton']); + instance.UI.disableElements(['compareButton']); + instance.UI.enableElements(['closeCompareButton']); navigateToPage(); } catch (e) { diff --git a/apps/red-ui/src/app/modules/icons/icons.module.ts b/apps/red-ui/src/app/modules/icons/icons.module.ts index 73a9cfcff..4b98ecc4e 100644 --- a/apps/red-ui/src/app/modules/icons/icons.module.ts +++ b/apps/red-ui/src/app/modules/icons/icons.module.ts @@ -29,6 +29,7 @@ export class IconsModule { 'enter', 'entries', 'exclude-pages', + 'excluded-page', 'exit-fullscreen', 'folder', 'fullscreen', diff --git a/apps/red-ui/src/assets/i18n/en.json b/apps/red-ui/src/assets/i18n/en.json index 7144a3652..685d34dfb 100644 --- a/apps/red-ui/src/assets/i18n/en.json +++ b/apps/red-ui/src/assets/i18n/en.json @@ -1017,7 +1017,7 @@ "document-info": "Your Document Info lives here. This includes metadata required on each document.", "download-original-file": "Download Original File", "exclude-pages": "Exclude pages from redaction", - "excluded-from-redaction": "excluded from redaction", + "excluded-from-redaction": "excluded from automatic redaction", "fullscreen": "Full Screen (F)", "last-reviewer": "Last Reviewed by:", "no-data": { diff --git a/apps/red-ui/src/assets/icons/general/excluded-page.svg b/apps/red-ui/src/assets/icons/general/excluded-page.svg new file mode 100644 index 000000000..5857d07cf --- /dev/null +++ b/apps/red-ui/src/assets/icons/general/excluded-page.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + diff --git a/libs/common-ui b/libs/common-ui index 1df1b1ab8..e1ce89e38 160000 --- a/libs/common-ui +++ b/libs/common-ui @@ -1 +1 @@ -Subproject commit 1df1b1ab899e21093eb07c444acf90def933cb02 +Subproject commit e1ce89e38d3520ad11960074f74c381429c0251a