This commit is contained in:
Dan Percic 2022-03-15 16:38:45 +02:00
parent 8e97ed44ed
commit 9ae510bf5a
8 changed files with 190 additions and 828 deletions

View File

@ -1,704 +0,0 @@
import {
Component,
ElementRef,
EventEmitter,
Inject,
Input,
NgZone,
OnChanges,
OnInit,
Output,
SimpleChanges,
ViewChild,
} from '@angular/core';
import { Dossier, File, IHeaderElement, IManualRedactionEntry, RotationTypes } 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 { ManualRedactionService } from '../../services/manual-redaction.service';
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, LoadingService } from '@iqser/common-ui';
import { DossiersDialogService } from '../../../../services/dossiers-dialog.service';
import { loadCompareDocumentWrapper } from '../../../../utils/compare-mode.utils';
import { PdfViewer } from '../../services/pdf-viewer.service';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { ActivatedRoute } from '@angular/router';
import { toPosition } from '../../../../utils/pdf-calculation.utils';
import { ViewModeService } from '../../services/view-mode.service';
import { MultiSelectService } from '../../services/multi-select.service';
import { FilePreviewStateService } from '../../services/file-preview-state.service';
import { tap, withLatestFrom } from 'rxjs/operators';
import { PageRotationService } from '../../services/page-rotation.service';
import { ALLOWED_KEYBOARD_SHORTCUTS, HeaderElements, TextPopups } from '../../shared/constants';
import Tools = Core.Tools;
import TextTool = Tools.TextTool;
import Annotation = Core.Annotations.Annotation;
function getDivider(hiddenOn?: readonly ('desktop' | 'mobile' | 'tablet')[]) {
return {
type: 'divider',
hidden: hiddenOn,
};
}
@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;
@Input() annotations: AnnotationWrapper[];
@Output() readonly fileReady = new EventEmitter();
@Output() readonly annotationSelected = new EventEmitter<string[]>();
@Output() readonly manualAnnotationRequested = new EventEmitter<ManualRedactionEntryWrapper>();
@Output() readonly pageChanged = new EventEmitter<number>();
@Output() readonly keyUp = new EventEmitter<KeyboardEvent>();
@Output() readonly viewerReady = new EventEmitter<WebViewerInstance>();
@Output() readonly annotationsChanged = new EventEmitter<AnnotationWrapper>();
@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: DossiersDialogService,
private readonly _ngZone: NgZone,
private readonly _activatedRoute: ActivatedRoute,
private readonly _userPreferenceService: UserPreferenceService,
private readonly _annotationDrawService: AnnotationDrawService,
private readonly _annotationActionsService: AnnotationActionsService,
private readonly _configService: ConfigService,
private readonly _loadingService: LoadingService,
private readonly _pageRotationService: PageRotationService,
readonly stateService: FilePreviewStateService,
readonly viewModeService: ViewModeService,
readonly multiSelectService: MultiSelectService,
readonly pdf: PdfViewer,
) {
super();
}
private get _toggleTooltipsBtnTitle(): string {
return this._translateService.instant(_('pdf-viewer.toggle-tooltips'), {
active: this._userPreferenceService.getFilePreviewTooltipsPreference(),
});
}
private get _toggleTooltipsIcon(): string {
return this._convertPath(
this._userPreferenceService.getFilePreviewTooltipsPreference()
? '/assets/icons/general/pdftron-action-enable-tooltips.svg'
: '/assets/icons/general/pdftron-action-disable-tooltips.svg',
);
}
async ngOnInit() {
this._setReadyAndInitialState = this._setReadyAndInitialState.bind(this);
await this._loadViewer();
this.addActiveScreenSubscription = this.stateService.blob$
.pipe(
withLatestFrom(this.stateService.file$),
tap(() => (this.pdf.ready = false)),
tap(([blob, file]) => this._loadDocument(blob, file)),
)
.subscribe();
}
async ngOnChanges(changes: SimpleChanges): Promise<void> {
if (!this.instance) {
return;
}
if (changes.canPerformActions) {
await 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();
this.pdf.ready = false;
const mergedDocument = await pdfNet.PDFDoc.create();
const file = await this.stateService.file;
await loadCompareDocumentWrapper(
currentDocument,
compareDocument,
mergedDocument,
this.instance,
file,
() => {
this.viewModeService.compareMode = true;
},
() => {
this.pdf.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);
}
async closeCompareMode() {
this.viewModeService.compareMode = false;
const pdfNet = this.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 = (await this.stateService.file).filename ?? 'document.pdf';
this.instance.UI.loadDocument(currentDocument, { filename });
this.instance.UI.disableElements([HeaderElements.CLOSE_COMPARE_BUTTON]);
this.instance.UI.enableElements([HeaderElements.COMPARE_BUTTON]);
this.pdf.navigateToPage(1);
}
private async _loadViewer() {
this.instance = await this.pdf.loadViewer(this.viewer.nativeElement as HTMLElement);
this.documentViewer = this.pdf.documentViewer;
this.annotationManager = this.pdf.annotationManager;
this._setSelectionMode();
this._configureElements();
this.pdf.disableHotkeys();
await this._configureTextPopup();
this.annotationManager.addEventListener('annotationSelected', (annotations: Annotation[], action) => {
const nextAnnotations = this.multiSelectService.isEnabled ? this.annotationManager.getSelectedAnnotations() : annotations;
this.annotationSelected.emit(nextAnnotations.map(ann => ann.Id));
if (action === 'deselected') {
this._toggleRectangleAnnotationAction(true);
} else {
if (!this.multiSelectService.isEnabled) {
this.pdf.deselectAnnotations(this.annotations.filter(wrapper => !nextAnnotations.find(ann => ann.Id === wrapper.id)));
}
this._configureAnnotationSpecificActions(annotations);
this._toggleRectangleAnnotationAction(annotations.length === 1 && annotations[0].ReadOnly);
}
});
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.pdf.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', async (quads, selectedText, pageNumber: number) => {
this._selectedText = selectedText;
const textActions = [TextPopups.ADD_DICTIONARY, TextPopups.ADD_FALSE_POSITIVE];
const file = await 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.pdf.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',
]);
const applyRotation: IHeaderElement = {
type: 'customElement',
dataElement: HeaderElements.APPLY_ROTATION,
render: () => {
const paragraph = document.createElement('p');
paragraph.innerText = this._translateService.instant('page-rotation.apply');
paragraph.style.cssText = `
font-size: 11px;
font-weight: 600;
color: #DD4D50;
cursor: pointer;
margin: 0 12px;
`;
paragraph.addEventListener('click', async () => {
await this._pageRotationService.applyRotation();
});
return paragraph;
},
};
const discardRotation: IHeaderElement = {
type: 'customElement',
dataElement: HeaderElements.DISCARD_ROTATION,
render: () => {
const paragraph = document.createElement('p');
paragraph.innerText = this._translateService.instant('page-rotation.discard');
paragraph.style.cssText = `
font-size: 11px;
font-weight: 600;
color: #283241;
cursor: pointer;
opacity: 0.7;
`;
paragraph.addEventListener('click', () => {
this._pageRotationService.discardRotation();
});
return paragraph;
},
};
const divider = getDivider();
const headerItems: IHeaderElement[] = [
divider,
{
type: 'actionButton',
element: 'compare',
dataElement: HeaderElements.COMPARE_BUTTON,
img: this._convertPath('/assets/icons/general/pdftron-action-compare.svg'),
title: 'Compare',
onClick: () => {
this.compareFileInput.nativeElement.click();
},
},
{
type: 'actionButton',
element: 'closeCompare',
dataElement: HeaderElements.CLOSE_COMPARE_BUTTON,
img: this._convertPath('/assets/icons/general/pdftron-action-close-compare.svg'),
title: 'Leave Compare Mode',
onClick: async () => {
await this.closeCompareMode();
},
},
divider,
{
type: 'actionButton',
element: 'tooltips',
dataElement: HeaderElements.TOGGLE_TOOLTIPS,
img: this._toggleTooltipsIcon,
title: this._toggleTooltipsBtnTitle,
onClick: async () => {
await this._userPreferenceService.toggleFilePreviewTooltipsPreference();
this._updateTooltipsVisibility();
this.instance.UI.updateElement(HeaderElements.TOGGLE_TOOLTIPS, {
title: this._toggleTooltipsBtnTitle,
img: this._toggleTooltipsIcon,
});
},
},
divider,
{
type: 'toolGroupButton',
toolGroup: 'rectangleTools',
dataElement: HeaderElements.SHAPE_TOOL_GROUP_BUTTON,
img: this._convertPath('/assets/icons/general/rectangle.svg'),
title: 'annotation.rectangle',
},
divider,
{
type: 'actionButton',
element: 'tooltips',
dataElement: HeaderElements.ROTATE_LEFT_BUTTON,
img: this._convertPath('/assets/icons/general/rotate-left.svg'),
onClick: () => this._pageRotationService.addRotation(RotationTypes.LEFT),
},
{
type: 'actionButton',
element: 'tooltips',
dataElement: HeaderElements.ROTATE_RIGHT_BUTTON,
img: this._convertPath('/assets/icons/general/rotate-right.svg'),
onClick: () => this._pageRotationService.addRotation(RotationTypes.RIGHT),
},
applyRotation,
discardRotation,
divider,
];
this.instance.UI.setHeaderItems(header => {
header.getItems().splice(8, 0, ...headerItems);
});
this.instance.UI.disableElements([
HeaderElements.CLOSE_COMPARE_BUTTON,
HeaderElements.APPLY_ROTATION,
HeaderElements.DISCARD_ROTATION,
]);
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,
});
}
private _configureAnnotationSpecificActions(viewerAnnotations: Annotation[]) {
if (!this.canPerformActions) {
return;
}
const annotationWrappers = viewerAnnotations.map(va => this.annotations.find(a => a.id === 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._annotationActionsService.updateHiddenAnnotation(this.annotations, viewerAnnotations, allAreVisible);
});
},
},
]);
}
const actions = this._annotationActionsService.getViewerAvailableActions(this.dossier, annotationWrappers, this.annotationsChanged);
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 manualRedaction = this._getManualRedaction({ [activePage]: quads });
this._cleanUpSelectionAndButtonState();
this.manualAnnotationRequested.emit(new ManualRedactionEntryWrapper(quads, manualRedaction, 'REDACTION', activeAnnotation.Id));
}
private _cleanUpSelectionAndButtonState() {
const rectangleElements = [HeaderElements.SHAPE_TOOL_GROUP_BUTTON];
this.pdf.deselectAllAnnotations();
this.instance.UI.disableElements(rectangleElements);
this.instance.UI.enableElements(rectangleElements);
}
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 manualRedaction = this._getManualRedaction(selectedQuads, text, true);
this.manualAnnotationRequested.emit(new ManualRedactionEntryWrapper(selectedQuads, manualRedaction, type));
}
private async _handleCustomActions() {
this.instance.UI.setToolMode('AnnotationEdit');
const elementsToToggle = [
TextPopups.ADD_REDACTION,
TextPopups.ADD_RECTANGLE,
TextPopups.ADD_FALSE_POSITIVE,
HeaderElements.SHAPE_TOOL_GROUP_BUTTON,
HeaderElements.ANNOTATION_POPUP,
];
const isCurrentPageExcluded = this.pdf.isCurrentPageExcluded(await this.stateService.file);
if (this.canPerformActions && !isCurrentPageExcluded) {
try {
this.instance.UI.enableTools(['AnnotationCreateRectangle']);
} catch (e) {
// happens
}
this.instance.UI.enableElements(elementsToToggle);
if (this._selectedText.length > 2) {
this.instance.UI.enableElements([TextPopups.ADD_DICTIONARY, TextPopups.ADD_FALSE_POSITIVE]);
}
return;
}
let elementsToDisable = [...elementsToToggle, TextPopups.ADD_RECTANGLE];
if (isCurrentPageExcluded) {
const allowedActionsWhenPageExcluded: string[] = [
HeaderElements.ANNOTATION_POPUP,
TextPopups.ADD_RECTANGLE,
TextPopups.ADD_REDACTION,
HeaderElements.SHAPE_TOOL_GROUP_BUTTON,
];
elementsToDisable = elementsToDisable.filter(element => !allowedActionsWhenPageExcluded.includes(element));
} else {
this.instance.UI.disableTools(['AnnotationCreateRectangle']);
}
this.instance.UI.disableElements(elementsToDisable);
}
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.pdf.translateQuad(page, quad) : quad));
}
}
entry.value = text;
entry.rectangle = !text;
return entry;
}
private async _loadDocument(blob: Blob, file: File) {
const document = await this.instance.Core.documentViewer.getDocument()?.getPDFDoc();
await document?.lock();
this.instance.UI.loadDocument(blob, { filename: file?.filename ?? 'document.pdf' });
this._pageRotationService.clearRotationsHideActions();
}
private _setReadyAndInitialState(): void {
this._ngZone.run(() => {
this.viewerReady.emit(this.instance);
const routePageNumber: number = this._activatedRoute.snapshot.queryParams.page;
this.pageChanged.emit(routePageNumber || 1);
this._setInitialDisplayMode();
this._updateTooltipsVisibility();
});
}
private _updateTooltipsVisibility(): void {
const current = this._userPreferenceService.getFilePreviewTooltipsPreference();
this.instance.UI.setAnnotationContentOverlayHandler(() => (current ? undefined : false));
}
}

View File

@ -1,73 +0,0 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { IqserIconsModule } from '@iqser/common-ui';
import { TranslateModule } from '@ngx-translate/core';
import { RouterModule, Routes } from '@angular/router';
import { SharedModule } from '@shared/shared.module';
import { SharedDossiersModule } from '../../shared/shared-dossiers.module';
import { FilePreviewScreenComponent } from './file-preview-screen.component';
import { FileWorkloadComponent } from './components/file-workload/file-workload.component';
import { AnnotationDetailsComponent } from './components/annotation-details/annotation-details.component';
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';
import { TypeAnnotationIconComponent } from './components/type-annotation-icon/type-annotation-icon.component';
import { OverlayModule } from '@angular/cdk/overlay';
import { ViewSwitchComponent } from './components/view-switch/view-switch.component';
import { UserManagementComponent } from './components/user-management/user-management.component';
import { AnnotationReferencesListComponent } from './components/annotation-references-list/annotation-references-list.component';
import { AcceptRecommendationDialogComponent } from './dialogs/accept-recommendation-dialog/accept-recommendation-dialog.component';
import { AnnotationCardComponent } from './components/annotation-card/annotation-card.component';
import { AnnotationReferencesPageIndicatorComponent } from './components/annotation-references-page-indicator/annotation-references-page-indicator.component';
import { HighlightsSeparatorComponent } from './components/highlights-separator/highlights-separator.component';
import { PendingChangesGuard } from '../../../../guards/can-deactivate.guard';
import { ManualRedactionService } from './services/manual-redaction.service';
const routes: Routes = [
{
path: '',
component: FilePreviewScreenComponent,
pathMatch: 'full',
data: { reuse: true },
canDeactivate: [PendingChangesGuard],
},
];
const components = [
FileWorkloadComponent,
AnnotationDetailsComponent,
AnnotationsListComponent,
PageIndicatorComponent,
PageExclusionComponent,
PdfViewerComponent,
AnnotationActionsComponent,
CommentsComponent,
DocumentInfoComponent,
TypeAnnotationIconComponent,
ViewSwitchComponent,
UserManagementComponent,
AcceptRecommendationDialogComponent,
AnnotationReferencesListComponent,
AnnotationCardComponent,
AnnotationReferencesPageIndicatorComponent,
HighlightsSeparatorComponent,
];
@NgModule({
declarations: [FilePreviewScreenComponent, ...components],
imports: [
RouterModule.forChild(routes),
CommonModule,
SharedModule,
SharedDossiersModule,
IqserIconsModule,
TranslateModule,
OverlayModule,
],
providers: [ManualRedactionService],
})
export class FilePreviewModule {}

View File

@ -20,7 +20,6 @@ import {
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';
import { AnnotationDrawService } from '../../services/annotation-draw.service';
import { AnnotationActionsService } from '../../services/annotation-actions.service';
@ -44,6 +43,7 @@ import { from, fromEvent } 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;
@ -71,7 +71,7 @@ export class PdfViewerComponent extends AutoUnsubscribe implements OnInit, OnCha
constructor(
@Inject(BASE_HREF) private readonly _baseHref: string,
private readonly _translateService: TranslateService,
private readonly _manualAnnotationService: ManualAnnotationService,
private readonly _manualRedactionService: ManualRedactionService,
private readonly _dialogService: FilePreviewDialogService,
private readonly _ngZone: NgZone,
private readonly _activatedRoute: ActivatedRoute,
@ -481,7 +481,7 @@ export class PdfViewerComponent extends AutoUnsubscribe implements OnInit, OnCha
}
#getTitle(type: ManualRedactionEntryType) {
return this._translateService.instant(this._manualAnnotationService.getTitle(type, this.dossier));
return this._translateService.instant(this._manualRedactionService.getTitle(type, this.dossier));
}
private _addManualRedactionOfType(type: ManualRedactionEntryType) {

View File

@ -2,13 +2,13 @@ import { Component, Inject, Injector, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { ManualRedactionEntryWrapper } from '@models/file/manual-redaction-entry.wrapper';
import { ManualRedactionService } from '../../screens/file-preview-screen/services/manual-redaction.service';
import { JustificationsService } from '@services/entity-services/justifications.service';
import { Dictionary, Dossier, IAddRedactionRequest } from '@red/domain';
import { ActiveDossiersService } from '@services/dossiers/active-dossiers.service';
import { DictionaryService } from '@services/entity-services/dictionary.service';
import { BaseDialogComponent, CircleButtonTypes } from '@iqser/common-ui';
import { firstValueFrom } from 'rxjs';
import { ManualRedactionService } from '../../services/manual-redaction.service';
export interface LegalBasisOption {
label?: string;

View File

@ -35,6 +35,7 @@ import { HighlightActionDialogComponent } from './dialogs/highlight-action-dialo
import { FilePreviewDialogService } from './services/file-preview-dialog.service';
import { ColorPickerModule } from 'ngx-color-picker';
import { DocumentInfoDialogComponent } from './dialogs/document-info-dialog/document-info-dialog.component';
import { ManualRedactionService } from './services/manual-redaction.service';
const routes: Routes = [
{
@ -75,10 +76,11 @@ const components = [
AnnotationCardComponent,
AnnotationReferencesPageIndicatorComponent,
HighlightsSeparatorComponent,
FilePreviewScreenComponent,
];
@NgModule({
declarations: [FilePreviewScreenComponent, ...components, ...dialogs],
declarations: [...components, ...dialogs],
imports: [
RouterModule.forChild(routes),
CommonModule,
@ -89,6 +91,6 @@ const components = [
OverlayModule,
ColorPickerModule,
],
providers: [FilePreviewDialogService],
providers: [FilePreviewDialogService, ManualRedactionService],
})
export class FilePreviewModule {}

View File

@ -1,6 +1,7 @@
import { Injectable, Injector } from '@angular/core';
import type {
AnnotationActionMode,
DictionaryActions,
Dossier,
IAddRedactionRequest,
IApproveRequest,
@ -9,18 +10,23 @@ import type {
IManualAddResponse,
IRemoveRedactionRequest,
IResizeRequest,
ManualRedactionActions,
} from '@red/domain';
import { type AnnotationWrapper } from '@models/file/annotation.wrapper';
import { type AnnotationWrapper } from '../../../models/file/annotation.wrapper';
import { GenericService, RequiredParam, Toaster, Validate } from '@iqser/common-ui';
import { map, tap } from 'rxjs/operators';
import { PermissionsService } from '@services/permissions.service';
import { annotationActionsTranslations } from '../../../../../translations/annotation-actions-translations';
import { PermissionsService } from '../../../services/permissions.service';
import {
annotationActionsTranslations,
dictionaryActionsTranslations,
manualRedactionActionsTranslations,
} from '../../../translations/annotation-actions-translations';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { HttpErrorResponse, HttpStatusCode } from '@angular/common/http';
import { ActiveDossiersService } from '@services/dossiers/active-dossiers.service';
import { ActiveDossiersService } from '../../../services/dossiers/active-dossiers.service';
import { type Observable } from 'rxjs';
import { DictionariesMapService } from '@services/entity-services/dictionaries-map.service';
import { type ManualRedactionEntryType } from '@models/file/manual-redaction-entry.wrapper';
import { DictionariesMapService } from '../../../services/entity-services/dictionaries-map.service';
import { type ManualRedactionEntryType } from '../../../models/file/manual-redaction-entry.wrapper';
function getMessage(mode: AnnotationActionMode, modifyDictionary?: boolean, error = false, isConflict = false) {
const type = modifyDictionary ? 'dictionary' : 'manual-redaction';
@ -28,8 +34,22 @@ function getMessage(mode: AnnotationActionMode, modifyDictionary?: boolean, erro
return annotationActionsTranslations[type][mode][resultType];
}
function getResponseType(error: boolean, isConflict: boolean) {
return error ? (isConflict ? 'conflictError' : 'error') : 'success';
}
function getDictionaryMessage(action: DictionaryActions, error = false, isConflict = false) {
return dictionaryActionsTranslations[action][getResponseType(error, isConflict)];
}
function getManualRedactionMessage(action: ManualRedactionActions, error = false, isConflict = false) {
return manualRedactionActionsTranslations[action][getResponseType(error, isConflict)];
}
@Injectable()
export class ManualRedactionService extends GenericService<IManualAddResponse> {
readonly request = `${this._defaultModelPath}/request`;
readonly redaction = `${this._defaultModelPath}/redaction`;
CONFIG: {
[key in AnnotationActionMode]: string;
};
@ -153,6 +173,20 @@ export class ManualRedactionService extends GenericService<IManualAddResponse> {
return this._makeRequest(mode, dossierId, fileId, request);
}
_force(request: ILegalBasisChangeRequest, dossierId: string, fileId: string) {
const mode: AnnotationActionMode = this._permissionsService.isApprover(this.#dossier(dossierId))
? 'force-redaction'
: 'request-force-redaction';
return this._makeRequest(mode, dossierId, fileId, request);
}
_requestForce(request: ILegalBasisChangeRequest, dossierId: string, fileId: string) {
const mode: AnnotationActionMode = this._permissionsService.isApprover(this.#dossier(dossierId))
? 'force-redaction'
: 'request-force-redaction';
return this._makeRequest(mode, dossierId, fileId, request);
}
approve(annotationId: string, dossierId: string, fileId: string, addToDictionary: boolean = false) {
// for only here - approve the request
return this._makeRequest(
@ -236,7 +270,7 @@ export class ManualRedactionService extends GenericService<IManualAddResponse> {
@Validate()
addRedaction(@RequiredParam() body: IAddRedactionRequest, @RequiredParam() dossierId: string, @RequiredParam() fileId: string) {
const url = `${this._defaultModelPath}/redaction/add/${dossierId}/${fileId}`;
const url = `${this.redaction}/add/${dossierId}/${fileId}`;
return this._post(body, url);
}
@ -246,7 +280,7 @@ export class ManualRedactionService extends GenericService<IManualAddResponse> {
@RequiredParam() dossierId: string,
@RequiredParam() fileId: string,
) {
const url = `${this._defaultModelPath}/redaction/recategorize/${dossierId}/${fileId}`;
const url = `${this.redaction}/recategorize/${dossierId}/${fileId}`;
return this._post(body, url);
}
@ -256,13 +290,13 @@ export class ManualRedactionService extends GenericService<IManualAddResponse> {
@RequiredParam() dossierId: string,
@RequiredParam() fileId: string,
) {
const url = `${this._defaultModelPath}/request/recategorize/${dossierId}/${fileId}`;
const url = `${this.request}/recategorize/${dossierId}/${fileId}`;
return this._post(body, url);
}
@Validate()
legalBasisChange(@RequiredParam() body: ILegalBasisChangeRequest, @RequiredParam() dossierId: string, @RequiredParam() fileId: string) {
const url = `${this._defaultModelPath}/redaction/legalBasisChange/${dossierId}/${fileId}`;
const url = `${this.redaction}/legalBasisChange/${dossierId}/${fileId}`;
return this._post(body, url);
}
@ -272,7 +306,7 @@ export class ManualRedactionService extends GenericService<IManualAddResponse> {
@RequiredParam() dossierId: string,
@RequiredParam() fileId: string,
) {
const url = `${this._defaultModelPath}/request/legalBasis/${dossierId}/${fileId}`;
const url = `${this.request}/legalBasis/${dossierId}/${fileId}`;
return this._post(body, url);
}
@ -282,7 +316,7 @@ export class ManualRedactionService extends GenericService<IManualAddResponse> {
@RequiredParam() dossierId: string,
@RequiredParam() fileId: string,
) {
const url = `${this._defaultModelPath}/request/remove/${dossierId}/${fileId}`;
const url = `${this.request}/remove/${dossierId}/${fileId}`;
return this._post(body, url);
}
@ -311,20 +345,17 @@ export class ManualRedactionService extends GenericService<IManualAddResponse> {
@Validate()
removeRedaction(@RequiredParam() body: IRemoveRedactionRequest, @RequiredParam() dossierId: string, @RequiredParam() fileId: string) {
const url = `${this._defaultModelPath}/redaction/remove/${dossierId}/${fileId}`;
return this._post(body, url);
return this._post(body, `${this.redaction}/remove/${dossierId}/${fileId}`);
}
@Validate()
requestAddRedaction(@RequiredParam() body: IAddRedactionRequest, @RequiredParam() dossierId: string, @RequiredParam() fileId: string) {
const url = `${this._defaultModelPath}/request/add/${dossierId}/${fileId}`;
return this._post(body, url);
return this._post(body, `${this.request}/add/${dossierId}/${fileId}`);
}
@Validate()
forceRedaction(@RequiredParam() body: ILegalBasisChangeRequest, @RequiredParam() dossierId: string, @RequiredParam() fileId: string) {
const url = `${this._defaultModelPath}/redaction/force/${dossierId}/${fileId}`;
return this._post(body, url);
return this._post(body, `${this.redaction}/force/${dossierId}/${fileId}`);
}
@Validate()
@ -333,41 +364,38 @@ export class ManualRedactionService extends GenericService<IManualAddResponse> {
@RequiredParam() dossierId: string,
@RequiredParam() fileId: string,
) {
const url = `${this._defaultModelPath}/request/force/${dossierId}/${fileId}`;
return this._post(body, url);
return this._post(body, `${this.request}/force/${dossierId}/${fileId}`);
}
@Validate()
resize(@RequiredParam() body: IResizeRequest, @RequiredParam() dossierId: string, @RequiredParam() fileId: string) {
const url = `${this._defaultModelPath}/redaction/resize/${dossierId}/${fileId}`;
return this._post(body, url);
return this._post(body, `${this.redaction}/resize/${dossierId}/${fileId}`);
}
@Validate()
requestResize(@RequiredParam() body: IResizeRequest, @RequiredParam() dossierId: string, @RequiredParam() fileId: string) {
const url = `${this._defaultModelPath}/request/resize/${dossierId}/${fileId}`;
return this._post(body, url);
return this._post(body, `${this.request}/resize/${dossierId}/${fileId}`);
}
// #showToast(body, dossierId: string) {
// return tap({
// next: () => this._toaster.success(getMessage(mode, modifyDictionary), { positionClass: 'toast-file-preview' }),
// error: (error: HttpErrorResponse) => {
// const isConflict = error.status === HttpStatusCode.Conflict;
// this._toaster.error(getMessage(mode, modifyDictionary, true, isConflict), {
// error,
// params: {
// dictionaryName: this._dictionariesMapService.getDictionary(
// body.type as string,
// this.#dossier(dossierId).dossierTemplateId,
// ).label,
// content: body.value,
// },
// positionClass: 'toast-file-preview',
// });
// },
// });
// }
#showToast(mode: AnnotationActionMode, body, dossierId: string, modifyDictionary = false) {
return tap({
next: () => this._toaster.success(getMessage(mode, modifyDictionary), { positionClass: 'toast-file-preview' }),
error: (error: HttpErrorResponse) => {
const isConflict = error.status === HttpStatusCode.Conflict;
this._toaster.error(getMessage(mode, modifyDictionary, true, isConflict), {
error,
params: {
dictionaryName: this._dictionariesMapService.getDictionary(
body.type as string,
this.#dossier(dossierId).dossierTemplateId,
).label,
content: body.value,
},
positionClass: 'toast-file-preview',
});
},
});
}
#dossier(dossierId: string): Dossier {
return this._activeDossiersService.find(dossierId);

View File

@ -1,7 +1,13 @@
import { AnnotationActionMode } from '@red/domain';
import { AnnotationActionMode, DictionaryActions, ManualRedactionActions } from '@red/domain';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
type ActionType = { [key in AnnotationActionMode]?: { error: string; success: string; conflictError?: string } };
interface AnnotationActionResponses {
error: string;
success: string;
conflictError?: string;
}
type ActionType = { [key in AnnotationActionMode]?: AnnotationActionResponses };
export const annotationActionsTranslations: {
dictionary: ActionType;
@ -93,3 +99,90 @@ export const annotationActionsTranslations: {
},
},
};
export const dictionaryActionsTranslations: Record<DictionaryActions, AnnotationActionResponses> = {
add: {
error: _('annotation-actions.message.dictionary.add.error'),
conflictError: _('annotation-actions.message.dictionary.add.conflict-error'),
success: _('annotation-actions.message.dictionary.add.success'),
},
approve: {
error: _('annotation-actions.message.dictionary.approve.error'),
success: _('annotation-actions.message.dictionary.approve.success'),
},
decline: {
error: _('annotation-actions.message.dictionary.decline.error'),
success: _('annotation-actions.message.dictionary.decline.success'),
},
remove: {
error: _('annotation-actions.message.dictionary.remove.error'),
success: _('annotation-actions.message.dictionary.remove.success'),
},
'request-remove': {
error: _('annotation-actions.message.dictionary.request-remove.error'),
success: _('annotation-actions.message.dictionary.request-remove.success'),
},
suggest: {
error: _('annotation-actions.message.dictionary.suggest.error'),
success: _('annotation-actions.message.dictionary.suggest.success'),
},
undo: {
error: _('annotation-actions.message.dictionary.undo.error'),
success: _('annotation-actions.message.dictionary.undo.success'),
},
} as const;
export const manualRedactionActionsTranslations: Record<ManualRedactionActions, AnnotationActionResponses> = {
add: {
error: _('annotation-actions.message.manual-redaction.add.error'),
success: _('annotation-actions.message.manual-redaction.add.success'),
},
approve: {
error: _('annotation-actions.message.manual-redaction.approve.error'),
success: _('annotation-actions.message.manual-redaction.approve.success'),
},
'change-legal-basis': {
error: _('annotation-actions.message.manual-redaction.change-legal-basis.error'),
success: _('annotation-actions.message.manual-redaction.change-legal-basis.success'),
},
decline: {
error: _('annotation-actions.message.manual-redaction.decline.error'),
success: _('annotation-actions.message.manual-redaction.decline.success'),
},
'force-redaction': {
error: _('annotation-actions.message.manual-redaction.force-redaction.error'),
success: _('annotation-actions.message.manual-redaction.force-redaction.success'),
},
'recategorize-image': {
error: _('annotation-actions.message.manual-redaction.recategorize-image.error'),
success: _('annotation-actions.message.manual-redaction.recategorize-image.success'),
},
'request-change-legal-basis': {
error: _('annotation-actions.message.manual-redaction.request-change-legal-basis.error'),
success: _('annotation-actions.message.manual-redaction.request-change-legal-basis.success'),
},
'request-force-redaction': {
error: _('annotation-actions.message.manual-redaction.request-force-redaction.error'),
success: _('annotation-actions.message.manual-redaction.request-force-redaction.success'),
},
'request-image-recategorization': {
error: _('annotation-actions.message.manual-redaction.request-image-recategorization.error'),
success: _('annotation-actions.message.manual-redaction.request-image-recategorization.success'),
},
suggest: {
error: _('annotation-actions.message.manual-redaction.suggest.error'),
success: _('annotation-actions.message.manual-redaction.suggest.success'),
},
undo: {
error: _('annotation-actions.message.manual-redaction.undo.error'),
success: _('annotation-actions.message.manual-redaction.undo.success'),
},
remove: {
error: _('annotation-actions.message.manual-redaction.remove.error'),
success: _('annotation-actions.message.manual-redaction.remove.success'),
},
'request-remove': {
error: _('annotation-actions.message.manual-redaction.request-remove.error'),
success: _('annotation-actions.message.manual-redaction.request-remove.success'),
},
} as const;

View File

@ -16,3 +16,19 @@ export type AnnotationActionMode =
| 'request-force-redaction'
| 'resize'
| 'request-resize';
export type DictionaryActions = 'add' | 'approve' | 'remove' | 'decline' | 'request-remove' | 'suggest' | 'undo';
export type ManualRedactionActions =
| 'add'
| 'approve'
| 'remove'
| 'change-legal-basis'
| 'decline'
| 'request-remove'
| 'request-change-legal-basis'
| 'recategorize-image'
| 'request-image-recategorization'
| 'suggest'
| 'undo'
| 'force-redaction'
| 'request-force-redaction';