Pull request #358: Text highlights
Merge in RED/ui from text-highlights to master * commit 'c807524991ec556cbf61db82d53f0db1342ad9ca': Text highlight cleanup text-highlights
This commit is contained in:
commit
9dc94f6eca
@ -1,9 +1,10 @@
|
||||
import { annotationTypesTranslations } from '../../translations/annotation-types-translations';
|
||||
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
|
||||
import { IComment, IManualChange, IPoint, IRectangle, LogEntryStatus, ManualRedactionType } from '@red/domain';
|
||||
import { IComment, IManualChange, ImportedRedaction, IPoint, IRectangle, LogEntryStatus, ManualRedactionType } from '@red/domain';
|
||||
import { RedactionLogEntry } from '@models/file/redaction-log.entry';
|
||||
|
||||
export type AnnotationSuperType =
|
||||
| 'text-highlight'
|
||||
| 'suggestion-change-legal-basis'
|
||||
| 'suggestion-recategorize-image'
|
||||
| 'suggestion-add-dictionary'
|
||||
@ -121,6 +122,10 @@ export class AnnotationWrapper {
|
||||
}
|
||||
|
||||
get filterKey() {
|
||||
if (this.isHighlight) {
|
||||
return this.color;
|
||||
}
|
||||
|
||||
return this.topLevelFilter ? this.superType : this.superType + this.type;
|
||||
}
|
||||
|
||||
@ -154,6 +159,10 @@ export class AnnotationWrapper {
|
||||
return this.superType === 'hint';
|
||||
}
|
||||
|
||||
get isHighlight() {
|
||||
return this.superType === 'text-highlight';
|
||||
}
|
||||
|
||||
get isIgnoredHint() {
|
||||
return this.superType === 'ignored-hint';
|
||||
}
|
||||
@ -235,10 +244,34 @@ export class AnnotationWrapper {
|
||||
return this.legalBasisChangeValue || this.legalBasisValue;
|
||||
}
|
||||
|
||||
get width(): number {
|
||||
return Math.floor(this.positions[0].width);
|
||||
}
|
||||
|
||||
get height(): number {
|
||||
return Math.floor(this.positions[0].height);
|
||||
}
|
||||
|
||||
get previewAnnotation() {
|
||||
return this.isRedacted || this.isSuggestionAdd;
|
||||
}
|
||||
|
||||
static fromHighlight(color: string, entry: ImportedRedaction) {
|
||||
const annotationWrapper = new AnnotationWrapper();
|
||||
|
||||
annotationWrapper.annotationId = entry.id;
|
||||
annotationWrapper.pageNumber = entry.positions[0].page;
|
||||
annotationWrapper.superType = 'text-highlight';
|
||||
annotationWrapper.typeValue = 'text-highlight';
|
||||
annotationWrapper.value = 'Imported';
|
||||
annotationWrapper.color = color;
|
||||
annotationWrapper.positions = entry.positions;
|
||||
annotationWrapper.firstTopLeftPoint = entry.positions[0]?.topLeft;
|
||||
annotationWrapper.typeLabel = annotationTypesTranslations[annotationWrapper.superType];
|
||||
|
||||
return annotationWrapper;
|
||||
}
|
||||
|
||||
static fromData(redactionLogEntry?: RedactionLogEntry) {
|
||||
const annotationWrapper = new AnnotationWrapper();
|
||||
|
||||
|
||||
@ -7,6 +7,7 @@ import {
|
||||
IViewedPage,
|
||||
LogEntryStatus,
|
||||
ManualRedactionType,
|
||||
TextHighlightResponse,
|
||||
ViewMode,
|
||||
} from '@red/domain';
|
||||
import { AnnotationWrapper } from './annotation.wrapper';
|
||||
@ -19,6 +20,8 @@ export class FileDataModel {
|
||||
allAnnotations: AnnotationWrapper[] = [];
|
||||
readonly hasChangeLog$ = new BehaviorSubject<boolean>(false);
|
||||
missingTypes = new Set<string>();
|
||||
_textHighlightResponse: TextHighlightResponse;
|
||||
textHighlightAnnotations: AnnotationWrapper[] = [];
|
||||
|
||||
constructor(
|
||||
private readonly _file: File,
|
||||
@ -39,7 +42,24 @@ export class FileDataModel {
|
||||
this._buildAllAnnotations();
|
||||
}
|
||||
|
||||
set textHighlights(textHighlightResponse: TextHighlightResponse) {
|
||||
this._textHighlightResponse = textHighlightResponse;
|
||||
|
||||
const highlights = [];
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||
for (const color of Object.keys(textHighlightResponse.redactionPerColor)) {
|
||||
for (const entry of textHighlightResponse.redactionPerColor[color]) {
|
||||
const annotation = AnnotationWrapper.fromHighlight(color, entry);
|
||||
highlights.push(annotation);
|
||||
}
|
||||
}
|
||||
this.textHighlightAnnotations = highlights;
|
||||
}
|
||||
|
||||
getVisibleAnnotations(viewMode: ViewMode) {
|
||||
if (viewMode === 'TEXT_HIGHLIGHTS') {
|
||||
return this.textHighlightAnnotations;
|
||||
}
|
||||
return this.allAnnotations.filter(annotation => {
|
||||
if (viewMode === 'STANDARD') {
|
||||
return !annotation.isChangeLogRemoved;
|
||||
|
||||
@ -25,6 +25,8 @@ import { OverlayModule } from '@angular/cdk/overlay';
|
||||
import { SharedDossiersModule } from './shared/shared-dossiers.module';
|
||||
import { ResizeAnnotationDialogComponent } from './dialogs/resize-annotation-dialog/resize-annotation-dialog.component';
|
||||
import { EditDossierTeamComponent } from './dialogs/edit-dossier-dialog/edit-dossier-team/edit-dossier-team.component';
|
||||
import { HighlightActionDialogComponent } from './screens/file-preview-screen/dialogs/highlight-action-dialog/highlight-action-dialog.component';
|
||||
import { ColorPickerModule } from 'ngx-color-picker';
|
||||
|
||||
const screens = [SearchScreenComponent];
|
||||
|
||||
@ -39,6 +41,7 @@ const dialogs = [
|
||||
AssignReviewerApproverDialogComponent,
|
||||
ChangeLegalBasisDialogComponent,
|
||||
RecategorizeImageDialogComponent,
|
||||
HighlightActionDialogComponent,
|
||||
];
|
||||
|
||||
const components = [
|
||||
@ -58,6 +61,14 @@ const services = [DossiersDialogService, ManualAnnotationService, AnnotationProc
|
||||
@NgModule({
|
||||
declarations: [...components],
|
||||
providers: [...services],
|
||||
imports: [CommonModule, SharedModule, SharedDossiersModule, FileUploadDownloadModule, DossiersRoutingModule, OverlayModule],
|
||||
imports: [
|
||||
CommonModule,
|
||||
SharedModule,
|
||||
SharedDossiersModule,
|
||||
FileUploadDownloadModule,
|
||||
DossiersRoutingModule,
|
||||
OverlayModule,
|
||||
ColorPickerModule,
|
||||
],
|
||||
})
|
||||
export class DossiersModule {}
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
<div>
|
||||
<strong>{{ annotation.typeLabel | translate }}</strong>
|
||||
</div>
|
||||
<div *ngIf="annotation?.type !== 'manual'">
|
||||
<div *ngIf="annotation.type !== 'manual' && !annotation.isHighlight">
|
||||
<strong>
|
||||
<span>{{ annotation.descriptor | translate }}</span
|
||||
>: </strong
|
||||
@ -14,6 +14,12 @@
|
||||
<div *ngIf="annotation.shortContent && !annotation.isHint">
|
||||
<strong><span translate="content"></span>: </strong>{{ annotation.shortContent }}
|
||||
</div>
|
||||
<div *ngIf="annotation.isHighlight">
|
||||
<strong><span translate="color"></span>: </strong>{{ annotation.color }}
|
||||
</div>
|
||||
<div *ngIf="annotation.isHighlight">
|
||||
<strong><span translate="size"></span>: </strong>{{ annotation.width }}x{{ annotation.height }} px
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="active-icon-marker-container">
|
||||
|
||||
@ -1,46 +1,75 @@
|
||||
<div
|
||||
(click)="annotationClicked(annotation, $event)"
|
||||
*ngFor="let annotation of annotations"
|
||||
[attr.annotation-id]="annotation.id"
|
||||
[attr.annotation-page]="activeViewerPage"
|
||||
[class.active]="isSelected(annotation.annotationId)"
|
||||
[class.multi-select-active]="multiSelectService.active$ | async"
|
||||
class="annotation-wrapper"
|
||||
>
|
||||
<div class="active-bar-marker"></div>
|
||||
|
||||
<div [class.removed]="annotation.isChangeLogRemoved" class="annotation">
|
||||
<redaction-annotation-card
|
||||
[annotation]="annotation"
|
||||
[isSelected]="isSelected(annotation.annotationId)"
|
||||
[matTooltip]="annotation.content"
|
||||
matTooltipPosition="above"
|
||||
></redaction-annotation-card>
|
||||
|
||||
<div class="actions-wrapper">
|
||||
<div
|
||||
(click)="comments.toggleExpandComments($event)"
|
||||
[matTooltip]="'comments.comments' | translate: { count: annotation.comments?.length }"
|
||||
class="comments-counter"
|
||||
matTooltipPosition="above"
|
||||
>
|
||||
<mat-icon svgIcon="red:comment"></mat-icon>
|
||||
{{ annotation.comments.length }}
|
||||
</div>
|
||||
|
||||
<div *ngIf="multiSelectService.inactive$ | async" class="actions">
|
||||
<ng-container
|
||||
[ngTemplateOutletContext]="{ annotation: annotation }"
|
||||
[ngTemplateOutlet]="annotationActionsTemplate"
|
||||
></ng-container>
|
||||
</div>
|
||||
<ng-container *ngFor="let annotation of annotations; let idx = index">
|
||||
<div *ngIf="showHighlightGroup(idx) as highlightGroup" class="workload-separator">
|
||||
<div>
|
||||
<redaction-type-annotation-icon [annotation]="annotation" class="mr-8"></redaction-type-annotation-icon>
|
||||
<span [translateParams]="highlightGroup" [translate]="'highlights'" class="all-caps-label"></span>
|
||||
</div>
|
||||
|
||||
<redaction-comments #comments [annotation]="annotation"></redaction-comments>
|
||||
<div *ngIf="state.isWritable$ | async">
|
||||
<iqser-circle-button
|
||||
(action)="convertHighlights(highlightGroup)"
|
||||
[size]="28"
|
||||
[tooltip]="'file-preview.highlights.convert' | translate"
|
||||
[type]="circleButtonTypes.dark"
|
||||
class="mr-2"
|
||||
icon="iqser:plus"
|
||||
tooltipPosition="above"
|
||||
></iqser-circle-button>
|
||||
|
||||
<iqser-circle-button
|
||||
(action)="removeHighlights(highlightGroup)"
|
||||
[size]="28"
|
||||
[tooltip]="'file-preview.highlights.remove' | translate"
|
||||
[type]="circleButtonTypes.dark"
|
||||
icon="iqser:trash"
|
||||
tooltipPosition="above"
|
||||
></iqser-circle-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<redaction-annotation-details [annotation]="annotation" [isSelected]="isSelected(annotation.id)"></redaction-annotation-details>
|
||||
</div>
|
||||
<div
|
||||
(click)="annotationClicked(annotation, $event)"
|
||||
[attr.annotation-id]="annotation.id"
|
||||
[attr.annotation-page]="activeViewerPage"
|
||||
[class.active]="isSelected(annotation.annotationId)"
|
||||
[class.multi-select-active]="multiSelectService.active$ | async"
|
||||
class="annotation-wrapper"
|
||||
>
|
||||
<div class="active-bar-marker"></div>
|
||||
|
||||
<div [class.removed]="annotation.isChangeLogRemoved" class="annotation">
|
||||
<redaction-annotation-card
|
||||
[annotation]="annotation"
|
||||
[isSelected]="isSelected(annotation.annotationId)"
|
||||
[matTooltip]="annotation.content"
|
||||
matTooltipPosition="above"
|
||||
></redaction-annotation-card>
|
||||
|
||||
<div *ngIf="!annotation.isHighlight" class="actions-wrapper">
|
||||
<div
|
||||
(click)="comments.toggleExpandComments($event)"
|
||||
[matTooltip]="'comments.comments' | translate: { count: annotation.comments?.length }"
|
||||
class="comments-counter"
|
||||
matTooltipPosition="above"
|
||||
>
|
||||
<mat-icon svgIcon="red:comment"></mat-icon>
|
||||
{{ annotation.comments.length }}
|
||||
</div>
|
||||
|
||||
<div *ngIf="multiSelectService.inactive$ | async" class="actions">
|
||||
<ng-container
|
||||
[ngTemplateOutletContext]="{ annotation: annotation }"
|
||||
[ngTemplateOutlet]="annotationActionsTemplate"
|
||||
></ng-container>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<redaction-comments #comments [annotation]="annotation"></redaction-comments>
|
||||
</div>
|
||||
|
||||
<redaction-annotation-details [annotation]="annotation" [isSelected]="isSelected(annotation.id)"></redaction-annotation-details>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="annotationReferencesService.annotation$ | async">
|
||||
<redaction-annotation-references-list
|
||||
|
||||
@ -76,3 +76,7 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.workload-separator > div {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
@ -1,11 +1,20 @@
|
||||
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, Output, SimpleChanges, TemplateRef } from '@angular/core';
|
||||
import { AnnotationWrapper } from '@models/file/annotation.wrapper';
|
||||
import { FilterService, IqserEventTarget } from '@iqser/common-ui';
|
||||
import { CircleButtonTypes, FilterService, IqserEventTarget } from '@iqser/common-ui';
|
||||
import { MultiSelectService } from '../../services/multi-select.service';
|
||||
import { AnnotationReferencesService } from '../../services/annotation-references.service';
|
||||
import { ViewModeService } from '../../services/view-mode.service';
|
||||
import { FilePreviewStateService } from '../../services/file-preview-state.service';
|
||||
import { UserPreferenceService } from '../../../../../../services/user-preference.service';
|
||||
import { UserPreferenceService } from '@services/user-preference.service';
|
||||
import { ViewModeService } from '../../services/view-mode.service';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { DossiersDialogService } from '../../../../services/dossiers-dialog.service';
|
||||
import { TextHighlightOperation } from '@red/domain';
|
||||
|
||||
interface HighlightGroup {
|
||||
startIdx: number;
|
||||
color: string;
|
||||
length: number;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'redaction-annotations-list',
|
||||
@ -14,6 +23,8 @@ import { UserPreferenceService } from '../../../../../../services/user-preferenc
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class AnnotationsListComponent implements OnChanges {
|
||||
readonly circleButtonTypes = CircleButtonTypes;
|
||||
|
||||
@Input() annotations: AnnotationWrapper[];
|
||||
@Input() selectedAnnotations: AnnotationWrapper[];
|
||||
@Input() annotationActionsTemplate: TemplateRef<unknown>;
|
||||
@ -23,19 +34,36 @@ export class AnnotationsListComponent implements OnChanges {
|
||||
@Output() readonly selectAnnotations = new EventEmitter<AnnotationWrapper[]>();
|
||||
@Output() readonly deselectAnnotations = new EventEmitter<AnnotationWrapper[]>();
|
||||
|
||||
highlightGroups$ = new BehaviorSubject<HighlightGroup[]>([]);
|
||||
|
||||
constructor(
|
||||
readonly multiSelectService: MultiSelectService,
|
||||
readonly viewModeService: ViewModeService,
|
||||
readonly annotationReferencesService: AnnotationReferencesService,
|
||||
readonly state: FilePreviewStateService,
|
||||
private readonly _filterService: FilterService,
|
||||
private readonly _state: FilePreviewStateService,
|
||||
private readonly _userPreferenceService: UserPreferenceService,
|
||||
private readonly _viewModeService: ViewModeService,
|
||||
private readonly _dialogService: DossiersDialogService,
|
||||
) {}
|
||||
|
||||
convertHighlights(highlightGroup: HighlightGroup): void {
|
||||
const data = this._getActionData(highlightGroup, TextHighlightOperation.CONVERT);
|
||||
this._dialogService.openDialog('highlightAction', null, data);
|
||||
}
|
||||
|
||||
removeHighlights(highlightGroup: HighlightGroup): void {
|
||||
const data = this._getActionData(highlightGroup, TextHighlightOperation.REMOVE);
|
||||
this._dialogService.openDialog('highlightAction', null, data);
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
if (changes.annotations && this.annotations) {
|
||||
if (changes.annotations && this.annotations && !this._viewModeService.isTextHighlights) {
|
||||
this.annotations = this.annotations.sort(this.annotationsPositionCompare);
|
||||
}
|
||||
|
||||
if (this._viewModeService.isTextHighlights) {
|
||||
this._updateHighlightGroups();
|
||||
}
|
||||
}
|
||||
|
||||
annotationClicked(annotation: AnnotationWrapper, $event: MouseEvent): void {
|
||||
@ -80,4 +108,37 @@ export class AnnotationsListComponent implements OnChanges {
|
||||
return first.x < second.y ? -1 : 1;
|
||||
}
|
||||
}
|
||||
|
||||
showHighlightGroup(idx: number): HighlightGroup {
|
||||
return this._viewModeService.isTextHighlights && this.highlightGroups$.value.find(h => h.startIdx === idx);
|
||||
}
|
||||
|
||||
private _getActionData(highlightGroup: HighlightGroup, operation: TextHighlightOperation) {
|
||||
return {
|
||||
dossierId: this.state.dossierId,
|
||||
fileId: this.state.fileId,
|
||||
color: highlightGroup.color,
|
||||
operation,
|
||||
};
|
||||
}
|
||||
|
||||
private _updateHighlightGroups(): void {
|
||||
if (!this.annotations?.length) {
|
||||
return;
|
||||
}
|
||||
const highlightGroups: HighlightGroup[] = [];
|
||||
let lastGroup: HighlightGroup;
|
||||
for (let idx = 0; idx < this.annotations.length; ++idx) {
|
||||
if (idx === 0 || this.annotations[idx].color !== this.annotations[idx - 1].color) {
|
||||
if (lastGroup) {
|
||||
highlightGroups.push(lastGroup);
|
||||
}
|
||||
lastGroup = { startIdx: idx, length: 1, color: this.annotations[idx].color };
|
||||
} else {
|
||||
lastGroup.length += 1;
|
||||
}
|
||||
}
|
||||
highlightGroups.push(lastGroup);
|
||||
this.highlightGroups$.next(highlightGroups);
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,7 +10,8 @@
|
||||
</div>
|
||||
|
||||
<ng-template #selectAndFilter>
|
||||
<div class="right-title heading" translate="file-preview.tabs.annotations.label">
|
||||
<div class="right-title heading">
|
||||
{{ title$ | async | translate }}
|
||||
<div>
|
||||
<div
|
||||
(click)="multiSelectService.activate()"
|
||||
@ -111,16 +112,18 @@
|
||||
|
||||
<div style="overflow: hidden; width: 100%">
|
||||
<div
|
||||
*ngIf="(isHighlights$ | async) === false"
|
||||
[attr.anotation-page-header]="activeViewerPage"
|
||||
[class.padding-left-0]="currentPageIsExcluded"
|
||||
[hidden]="excludedPagesService.shown$ | async"
|
||||
class="page-separator"
|
||||
class="workload-separator"
|
||||
>
|
||||
<span *ngIf="!!activeViewerPage" class="flex-align-items-center">
|
||||
<iqser-circle-button
|
||||
(action)="excludedPagesService.toggle()"
|
||||
*ngIf="currentPageIsExcluded"
|
||||
[tooltip]="'file-preview.excluded-from-redaction' | translate | capitalize"
|
||||
class="excluded"
|
||||
icon="red:exclude-pages"
|
||||
tooltipPosition="above"
|
||||
></iqser-circle-button>
|
||||
|
||||
@ -115,25 +115,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.page-separator {
|
||||
border-bottom: 1px solid variables.$separator;
|
||||
height: 32px;
|
||||
box-sizing: border-box;
|
||||
padding: 0 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background-color: variables.$grey-6;
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
|
||||
> div:not(:last-child) {
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.annotations {
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
@ -157,7 +138,7 @@
|
||||
padding-left: 0 !important;
|
||||
}
|
||||
|
||||
::ng-deep .page-separator iqser-circle-button mat-icon {
|
||||
::ng-deep .workload-separator iqser-circle-button.excluded mat-icon {
|
||||
color: var(--iqser-primary);
|
||||
}
|
||||
|
||||
|
||||
@ -34,6 +34,8 @@ import { MultiSelectService } from '../../services/multi-select.service';
|
||||
import { DocumentInfoService } from '../../services/document-info.service';
|
||||
import { SkippedService } from '../../services/skipped.service';
|
||||
import { FilePreviewStateService } from '../../services/file-preview-state.service';
|
||||
import { ViewModeService } from '../../services/view-mode.service';
|
||||
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
|
||||
|
||||
const COMMAND_KEY_ARRAY = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Escape'];
|
||||
const ALL_HOTKEY_ARRAY = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'];
|
||||
@ -67,6 +69,8 @@ export class FileWorkloadComponent {
|
||||
readonly multiSelectActive$: Observable<boolean>;
|
||||
readonly multiSelectInactive$: Observable<boolean>;
|
||||
readonly showExcludedPages$: Observable<boolean>;
|
||||
readonly title$: Observable<string>;
|
||||
readonly isHighlights$: Observable<boolean>;
|
||||
private _annotations$ = new BehaviorSubject<AnnotationWrapper[]>([]);
|
||||
@ViewChild('annotationsElement') private readonly _annotationsElement: ElementRef;
|
||||
@ViewChild('quickNavigation') private readonly _quickNavigationElement: ElementRef;
|
||||
@ -78,6 +82,7 @@ export class FileWorkloadComponent {
|
||||
readonly multiSelectService: MultiSelectService,
|
||||
readonly documentInfoService: DocumentInfoService,
|
||||
readonly excludedPagesService: ExcludedPagesService,
|
||||
private readonly _viewModeService: ViewModeService,
|
||||
private readonly _changeDetectorRef: ChangeDetectorRef,
|
||||
private readonly _permissionsService: PermissionsService,
|
||||
private readonly _annotationProcessingService: AnnotationProcessingService,
|
||||
@ -86,6 +91,8 @@ export class FileWorkloadComponent {
|
||||
this.multiSelectActive$ = this._multiSelectActive$;
|
||||
this.multiSelectInactive$ = this._multiSelectInactive$;
|
||||
this.showExcludedPages$ = this._showExcludedPages$;
|
||||
this.isHighlights$ = this._isHighlights$;
|
||||
this.title$ = this._title$;
|
||||
}
|
||||
|
||||
@Input()
|
||||
@ -112,6 +119,16 @@ export class FileWorkloadComponent {
|
||||
);
|
||||
}
|
||||
|
||||
private get _title$(): Observable<string> {
|
||||
return this.isHighlights$.pipe(
|
||||
map(isHighlights => (isHighlights ? _('file-preview.tabs.highlights.label') : _('file-preview.tabs.annotations.label'))),
|
||||
);
|
||||
}
|
||||
|
||||
private get _isHighlights$(): Observable<boolean> {
|
||||
return this._viewModeService.viewMode$.pipe(map(() => this._viewModeService.isTextHighlights));
|
||||
}
|
||||
|
||||
private get _multiSelectInactive$() {
|
||||
return this.multiSelectService.inactive$.pipe(
|
||||
tap(value => {
|
||||
|
||||
@ -26,9 +26,11 @@ export class TypeAnnotationIconComponent implements OnChanges {
|
||||
return;
|
||||
}
|
||||
|
||||
const { isSuggestion, isRecommendation, isSkipped, isDeclinedSuggestion, isHint, isIgnoredHint } = this.annotation;
|
||||
const { isHighlight, isSuggestion, isRecommendation, isSkipped, isDeclinedSuggestion, isHint, isIgnoredHint } = this.annotation;
|
||||
|
||||
if (this.annotation.isSuperTypeBasedColor) {
|
||||
if (isHighlight) {
|
||||
this.color = this.annotation.color;
|
||||
} else if (this.annotation.isSuperTypeBasedColor) {
|
||||
this.color = this._dictionariesMapService.getDictionaryColor(this.annotation.superType, this._dossierTemplateId);
|
||||
} else {
|
||||
this.color = this._dictionariesMapService.getDictionaryColor(this.annotation.type, this._dossierTemplateId);
|
||||
@ -36,6 +38,7 @@ export class TypeAnnotationIconComponent implements OnChanges {
|
||||
|
||||
this.type =
|
||||
isSuggestion || isDeclinedSuggestion ? 'rhombus' : isHint || isIgnoredHint ? 'circle' : isRecommendation ? 'hexagon' : 'square';
|
||||
this.label = isSuggestion || isDeclinedSuggestion ? 'S' : isSkipped ? 'S' : this.annotation.type[0].toUpperCase();
|
||||
|
||||
this.label = isHighlight ? '' : isSuggestion || isDeclinedSuggestion || isSkipped ? 'S' : this.annotation.type[0].toUpperCase();
|
||||
}
|
||||
}
|
||||
|
||||
@ -30,4 +30,15 @@
|
||||
>
|
||||
{{ 'file-preview.redacted' | translate }}
|
||||
</button>
|
||||
|
||||
<button
|
||||
(click)="switchView.emit('TEXT_HIGHLIGHTS')"
|
||||
[class.active]="viewModeService.isTextHighlights"
|
||||
[disabled]="(canSwitchToRedactedView$ | async) === false"
|
||||
[matTooltip]="'file-preview.text-highlights-tooltip' | translate"
|
||||
class="red-tab"
|
||||
>
|
||||
<!-- iqserHelpMode="text_highlights_view"-->
|
||||
{{ 'file-preview.text-highlights' | translate }}
|
||||
</button>
|
||||
</ng-container>
|
||||
|
||||
@ -0,0 +1,35 @@
|
||||
<section class="dialog">
|
||||
<form (submit)="save()" [formGroup]="form">
|
||||
<div [translate]="title" class="dialog-header heading-l"></div>
|
||||
|
||||
<div class="dialog-content">
|
||||
<div [translate]="details" class="mb-24"></div>
|
||||
|
||||
<div class="iqser-input-group required w-150">
|
||||
<label translate="highlight-action-dialog.form.color.label"></label>
|
||||
<input class="hex-color-input" formControlName="color" name="color" type="text" />
|
||||
<div
|
||||
[colorPicker]="form.get('color').value"
|
||||
[cpDisabled]="true"
|
||||
[style.background]="form.get('color').value"
|
||||
class="input-icon"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<div class="iqser-input-group">
|
||||
<mat-checkbox color="primary" formControlName="confirmation" name="confirmation">
|
||||
{{ confirmationMessage | translate }}
|
||||
</mat-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dialog-actions">
|
||||
<button [disabled]="form.invalid" color="primary" mat-flat-button type="submit">
|
||||
{{ saveMessage | translate }}
|
||||
</button>
|
||||
<div class="all-caps-label cancel" mat-dialog-close translate="highlight-action-dialog.actions.cancel"></div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<iqser-circle-button (action)="close()" class="dialog-close" icon="iqser:close"></iqser-circle-button>
|
||||
</section>
|
||||
@ -0,0 +1,70 @@
|
||||
import { Component, Inject, Injector } from '@angular/core';
|
||||
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import { TextHighlightOperation } from '@red/domain';
|
||||
import { BaseDialogComponent, LoadingService } from '@iqser/common-ui';
|
||||
import { TextHighlightService } from '../../../../services/text-highlight.service';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
|
||||
|
||||
export interface HighlightActionData {
|
||||
readonly operation: TextHighlightOperation;
|
||||
readonly color: string;
|
||||
readonly dossierId: string;
|
||||
readonly fileId: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
templateUrl: './highlight-action-dialog.component.html',
|
||||
})
|
||||
export class HighlightActionDialogComponent extends BaseDialogComponent {
|
||||
readonly title: string;
|
||||
readonly details: string;
|
||||
readonly confirmationMessage: string;
|
||||
readonly saveMessage: string;
|
||||
|
||||
constructor(
|
||||
private readonly _formBuilder: FormBuilder,
|
||||
protected readonly _injector: Injector,
|
||||
protected readonly _dialogRef: MatDialogRef<HighlightActionDialogComponent>,
|
||||
private readonly _textHighlightService: TextHighlightService,
|
||||
private readonly _loadingService: LoadingService,
|
||||
@Inject(MAT_DIALOG_DATA) readonly data: HighlightActionData,
|
||||
) {
|
||||
super(_injector, _dialogRef);
|
||||
this.form = this._getForm();
|
||||
this.initialFormValue = this.form.getRawValue();
|
||||
|
||||
this.title =
|
||||
data.operation === TextHighlightOperation.CONVERT
|
||||
? _('highlight-action-dialog.convert.title')
|
||||
: _('highlight-action-dialog.remove.title');
|
||||
this.details =
|
||||
data.operation === TextHighlightOperation.CONVERT
|
||||
? _('highlight-action-dialog.convert.details')
|
||||
: _('highlight-action-dialog.remove.details');
|
||||
this.confirmationMessage =
|
||||
data.operation === TextHighlightOperation.CONVERT
|
||||
? _('highlight-action-dialog.convert.confirmation')
|
||||
: _('highlight-action-dialog.remove.confirmation');
|
||||
this.saveMessage =
|
||||
data.operation === TextHighlightOperation.CONVERT
|
||||
? _('highlight-action-dialog.convert.save')
|
||||
: _('highlight-action-dialog.remove.save');
|
||||
}
|
||||
|
||||
async save(): Promise<void> {
|
||||
this._loadingService.start();
|
||||
const { dossierId, fileId, color, operation } = this.data;
|
||||
await firstValueFrom(this._textHighlightService.performHighlightsAction(dossierId, fileId, [color], operation));
|
||||
this._loadingService.stop();
|
||||
this._dialogRef.close(true);
|
||||
}
|
||||
|
||||
private _getForm(): FormGroup {
|
||||
return this._formBuilder.group({
|
||||
color: [{ value: this.data.color, disabled: true }, Validators.required],
|
||||
confirmation: [false, Validators.requiredTrue],
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -152,6 +152,16 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
|
||||
return;
|
||||
}
|
||||
|
||||
const textHighlightAnnotationIds = this._fileData.textHighlightAnnotations.map(a => a.id);
|
||||
const textHighlightAnnotations = this._getAnnotations((a: Core.Annotations.Annotation) =>
|
||||
textHighlightAnnotationIds.includes(a.Id),
|
||||
);
|
||||
|
||||
this._instance.Core.annotationManager.deleteAnnotations(textHighlightAnnotations, {
|
||||
imported: true,
|
||||
force: true,
|
||||
});
|
||||
|
||||
const ocrAnnotationIds = this._fileData.allAnnotations.filter(a => a.isOCR).map(a => a.id);
|
||||
const annotations = this._getAnnotations(a => a.getCustomData('redact-manager'));
|
||||
const redactions = annotations.filter(a => a.getCustomData('redaction'));
|
||||
@ -165,7 +175,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
|
||||
const nonStandardEntries = annotations.filter(a => a.getCustomData('changeLogRemoved') === 'true');
|
||||
this._setAnnotationsOpacity(standardEntries, true);
|
||||
this._show(standardEntries);
|
||||
this._hide(nonStandardEntries);
|
||||
this._hide([...nonStandardEntries]);
|
||||
break;
|
||||
}
|
||||
case 'DELTA': {
|
||||
@ -174,7 +184,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
|
||||
this._setAnnotationsColor(redactions, 'annotationColor');
|
||||
this._setAnnotationsOpacity(changeLogEntries, true);
|
||||
this._show(changeLogEntries);
|
||||
this._hide(nonChangeLogEntries);
|
||||
this._hide([...nonChangeLogEntries]);
|
||||
break;
|
||||
}
|
||||
case 'REDACTED': {
|
||||
@ -182,9 +192,23 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
|
||||
this._setAnnotationsOpacity(redactions);
|
||||
this._setAnnotationsColor(redactions, 'redactionColor');
|
||||
this._show(redactions);
|
||||
this._hide(nonRedactionEntries);
|
||||
this._hide([...nonRedactionEntries]);
|
||||
break;
|
||||
}
|
||||
case 'TEXT_HIGHLIGHTS': {
|
||||
this._loadingService.start();
|
||||
const textHighlights = await firstValueFrom(this._pdfViewerDataService.loadTextHighlightsFor(this.dossierId, this.fileId));
|
||||
this._hide(annotations);
|
||||
this._fileData.textHighlights = textHighlights;
|
||||
await this._annotationDrawService.drawAnnotations(
|
||||
this.activeViewer,
|
||||
this._fileData.textHighlightAnnotations,
|
||||
this.dossierId,
|
||||
this.fileId,
|
||||
false,
|
||||
);
|
||||
this._loadingService.stop();
|
||||
}
|
||||
}
|
||||
|
||||
await this._stampPDF();
|
||||
|
||||
@ -156,66 +156,85 @@ export class AnnotationDrawService {
|
||||
compareMode: boolean,
|
||||
) {
|
||||
const pageNumber = compareMode ? annotationWrapper.pageNumber * 2 - 1 : annotationWrapper.pageNumber;
|
||||
const dossierTemplateId = this._dossiersService.find(dossierId).dossierTemplateId;
|
||||
|
||||
let annotation: Core.Annotations.RectangleAnnotation | Core.Annotations.TextHighlightAnnotation;
|
||||
if (annotationWrapper.rectangle || annotationWrapper.isImage) {
|
||||
annotation = new activeViewer.Core.Annotations.RectangleAnnotation();
|
||||
if (annotationWrapper.superType === 'text-highlight') {
|
||||
const rectangleAnnot = new activeViewer.Core.Annotations.RectangleAnnotation();
|
||||
const pageHeight = activeViewer.Core.documentViewer.getPageHeight(pageNumber);
|
||||
const firstPosition = annotationWrapper.positions[0];
|
||||
annotation.X = firstPosition.topLeft.x;
|
||||
annotation.Y = pageHeight - (firstPosition.topLeft.y + firstPosition.height);
|
||||
annotation.Width = firstPosition.width;
|
||||
annotation.FillColor = this.getAndConvertColor(
|
||||
const rectangle: IRectangle = annotationWrapper.positions[0];
|
||||
rectangleAnnot.PageNumber = pageNumber;
|
||||
rectangleAnnot.X = rectangle.topLeft.x;
|
||||
rectangleAnnot.Y = pageHeight - (rectangle.topLeft.y + rectangle.height);
|
||||
rectangleAnnot.Width = rectangle.width;
|
||||
rectangleAnnot.Height = rectangle.height;
|
||||
rectangleAnnot.ReadOnly = true;
|
||||
rectangleAnnot.StrokeColor = this.convertColor(activeViewer, annotationWrapper.color);
|
||||
rectangleAnnot.StrokeThickness = 1;
|
||||
rectangleAnnot.Id = annotationWrapper.id;
|
||||
|
||||
return rectangleAnnot;
|
||||
} else {
|
||||
const dossierTemplateId = this._dossiersService.find(dossierId).dossierTemplateId;
|
||||
|
||||
let annotation: Core.Annotations.RectangleAnnotation | Core.Annotations.TextHighlightAnnotation;
|
||||
if (annotationWrapper.rectangle || annotationWrapper.isImage) {
|
||||
annotation = new activeViewer.Core.Annotations.RectangleAnnotation();
|
||||
const pageHeight = activeViewer.Core.documentViewer.getPageHeight(pageNumber);
|
||||
const firstPosition = annotationWrapper.positions[0];
|
||||
annotation.X = firstPosition.topLeft.x;
|
||||
annotation.Y = pageHeight - (firstPosition.topLeft.y + firstPosition.height);
|
||||
annotation.Width = firstPosition.width;
|
||||
annotation.FillColor = this.getAndConvertColor(
|
||||
activeViewer,
|
||||
dossierTemplateId,
|
||||
annotationWrapper.superType,
|
||||
annotationWrapper.type,
|
||||
);
|
||||
annotation.Opacity = annotationWrapper.isChangeLogRemoved
|
||||
? AnnotationDrawService.DEFAULT_REMOVED_ANNOTATION_OPACITY
|
||||
: AnnotationDrawService.DEFAULT_RECTANGLE_ANNOTATION_OPACITY;
|
||||
annotation.Height = firstPosition.height;
|
||||
annotation.Intensity = 100;
|
||||
} else {
|
||||
annotation = new activeViewer.Core.Annotations.TextHighlightAnnotation();
|
||||
annotation.Quads = this._rectanglesToQuads(annotationWrapper.positions, activeViewer, pageNumber);
|
||||
annotation.Opacity = annotationWrapper.isChangeLogRemoved
|
||||
? AnnotationDrawService.DEFAULT_REMOVED_ANNOTATION_OPACITY
|
||||
: AnnotationDrawService.DEFAULT_TEXT_ANNOTATION_OPACITY;
|
||||
}
|
||||
|
||||
annotation.setContents(annotationWrapper.content);
|
||||
|
||||
annotation.PageNumber = pageNumber;
|
||||
annotation.StrokeColor = this.getAndConvertColor(
|
||||
activeViewer,
|
||||
dossierTemplateId,
|
||||
annotationWrapper.superType,
|
||||
annotationWrapper.type,
|
||||
);
|
||||
annotation.Opacity = annotationWrapper.isChangeLogRemoved
|
||||
? AnnotationDrawService.DEFAULT_REMOVED_ANNOTATION_OPACITY
|
||||
: AnnotationDrawService.DEFAULT_RECTANGLE_ANNOTATION_OPACITY;
|
||||
annotation.Height = firstPosition.height;
|
||||
annotation.Intensity = 100;
|
||||
} else {
|
||||
annotation = new activeViewer.Core.Annotations.TextHighlightAnnotation();
|
||||
annotation.Quads = this._rectanglesToQuads(annotationWrapper.positions, activeViewer, pageNumber);
|
||||
annotation.Opacity = annotationWrapper.isChangeLogRemoved
|
||||
? AnnotationDrawService.DEFAULT_REMOVED_ANNOTATION_OPACITY
|
||||
: AnnotationDrawService.DEFAULT_TEXT_ANNOTATION_OPACITY;
|
||||
annotation.Id = annotationWrapper.id;
|
||||
annotation.ReadOnly = true;
|
||||
// change log entries are drawn lighter
|
||||
|
||||
annotation.Hidden =
|
||||
annotationWrapper.isChangeLogRemoved ||
|
||||
(this._skippedService.hideSkipped && annotationWrapper.isSkipped) ||
|
||||
annotationWrapper.isOCR ||
|
||||
annotationWrapper.hidden;
|
||||
annotation.setCustomData('redact-manager', 'true');
|
||||
annotation.setCustomData('redaction', String(annotationWrapper.previewAnnotation));
|
||||
annotation.setCustomData('highlight', String(annotationWrapper.isHighlight));
|
||||
annotation.setCustomData('skipped', String(annotationWrapper.isSkipped));
|
||||
annotation.setCustomData('changeLog', String(annotationWrapper.isChangeLogEntry));
|
||||
annotation.setCustomData('changeLogRemoved', String(annotationWrapper.isChangeLogRemoved));
|
||||
annotation.setCustomData('opacity', String(annotation.Opacity));
|
||||
annotation.setCustomData('redactionColor', String(this.getColor(activeViewer, dossierTemplateId, 'redaction', 'redaction')));
|
||||
annotation.setCustomData(
|
||||
'annotationColor',
|
||||
String(this.getColor(activeViewer, dossierTemplateId, annotationWrapper.superType, annotationWrapper.type)),
|
||||
);
|
||||
|
||||
return annotation;
|
||||
}
|
||||
|
||||
annotation.setContents(annotationWrapper.content);
|
||||
|
||||
annotation.PageNumber = pageNumber;
|
||||
annotation.StrokeColor = this.getAndConvertColor(
|
||||
activeViewer,
|
||||
dossierTemplateId,
|
||||
annotationWrapper.superType,
|
||||
annotationWrapper.type,
|
||||
);
|
||||
annotation.Id = annotationWrapper.id;
|
||||
annotation.ReadOnly = true;
|
||||
// change log entries are drawn lighter
|
||||
|
||||
annotation.Hidden =
|
||||
annotationWrapper.isChangeLogRemoved ||
|
||||
(this._skippedService.hideSkipped && annotationWrapper.isSkipped) ||
|
||||
annotationWrapper.isOCR ||
|
||||
annotationWrapper.hidden;
|
||||
annotation.setCustomData('redact-manager', 'true');
|
||||
annotation.setCustomData('redaction', String(annotationWrapper.previewAnnotation));
|
||||
annotation.setCustomData('skipped', String(annotationWrapper.isSkipped));
|
||||
annotation.setCustomData('changeLog', String(annotationWrapper.isChangeLogEntry));
|
||||
annotation.setCustomData('changeLogRemoved', String(annotationWrapper.isChangeLogRemoved));
|
||||
annotation.setCustomData('opacity', String(annotation.Opacity));
|
||||
annotation.setCustomData('redactionColor', String(this.getColor(activeViewer, dossierTemplateId, 'redaction', 'redaction')));
|
||||
annotation.setCustomData(
|
||||
'annotationColor',
|
||||
String(this.getColor(activeViewer, dossierTemplateId, annotationWrapper.superType, annotationWrapper.type)),
|
||||
);
|
||||
|
||||
return annotation;
|
||||
}
|
||||
|
||||
private _rectanglesToQuads(positions: IRectangle[], activeViewer: WebViewerInstance, pageNumber: number): any[] {
|
||||
|
||||
@ -43,6 +43,10 @@ export class ViewModeService {
|
||||
return this._viewMode$.value === 'REDACTED';
|
||||
}
|
||||
|
||||
get isTextHighlights() {
|
||||
return this._viewMode$.value === 'TEXT_HIGHLIGHTS';
|
||||
}
|
||||
|
||||
get isCompare() {
|
||||
return this._compareMode$.value;
|
||||
}
|
||||
@ -63,6 +67,10 @@ export class ViewModeService {
|
||||
this._switchTo('REDACTED');
|
||||
}
|
||||
|
||||
switchToHighlights() {
|
||||
this._switchTo('TEXT_HIGHLIGHTS');
|
||||
}
|
||||
|
||||
private _switchTo(mode: ViewMode) {
|
||||
this._viewMode$.next(mode);
|
||||
}
|
||||
|
||||
@ -58,7 +58,13 @@ export class AnnotationProcessingService {
|
||||
} else {
|
||||
// top level filter
|
||||
if (topLevelFilter) {
|
||||
this._createParentFilter(a.superType, filterMap, filters);
|
||||
this._createParentFilter(
|
||||
a.isHighlight ? a.filterKey : a.superType,
|
||||
filterMap,
|
||||
filters,
|
||||
a.isHighlight,
|
||||
a.isHighlight ? a.color : null,
|
||||
);
|
||||
} else {
|
||||
let parentFilter = filterMap.get(a.superType);
|
||||
if (!parentFilter) {
|
||||
@ -124,18 +130,28 @@ export class AnnotationProcessingService {
|
||||
}
|
||||
|
||||
obj.forEach((values, page) => {
|
||||
obj.set(page, this._sortAnnotations(values));
|
||||
if (!values[0].isHighlight) {
|
||||
obj.set(page, this._sortAnnotations(values));
|
||||
}
|
||||
});
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
private _createParentFilter(key: string, filterMap: Map<string, INestedFilter>, filters: INestedFilter[]) {
|
||||
private _createParentFilter(
|
||||
key: string,
|
||||
filterMap: Map<string, INestedFilter>,
|
||||
filters: INestedFilter[],
|
||||
skipTranslation = false,
|
||||
color?: string,
|
||||
) {
|
||||
const filter: INestedFilter = new NestedFilter({
|
||||
id: key,
|
||||
topLevelFilter: true,
|
||||
matches: 1,
|
||||
label: annotationTypesTranslations[key],
|
||||
label: skipTranslation ? key : annotationTypesTranslations[key],
|
||||
skipTranslation,
|
||||
color,
|
||||
});
|
||||
filterMap.set(key, filter);
|
||||
filters.push(filter);
|
||||
|
||||
@ -11,6 +11,7 @@ import { ChangeLegalBasisDialogComponent } from '../dialogs/change-legal-basis-d
|
||||
import { RecategorizeImageDialogComponent } from '../dialogs/recategorize-image-dialog/recategorize-image-dialog.component';
|
||||
import { ConfirmationDialogComponent, DialogConfig, DialogService, largeDialogConfig } from '@iqser/common-ui';
|
||||
import { ResizeAnnotationDialogComponent } from '../dialogs/resize-annotation-dialog/resize-annotation-dialog.component';
|
||||
import { HighlightActionDialogComponent } from '../screens/file-preview-screen/dialogs/highlight-action-dialog/highlight-action-dialog.component';
|
||||
|
||||
type DialogType =
|
||||
| 'confirm'
|
||||
@ -23,7 +24,8 @@ type DialogType =
|
||||
| 'removeAnnotations'
|
||||
| 'resizeAnnotation'
|
||||
| 'forceAnnotation'
|
||||
| 'manualAnnotation';
|
||||
| 'manualAnnotation'
|
||||
| 'highlightAction';
|
||||
|
||||
@Injectable()
|
||||
export class DossiersDialogService extends DialogService<DialogType> {
|
||||
@ -67,6 +69,9 @@ export class DossiersDialogService extends DialogService<DialogType> {
|
||||
component: ManualAnnotationDialogComponent,
|
||||
dialogConfig: { autoFocus: true },
|
||||
},
|
||||
highlightAction: {
|
||||
component: HighlightActionDialogComponent,
|
||||
},
|
||||
};
|
||||
|
||||
constructor(protected readonly _dialog: MatDialog) {
|
||||
|
||||
@ -3,7 +3,7 @@ import { forkJoin, Observable, of } from 'rxjs';
|
||||
import { catchError, map, tap } from 'rxjs/operators';
|
||||
import { FileDataModel } from '@models/file/file-data.model';
|
||||
import { PermissionsService } from '@services/permissions.service';
|
||||
import { Dictionary, File, IRedactionLog, IViewedPage } from '@red/domain';
|
||||
import { Dictionary, File, IRedactionLog, IViewedPage, TextHighlightResponse } from '@red/domain';
|
||||
import { RedactionLogService } from './redaction-log.service';
|
||||
import { ViewedPagesService } from '@services/entity-services/viewed-pages.service';
|
||||
import { UserPreferenceService } from '@services/user-preference.service';
|
||||
@ -11,12 +11,14 @@ import { FilePreviewStateService } from '../screens/file-preview-screen/services
|
||||
import { Toaster } from '@iqser/common-ui';
|
||||
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
|
||||
import { DictionariesMapService } from '@services/entity-services/dictionaries-map.service';
|
||||
import { TextHighlightService } from './text-highlight.service';
|
||||
|
||||
@Injectable()
|
||||
export class PdfViewerDataService {
|
||||
constructor(
|
||||
private readonly _permissionsService: PermissionsService,
|
||||
private readonly _redactionLogService: RedactionLogService,
|
||||
private readonly _textHighlightService: TextHighlightService,
|
||||
private readonly _viewedPagesService: ViewedPagesService,
|
||||
private readonly _userPreferenceService: UserPreferenceService,
|
||||
private readonly _stateService: FilePreviewStateService,
|
||||
@ -31,6 +33,10 @@ export class PdfViewerDataService {
|
||||
);
|
||||
}
|
||||
|
||||
loadTextHighlightsFor(dossierId: string, fileId: string): Observable<TextHighlightResponse> {
|
||||
return this._textHighlightService.getTextHighlights(dossierId, fileId).pipe(catchError(() => of({})));
|
||||
}
|
||||
|
||||
loadDataFor(newFile: File): Observable<FileDataModel> {
|
||||
const redactionLog$ = this.loadRedactionLogFor(newFile.dossierId, newFile.fileId);
|
||||
const viewedPages$ = this.getViewedPagesFor(newFile);
|
||||
|
||||
@ -0,0 +1,40 @@
|
||||
import { Injectable, Injector } from '@angular/core';
|
||||
import { GenericService, RequiredParam, Toaster, Validate } from '@iqser/common-ui';
|
||||
import { TextHighlightOperation, TextHighlightRequest, TextHighlightResponse } from '@red/domain';
|
||||
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
|
||||
import { tap } from 'rxjs/operators';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class TextHighlightService extends GenericService<unknown> {
|
||||
constructor(protected readonly _injector: Injector, private readonly _toaster: Toaster) {
|
||||
super(_injector, '');
|
||||
}
|
||||
|
||||
@Validate()
|
||||
getTextHighlights(@RequiredParam() dossierId: string, @RequiredParam() fileId: string) {
|
||||
const request: TextHighlightRequest = {
|
||||
dossierId,
|
||||
fileId,
|
||||
operation: TextHighlightOperation.INFO,
|
||||
};
|
||||
|
||||
return this._post<TextHighlightResponse>(request, 'texthighlights-conversion');
|
||||
}
|
||||
|
||||
@Validate()
|
||||
performHighlightsAction(
|
||||
@RequiredParam() dossierId: string,
|
||||
@RequiredParam() fileId: string,
|
||||
@RequiredParam() colors: string[],
|
||||
@RequiredParam() operation: TextHighlightOperation,
|
||||
) {
|
||||
const request: TextHighlightRequest = { dossierId, fileId, colors, operation };
|
||||
return this._post<TextHighlightResponse>(request, 'texthighlights-conversion').pipe(
|
||||
tap(() => {
|
||||
this._toaster.success(_('highlight-action-dialog.success'), { params: { operation } });
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -6,5 +6,5 @@
|
||||
[class.request]="isRequest"
|
||||
class="icon"
|
||||
>
|
||||
<span>{{ label || dictionary.label.charAt(0) }}</span>
|
||||
<span>{{ label || dictionary?.label?.charAt(0) }}</span>
|
||||
</div>
|
||||
|
||||
@ -60,10 +60,13 @@
|
||||
<div *ngIf="filter.id === 'comment'">
|
||||
<mat-icon svgIcon="red:comment"></mat-icon>
|
||||
</div>
|
||||
|
||||
<redaction-annotation-icon *ngIf="filter.color" [color]="filter.color" [label]="''" type="square"></redaction-annotation-icon>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="filter.icon">
|
||||
<mat-icon [svgIcon]="filter.icon"></mat-icon>
|
||||
</ng-container>
|
||||
|
||||
{{ filter.label | translate }}
|
||||
<ng-container *ngIf="filter.skipTranslation; else translate"> {{ filter.label }}</ng-container>
|
||||
<ng-template #translate>{{ filter.label | translate }} </ng-template>
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
|
||||
import { AnnotationSuperType } from '../models/file/annotation.wrapper';
|
||||
import { AnnotationSuperType } from '@models/file/annotation.wrapper';
|
||||
|
||||
export const annotationTypesTranslations: { [key in AnnotationSuperType]: string } = {
|
||||
'text-highlight': _('annotation-type.text-highlight'),
|
||||
'declined-suggestion': _('annotation-type.declined-suggestion'),
|
||||
hint: _('annotation-type.hint'),
|
||||
'ignored-hint': _('annotation-type.ignored-hint'),
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { AnnotationSuperType } from '../../models/file/annotation.wrapper';
|
||||
|
||||
export const SuperTypeSorter: { [key in AnnotationSuperType]: number } = {
|
||||
'text-highlight': 100,
|
||||
'suggestion-change-legal-basis': 14,
|
||||
'suggestion-force-redaction': 15,
|
||||
'suggestion-force-hint': 16,
|
||||
|
||||
@ -310,7 +310,8 @@
|
||||
"suggestion-recategorize-image": "Suggested recategorize image",
|
||||
"suggestion-remove": "Suggested local removal",
|
||||
"suggestion-remove-dictionary": "Suggested dictionary removal",
|
||||
"suggestion-resize": "Suggested Resize"
|
||||
"suggestion-resize": "Suggested Resize",
|
||||
"text-highlight": "Highlight"
|
||||
},
|
||||
"annotations": "Annotations",
|
||||
"assign-dossier-owner": {
|
||||
@ -393,6 +394,7 @@
|
||||
},
|
||||
"header": "Edit Redaction Reason"
|
||||
},
|
||||
"color": "Color",
|
||||
"comments": {
|
||||
"add-comment": "Enter comment",
|
||||
"comments": "{count} {count, plural, one{comment} other{comments}}",
|
||||
@ -1207,6 +1209,10 @@
|
||||
"exclude-pages": "Exclude pages from redaction",
|
||||
"excluded-from-redaction": "excluded",
|
||||
"fullscreen": "Full Screen (F)",
|
||||
"highlights": {
|
||||
"convert": "Convert highlights",
|
||||
"remove": "Remove highlights"
|
||||
},
|
||||
"last-reviewer": "Last Reviewed by:",
|
||||
"no-data": {
|
||||
"title": "There have been no changes to this page."
|
||||
@ -1254,8 +1260,13 @@
|
||||
"put-back": "Undo",
|
||||
"removed-from-redaction": "Removed from redaction"
|
||||
},
|
||||
"highlights": {
|
||||
"label": "Highlights"
|
||||
},
|
||||
"is-excluded": "Redaction is disabled for this document."
|
||||
},
|
||||
"text-highlights": "Highlights",
|
||||
"text-highlights-tooltip": "Shows all text-highlights and allows removing or importing them as redactions",
|
||||
"toggle-analysis": {
|
||||
"disable": "Disable redaction",
|
||||
"enable": "Enable for redaction",
|
||||
@ -1363,6 +1374,30 @@
|
||||
"text": "Help Mode",
|
||||
"welcome-to-help-mode": "<b> Welcome to Help Mode! <br> Clicking on interactive elements will open info about them in new tab. </b>"
|
||||
},
|
||||
"highlight-action-dialog": {
|
||||
"actions": {
|
||||
"cancel": "Cancel"
|
||||
},
|
||||
"convert": {
|
||||
"confirmation": "All highlights in the document will be converted",
|
||||
"details": "All highlights from the document will be converted to Imported Redactions, using the color set up in the Default Colors section of the app.",
|
||||
"save": "Convert Highlights",
|
||||
"title": "Convert highlights to imported redactions"
|
||||
},
|
||||
"form": {
|
||||
"color": {
|
||||
"label": "Highlight HEX Color"
|
||||
}
|
||||
},
|
||||
"remove": {
|
||||
"confirmation": "All highlights in this HEX Color will be removed from the document",
|
||||
"details": "Removing highlights from the document will delete all the rectangles and leave a white background behind the highlighted text.",
|
||||
"save": "Remove Highlights",
|
||||
"title": "Remove highlights"
|
||||
},
|
||||
"success": "{operation, select, CONVERT{Converting} REMOVE{Removing} other{}} highlights in progress..."
|
||||
},
|
||||
"highlights": "{color} - {length} {length, plural, one{highlight} other{highlights}}",
|
||||
"hint": "Hint",
|
||||
"image-category": {
|
||||
"formula": "Formula",
|
||||
@ -1697,6 +1732,7 @@
|
||||
"placeholder": "Search documents...",
|
||||
"this-dossier": "in this dossier"
|
||||
},
|
||||
"size": "Size",
|
||||
"smtp-auth-config": {
|
||||
"actions": {
|
||||
"cancel": "Cancel",
|
||||
|
||||
@ -2,13 +2,13 @@
|
||||
@use 'common-mixins';
|
||||
|
||||
.NEW {
|
||||
stroke: variables.$grey-5;
|
||||
background-color: variables.$grey-5;
|
||||
stroke: var(--iqser-grey-5);
|
||||
background-color: var(--iqser-grey-5);
|
||||
}
|
||||
|
||||
.UNPROCESSED {
|
||||
stroke: variables.$grey-3;
|
||||
background-color: variables.$grey-3;
|
||||
stroke: var(--iqser-grey-3);
|
||||
background-color: var(--iqser-grey-3);
|
||||
}
|
||||
|
||||
.UNDER_REVIEW,
|
||||
@ -70,8 +70,8 @@
|
||||
}
|
||||
|
||||
.INACTIVE {
|
||||
stroke: variables.$grey-5;
|
||||
background-color: variables.$grey-5;
|
||||
stroke: var(--iqser-grey-5);
|
||||
background-color: var(--iqser-grey-5);
|
||||
}
|
||||
|
||||
.MANAGER,
|
||||
@ -79,3 +79,22 @@
|
||||
stroke: variables.$primary;
|
||||
background-color: variables.$primary;
|
||||
}
|
||||
|
||||
.workload-separator {
|
||||
border-bottom: 1px solid var(--iqser-separator);
|
||||
height: 32px;
|
||||
box-sizing: border-box;
|
||||
padding: 0 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background-color: var(--iqser-grey-6);
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
|
||||
> div:not(:last-child) {
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1 +1 @@
|
||||
Subproject commit dd87dd6821ecc8d591b03191964e77fb5fc6e8f8
|
||||
Subproject commit 17008f44757a29ae3d09581a94520c2a05670432
|
||||
@ -20,3 +20,4 @@ export * from './lib/signature';
|
||||
export * from './lib/legal-basis';
|
||||
export * from './lib/dossier-stats';
|
||||
export * from './lib/dossier-state';
|
||||
export * from './lib/text-highlight';
|
||||
|
||||
@ -40,6 +40,7 @@ export class File extends Entity<IFile> implements IFile {
|
||||
readonly hasSuggestions: boolean;
|
||||
readonly processingStatus: ProcessingFileStatus;
|
||||
readonly workflowStatus: WorkflowFileStatus;
|
||||
readonly fileManipulationDate: string;
|
||||
|
||||
readonly statusSort: number;
|
||||
readonly cacheIdentifier?: string;
|
||||
@ -96,9 +97,10 @@ export class File extends Entity<IFile> implements IFile {
|
||||
this.uploader = file.uploader;
|
||||
this.excludedPages = file.excludedPages || [];
|
||||
this.hasSuggestions = !!file.hasSuggestions;
|
||||
this.fileManipulationDate = file.fileManipulationDate;
|
||||
|
||||
this.statusSort = StatusSorter[this.workflowStatus];
|
||||
this.cacheIdentifier = btoa((this.lastUploaded ?? '') + (this.lastOCRTime ?? ''));
|
||||
this.cacheIdentifier = btoa(this.fileManipulationDate ?? '');
|
||||
this.hintsOnly = this.hasHints && !this.hasRedactions;
|
||||
this.hasNone = !this.hasRedactions && !this.hasHints && !this.hasSuggestions;
|
||||
this.isProcessing = isProcessingStatuses.includes(this.processingStatus);
|
||||
|
||||
@ -147,4 +147,9 @@ export interface IFile {
|
||||
readonly processingStatus: ProcessingFileStatus;
|
||||
|
||||
readonly workflowStatus: WorkflowFileStatus;
|
||||
|
||||
/**
|
||||
* Last time the actual file was touched
|
||||
*/
|
||||
readonly fileManipulationDate: string;
|
||||
}
|
||||
|
||||
@ -1 +1 @@
|
||||
export type ViewMode = 'STANDARD' | 'DELTA' | 'REDACTED';
|
||||
export type ViewMode = 'STANDARD' | 'DELTA' | 'REDACTED' | 'TEXT_HIGHLIGHTS';
|
||||
|
||||
@ -0,0 +1,6 @@
|
||||
import { IRectangle } from '../geometry/rectangle';
|
||||
|
||||
export interface ImportedRedaction {
|
||||
id: string;
|
||||
positions: IRectangle[];
|
||||
}
|
||||
4
libs/red-domain/src/lib/text-highlight/index.ts
Normal file
4
libs/red-domain/src/lib/text-highlight/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export * from './imported-redaction';
|
||||
export * from './text-highlight-operation';
|
||||
export * from './text-highlight.response';
|
||||
export * from './text-highlight.request';
|
||||
@ -0,0 +1,5 @@
|
||||
export enum TextHighlightOperation {
|
||||
REMOVE = 'REMOVE',
|
||||
CONVERT = 'CONVERT',
|
||||
INFO = 'INFO',
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
import { TextHighlightOperation } from './text-highlight-operation';
|
||||
|
||||
export interface TextHighlightRequest {
|
||||
dossierId: string;
|
||||
fileId: string;
|
||||
operation: TextHighlightOperation;
|
||||
colors?: string[];
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
import { ImportedRedaction } from './imported-redaction';
|
||||
import { TextHighlightOperation } from './text-highlight-operation';
|
||||
|
||||
export interface TextHighlightResponse {
|
||||
dossierId?: string;
|
||||
fileId?: string;
|
||||
operation?: TextHighlightOperation;
|
||||
redactionPerColor?: { [key: string]: ImportedRedaction[] };
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user