Merge remote-tracking branch 'origin/master' into RED-3313

This commit is contained in:
Adina Țeudan 2022-03-01 13:28:51 +02:00
commit d4cbf647ad
62 changed files with 775 additions and 206 deletions

View File

@ -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,6 +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();

View File

@ -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,14 +42,31 @@ 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;
} else if (viewMode === 'DELTA') {
return annotation.isChangeLogEntry;
} else {
return annotation.isRedacted;
return annotation.previewAnnotation;
}
});
}

View File

@ -76,7 +76,6 @@ const routes: Routes = [
path: 'rules',
component: BaseDossierTemplateScreenComponent,
canActivate: [CompositeRouteGuard],
canDeactivate: [PendingChangesGuard],
data: {
routeGuards: [AuthGuard, RedRoleGuard],
},

View File

@ -41,7 +41,7 @@ export class AddEditDictionaryDialogComponent extends BaseDialogComponent {
@Inject(MAT_DIALOG_DATA)
private readonly _data: { readonly dictionary: Dictionary; readonly dossierTemplateId: string },
) {
super(_injector, _dialogRef);
super(_injector, _dialogRef, !!_data.dictionary);
this.form = this._getForm(this.dictionary);
this.initialFormValue = this.form.getRawValue();
this.hasColor$ = this._colorEmpty$;

View File

@ -27,7 +27,7 @@ export class AddEditDossierAttributeDialogComponent extends BaseDialogComponent
@Inject(MAT_DIALOG_DATA)
readonly data: { readonly dossierAttribute: IDossierAttributeConfig; dossierTemplateId: string },
) {
super(_injector, _dialogRef);
super(_injector, _dialogRef, !!data.dossierAttribute);
this.form = this._getForm(this.dossierAttribute);
this.initialFormValue = this.form.getRawValue();
}

View File

@ -22,7 +22,7 @@ export class AddEditDossierStateDialogComponent extends BaseDialogComponent {
protected readonly _dialogRef: MatDialogRef<AddEditDossierStateDialogComponent>,
@Inject(MAT_DIALOG_DATA) readonly data: DialogData,
) {
super(_injector, _dialogRef);
super(_injector, _dialogRef, !!data.dossierState);
this.form = this.#getForm();
this.initialFormValue = this.form.getRawValue();
}

View File

@ -41,7 +41,7 @@ export class AddEditDossierTemplateDialogComponent extends BaseDialogComponent {
private readonly _loadingService: LoadingService,
@Inject(MAT_DIALOG_DATA) readonly dossierTemplateId: string,
) {
super(_injector, _dialogRef);
super(_injector, _dialogRef, !!dossierTemplateId);
this.dossierTemplate = this._dossierTemplatesService.find(this.dossierTemplateId);
this.form = this._getForm();
this.initialFormValue = this.form.getRawValue();

View File

@ -34,7 +34,7 @@ export class AddEditFileAttributeDialogComponent extends BaseDialogComponent {
numberOfFilterableAttrs: number;
},
) {
super(_injector, _dialogRef);
super(_injector, _dialogRef, !!data.fileAttribute);
this.canSetDisplayed = data.numberOfDisplayedAttrs < this.DISPLAYED_FILTERABLE_LIMIT || data.fileAttribute?.displayedInFileList;
this.canSetFilterable = data.numberOfFilterableAttrs < this.DISPLAYED_FILTERABLE_LIMIT || data.fileAttribute?.filterable;
this.form = this._getForm(this.fileAttribute);

View File

@ -10,24 +10,15 @@ import { BaseDialogComponent } from '@iqser/common-ui';
styleUrls: ['./add-edit-user-dialog.component.scss'],
})
export class AddEditUserDialogComponent extends BaseDialogComponent {
@ViewChild(UserDetailsComponent) private readonly _userDetailsComponent: UserDetailsComponent;
resettingPassword = false;
@ViewChild(UserDetailsComponent) private readonly _userDetailsComponent: UserDetailsComponent;
constructor(
protected readonly _injector: Injector,
protected readonly _dialogRef: MatDialogRef<AddEditUserDialogComponent>,
@Inject(MAT_DIALOG_DATA) readonly user: User,
) {
super(_injector, _dialogRef);
}
toggleResetPassword() {
this.resettingPassword = !this.resettingPassword;
}
async save(): Promise<void> {
await this._userDetailsComponent.save();
super(_injector, _dialogRef, !!user);
}
get changed(): boolean {
@ -38,6 +29,14 @@ export class AddEditUserDialogComponent extends BaseDialogComponent {
return this._userDetailsComponent.valid;
}
toggleResetPassword() {
this.resettingPassword = !this.resettingPassword;
}
async save(): Promise<void> {
await this._userDetailsComponent.save();
}
closeDialog(event) {
this._dialogRef.close(event);
}

View File

@ -33,7 +33,7 @@ export class EditColorDialogComponent extends BaseDialogComponent {
@Inject(MAT_DIALOG_DATA)
readonly data: IEditColorData,
) {
super(_injector, _dialogRef);
super(_injector, _dialogRef, true);
this._dossierTemplateId = data.dossierTemplateId;
this.form = this._getForm();

View File

@ -28,7 +28,7 @@ export class FileAttributesConfigurationsDialogComponent extends BaseDialogCompo
protected readonly _dialogRef: MatDialogRef<FileAttributesConfigurationsDialogComponent>,
@Inject(MAT_DIALOG_DATA) private _data: { config: IFileAttributesConfig; dossierTemplateId: string },
) {
super(_injector, _dialogRef);
super(_injector, _dialogRef, true);
this.form = this._getForm();
this.initialFormValue = this.form.getRawValue();
}

View File

@ -23,7 +23,7 @@ export class AddEditJustificationDialogComponent extends BaseDialogComponent {
protected readonly _dialogRef: MatDialogRef<AddEditJustificationDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: { justification?: Justification; dossierTemplateId: string },
) {
super(_injector, _dialogRef);
super(_injector, _dialogRef, !!data.justification);
this.form = this._getForm();
this.initialFormValue = this.form.getRawValue();

View File

@ -4,8 +4,9 @@ import { RouterModule } from '@angular/router';
import { SharedModule } from '../../../shared/shared.module';
import { RulesScreenComponent } from './rules-screen/rules-screen.component';
import { MonacoEditorModule } from '@materia-ui/ngx-monaco-editor';
import { PendingChangesGuard } from '../../../../guards/can-deactivate.guard';
const routes = [{ path: '', component: RulesScreenComponent }];
const routes = [{ path: '', component: RulesScreenComponent, canDeactivate: [PendingChangesGuard] }];
@NgModule({
declarations: [RulesScreenComponent],

View File

@ -31,7 +31,7 @@ export class ChangeLegalBasisDialogComponent extends BaseDialogComponent impleme
protected readonly _dialogRef: MatDialogRef<ChangeLegalBasisDialogComponent>,
@Inject(MAT_DIALOG_DATA) private readonly _data: { annotations: AnnotationWrapper[]; dossier: Dossier },
) {
super(_injector, _dialogRef);
super(_injector, _dialogRef, true);
this.form = this._getForm();
}

View File

@ -55,7 +55,7 @@ export class EditDossierDialogComponent extends BaseDialogComponent implements A
section?: Section;
},
) {
super(_injector, _dialogRef);
super(_injector, _dialogRef, true);
this.navItems = [
{
key: 'dossierInfo',

View File

@ -12,6 +12,8 @@ import { SearchScreenComponent } from './screens/search-screen/search-screen.com
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 { HighlightActionDialogComponent } from './screens/file-preview-screen/dialogs/highlight-action-dialog/highlight-action-dialog.component';
import { ColorPickerModule } from 'ngx-color-picker';
const screens = [SearchScreenComponent];
@ -22,12 +24,21 @@ const dialogs = [
ResizeAnnotationDialogComponent,
ChangeLegalBasisDialogComponent,
RecategorizeImageDialogComponent,
HighlightActionDialogComponent,
];
const components = [...screens, ...dialogs];
@NgModule({
declarations: [...components],
imports: [CommonModule, SharedModule, SharedDossiersModule, FileUploadDownloadModule, DossiersRoutingModule, OverlayModule],
imports: [
CommonModule,
SharedModule,
SharedDossiersModule,
FileUploadDownloadModule,
DossiersRoutingModule,
OverlayModule,
ColorPickerModule,
],
})
export class DossiersModule {}

View File

@ -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">

View File

@ -1,46 +1,51 @@
<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>
</div>
<redaction-comments #comments [annotation]="annotation"></redaction-comments>
<ng-container *ngFor="let annotation of annotations; let idx = index">
<div *ngIf="showHighlightGroup(idx) as highlightGroup" class="workload-separator">
<redaction-highlights-separator [annotation]="annotation" [highlightGroup]="highlightGroup"></redaction-highlights-separator>
</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

View File

@ -3,9 +3,11 @@ import { AnnotationWrapper } from '@models/file/annotation.wrapper';
import { 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 { ViewModeService } from '../../services/view-mode.service';
import { BehaviorSubject } from 'rxjs';
import { DossiersDialogService } from '../../../../services/dossiers-dialog.service';
import { TextHighlightsGroup } from '@red/domain';
@Component({
selector: 'redaction-annotations-list',
@ -23,19 +25,25 @@ export class AnnotationsListComponent implements OnChanges {
@Output() readonly selectAnnotations = new EventEmitter<AnnotationWrapper[]>();
@Output() readonly deselectAnnotations = new EventEmitter<AnnotationWrapper[]>();
highlightGroups$ = new BehaviorSubject<TextHighlightsGroup[]>([]);
constructor(
readonly multiSelectService: MultiSelectService,
readonly viewModeService: ViewModeService,
readonly annotationReferencesService: AnnotationReferencesService,
private readonly _filterService: FilterService,
private readonly _state: FilePreviewStateService,
private readonly _userPreferenceService: UserPreferenceService,
private readonly _viewModeService: ViewModeService,
private readonly _dialogService: DossiersDialogService,
) {}
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 +88,28 @@ export class AnnotationsListComponent implements OnChanges {
return first.x < second.y ? -1 : 1;
}
}
showHighlightGroup(idx: number): TextHighlightsGroup {
return this._viewModeService.isTextHighlights && this.highlightGroups$.value.find(h => h.startIdx === idx);
}
private _updateHighlightGroups(): void {
if (!this.annotations?.length) {
return;
}
const highlightGroups: TextHighlightsGroup[] = [];
let lastGroup: TextHighlightsGroup;
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);
}
}

View File

@ -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>
@ -160,7 +163,7 @@
<ng-container *ngIf="activeViewerPage && !displayedAnnotations.get(activeViewerPage)?.length">
<iqser-empty-state
[horizontalPadding]="24"
[text]="(displayedPages.length ? noDataI18NKey : resetFiltersI18NKey) | translate"
[text]="'file-preview.no-data.title' | translate"
[verticalPadding]="40"
icon="iqser:document"
>

View File

@ -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);
}

View File

@ -34,6 +34,7 @@ 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'];
@ -48,8 +49,6 @@ const ALL_HOTKEY_ARRAY = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'];
export class FileWorkloadComponent {
readonly iconButtonTypes = IconButtonTypes;
readonly circleButtonTypes = CircleButtonTypes;
readonly noDataI18NKey = _('file-preview.no-data.title');
readonly resetFiltersI18NKey = _('file-preview.reset-filters');
displayedAnnotations = new Map<number, AnnotationWrapper[]>();
@Input() selectedAnnotations: AnnotationWrapper[];
@ -70,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;
@ -81,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,
@ -89,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()
@ -115,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 => {

View File

@ -0,0 +1,25 @@
<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>
<div *ngIf="isWritable$ | async">
<iqser-circle-button
(action)="convertHighlights(highlightGroup)"
[size]="28"
[tooltip]="'file-preview.highlights.convert' | translate"
[type]="circleButtonTypes.dark"
class="mr-2"
icon="red:convert"
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>

View File

@ -0,0 +1,7 @@
:host {
display: contents;
> div {
display: flex;
}
}

View File

@ -0,0 +1,41 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { CircleButtonTypes } from '@iqser/common-ui';
import { TextHighlightOperation, TextHighlightsGroup } from '@red/domain';
import { DossiersDialogService } from '../../../../services/dossiers-dialog.service';
import { FilePreviewStateService } from '../../services/file-preview-state.service';
import { AnnotationWrapper } from '@models/file/annotation.wrapper';
@Component({
selector: 'redaction-highlights-separator [highlightGroup] [annotation]',
templateUrl: './highlights-separator.component.html',
styleUrls: ['./highlights-separator.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class HighlightsSeparatorComponent {
@Input() highlightGroup: TextHighlightsGroup;
@Input() annotation: AnnotationWrapper;
readonly circleButtonTypes = CircleButtonTypes;
readonly isWritable$ = this._state.isWritable$;
constructor(private readonly _dialogService: DossiersDialogService, private readonly _state: FilePreviewStateService) {}
convertHighlights(highlightGroup: TextHighlightsGroup): void {
const data = this._getActionData(highlightGroup, TextHighlightOperation.CONVERT);
this._dialogService.openDialog('highlightAction', null, data);
}
removeHighlights(highlightGroup: TextHighlightsGroup): void {
const data = this._getActionData(highlightGroup, TextHighlightOperation.REMOVE);
this._dialogService.openDialog('highlightAction', null, data);
}
private _getActionData(highlightGroup: TextHighlightsGroup, operation: TextHighlightOperation) {
return {
dossierId: this._state.dossierId,
fileId: this._state.fileId,
color: highlightGroup.color,
operation,
};
}
}

View File

@ -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();
}
}

View File

@ -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>

View File

@ -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>

View File

@ -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],
});
}
}

View File

@ -12,6 +12,7 @@ import { AnnotationReferencesService } from './services/annotation-references.se
import { FilterService } from '@iqser/common-ui';
import { ManualAnnotationService } from '../../services/manual-annotation.service';
import { AnnotationProcessingService } from '../../services/annotation-processing.service';
import { dossiersServiceProvider } from '../../../../services/entity-services/dossiers.service.provider';
export const filePreviewScreenProviders = [
FilterService,
@ -28,4 +29,5 @@ export const filePreviewScreenProviders = [
AnnotationReferencesService,
ManualAnnotationService,
AnnotationProcessingService,
dossiersServiceProvider,
];

View File

@ -34,7 +34,6 @@ import { clearStamps, stampPDFPage } from '@utils/page-stamper';
import { TranslateService } from '@ngx-translate/core';
import { handleFilterDelta } from '@utils/filter-utils';
import { FilesService } from '@services/entity-services/files.service';
import { ActiveDossiersService } from '@services/entity-services/active-dossiers.service';
import { FileManagementService } from '@services/entity-services/file-management.service';
import { catchError, map, switchMap, tap } from 'rxjs/operators';
import { FilesMapService } from '@services/entity-services/files-map.service';
@ -50,6 +49,7 @@ import { FilePreviewStateService } from './services/file-preview-state.service';
import { FileDataModel } from '../../../../models/file/file-data.model';
import { filePreviewScreenProviders } from './file-preview-providers';
import { ManualAnnotationService } from '../../services/manual-annotation.service';
import { DossiersService } from '../../../../services/entity-services/dossiers.service';
import Annotation = Core.Annotations.Annotation;
import PDFNet = Core.PDFNet;
@ -104,7 +104,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
private readonly _filterService: FilterService,
private readonly _translateService: TranslateService,
private readonly _filesMapService: FilesMapService,
private readonly _activeDossiersService: ActiveDossiersService,
private readonly _dossiersService: DossiersService,
private readonly _reanalysisService: ReanalysisService,
private readonly _errorService: ErrorService,
private readonly _skippedService: SkippedService,
@ -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'));
@ -185,6 +195,20 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
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();
@ -200,7 +224,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
async ngOnAttach(previousRoute: ActivatedRouteSnapshot): Promise<boolean> {
const file = await this.stateService.file;
if (!file.canBeOpened) {
return this._router.navigate([this._activeDossiersService.find(this.dossierId)?.routerLink]);
return this._router.navigate([this._dossiersService.find(this.dossierId)?.routerLink]);
}
this.viewModeService.compareMode = false;
this.viewModeService.switchToStandard();
@ -491,7 +515,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
await clearStamps(pdfDoc, pdfNet, allPages);
if (this.viewModeService.isRedacted) {
const dossier = this._activeDossiersService.find(this.dossierId);
const dossier = this._dossiersService.find(this.dossierId);
if (dossier.watermarkPreviewEnabled) {
await this._stampPreview(pdfDoc, dossier.dossierTemplateId);
}
@ -548,7 +572,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
)
.subscribe();
this.addActiveScreenSubscription = this._activeDossiersService
this.addActiveScreenSubscription = this._dossiersService
.getEntityDeleted$(this.dossierId)
.pipe(tap(() => this._handleDeletedDossier()))
.subscribe();
@ -577,7 +601,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
private async _loadFileData(file: File): Promise<void | boolean> {
if (!file || file.isError) {
return this._router.navigate([this._activeDossiersService.find(this.dossierId).routerLink]);
return this._router.navigate([this._dossiersService.find(this.dossierId).routerLink]);
}
if (file.isUnprocessed) {
@ -705,7 +729,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
}
}
private _getAnnotations(predicate: (value) => unknown) {
private _getAnnotations(predicate: (value) => boolean) {
const annotations = this._instance.Core.annotationManager.getAnnotationsList();
return predicate ? annotations.filter(predicate) : annotations;
}
@ -726,7 +750,9 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni
private _setAnnotationsColor(annotations: Annotation[], customData: string) {
annotations.forEach(annotation => {
annotation['StrokeColor'] = this._annotationDrawService.convertColor(this._instance, annotation.getCustomData(customData));
const color = this._annotationDrawService.convertColor(this._instance, annotation.getCustomData(customData));
annotation['StrokeColor'] = color;
annotation['FillColor'] = color;
});
}
}

View File

@ -23,6 +23,7 @@ import { AnnotationReferencesListComponent } from './components/annotation-refer
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';
const routes: Routes = [
{
@ -33,26 +34,28 @@ const routes: Routes = [
},
];
const components = [
FileWorkloadComponent,
AnnotationDetailsComponent,
AnnotationsListComponent,
PageIndicatorComponent,
PageExclusionComponent,
PdfViewerComponent,
AnnotationActionsComponent,
CommentsComponent,
DocumentInfoComponent,
TypeAnnotationIconComponent,
ViewSwitchComponent,
UserManagementComponent,
AcceptRecommendationDialogComponent,
AnnotationReferencesListComponent,
AnnotationCardComponent,
AnnotationReferencesPageIndicatorComponent,
HighlightsSeparatorComponent,
];
@NgModule({
declarations: [
FilePreviewScreenComponent,
FileWorkloadComponent,
AnnotationDetailsComponent,
AnnotationsListComponent,
PageIndicatorComponent,
PageExclusionComponent,
PdfViewerComponent,
AnnotationActionsComponent,
CommentsComponent,
DocumentInfoComponent,
TypeAnnotationIconComponent,
ViewSwitchComponent,
UserManagementComponent,
AcceptRecommendationDialogComponent,
AnnotationReferencesListComponent,
AnnotationCardComponent,
AnnotationReferencesPageIndicatorComponent,
],
declarations: [FilePreviewScreenComponent, ...components],
imports: [
RouterModule.forChild(routes),
CommonModule,

View File

@ -3,7 +3,6 @@ import { Core, WebViewerInstance } from '@pdftron/webviewer';
import { hexToRgb } from '@utils/functions';
import { AnnotationWrapper } from '@models/file/annotation.wrapper';
import { UserPreferenceService } from '@services/user-preference.service';
import { ActiveDossiersService } from '@services/entity-services/active-dossiers.service';
import { RedactionLogService } from '../../../services/redaction-log.service';
import { environment } from '@environments/environment';
@ -11,6 +10,7 @@ import { IRectangle, ISectionGrid, ISectionRectangle } from '@red/domain';
import { SkippedService } from './skipped.service';
import { firstValueFrom } from 'rxjs';
import { DictionariesMapService } from '@services/entity-services/dictionaries-map.service';
import { DossiersService } from '../../../../../services/entity-services/dossiers.service';
import Annotation = Core.Annotations.Annotation;
@Injectable()
@ -21,7 +21,7 @@ export class AnnotationDrawService {
constructor(
private readonly _dictionariesMapService: DictionariesMapService,
private readonly _activeDossiersService: ActiveDossiersService,
private readonly _dossiersService: DossiersService,
private readonly _redactionLogService: RedactionLogService,
private readonly _userPreferenceService: UserPreferenceService,
private readonly _skippedService: SkippedService,
@ -128,7 +128,7 @@ export class AnnotationDrawService {
}
private _computeSection(activeViewer: WebViewerInstance, pageNumber: number, sectionRectangle: ISectionRectangle, dossierId: string) {
const dossierTemplateId = this._activeDossiersService.find(dossierId).dossierTemplateId;
const dossierTemplateId = this._dossiersService.find(dossierId).dossierTemplateId;
const rectangleAnnot = new activeViewer.Core.Annotations.RectangleAnnotation();
const pageHeight = activeViewer.Core.documentViewer.getPageHeight(pageNumber);
const rectangle: IRectangle = {
@ -156,66 +156,84 @@ export class AnnotationDrawService {
compareMode: boolean,
) {
const pageNumber = compareMode ? annotationWrapper.pageNumber * 2 - 1 : annotationWrapper.pageNumber;
const dossierTemplateId = this._activeDossiersService.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('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.isRedacted));
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[] {

View File

@ -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);
}

View File

@ -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);

View File

@ -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';
import { ConfirmArchiveDossierDialogComponent } from '../dialogs/confirm-archive-dossier-dialog/confirm-archive-dossier-dialog.component';
type DialogType =
@ -25,7 +26,8 @@ type DialogType =
| 'removeAnnotations'
| 'resizeAnnotation'
| 'forceAnnotation'
| 'manualAnnotation';
| 'manualAnnotation'
| 'highlightAction';
@Injectable()
export class DossiersDialogService extends DialogService<DialogType> {
@ -72,6 +74,9 @@ export class DossiersDialogService extends DialogService<DialogType> {
component: ManualAnnotationDialogComponent,
dialogConfig: { autoFocus: true },
},
highlightAction: {
component: HighlightActionDialogComponent,
},
};
constructor(protected readonly _dialog: MatDialog) {

View File

@ -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);

View File

@ -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 } });
}),
);
}
}

View File

@ -23,6 +23,7 @@ export class IconsModule {
'color-picker',
'comment',
'comment-fill',
'convert',
'csv',
'dictionary',
'denied',

View File

@ -6,5 +6,5 @@
[class.request]="isRequest"
class="icon"
>
<span>{{ label || dictionary.label.charAt(0) }}</span>
<span>{{ label || dictionary?.label?.charAt(0) }}</span>
</div>

View File

@ -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>

View File

@ -2,12 +2,23 @@ import { Injectable, Injector } from '@angular/core';
import { StatsService } from '@iqser/common-ui';
import { DossierStats, IDossierStats } from '@red/domain';
import { DOSSIER_ID } from '@utils/constants';
import { Observable, of } from 'rxjs';
import { UserService } from '@services/user.service';
@Injectable({
providedIn: 'root',
})
export class DossierStatsService extends StatsService<DossierStats, IDossierStats> {
constructor(protected readonly _injector: Injector) {
constructor(protected readonly _injector: Injector, private readonly _userService: UserService) {
super(_injector, DOSSIER_ID, DossierStats, 'dossier-stats');
}
getFor(ids: string[]): Observable<DossierStats[]> {
const isUserAdminOnly = this._userService.currentUser.roles.length === 1 && this._userService.currentUser.isUserAdmin;
if (isUserAdminOnly) {
return of([]);
}
return super.getFor(ids);
}
}

View File

@ -4,6 +4,7 @@ import { Dossier, File, IComment, IDossier } from '@red/domain';
import { DossiersService } from '@services/entity-services/dossiers.service';
import { ActivatedRoute } from '@angular/router';
import { dossiersServiceResolver } from '@services/entity-services/dossiers.service.provider';
import { FilesMapService } from '@services/entity-services/files-map.service';
@Injectable({ providedIn: 'root' })
export class PermissionsService {
@ -11,6 +12,7 @@ export class PermissionsService {
private readonly _userService: UserService,
private readonly _route: ActivatedRoute,
private readonly _injector: Injector,
private readonly _filesMapService: FilesMapService,
) {}
private get _dossiersService(): DossiersService {
@ -23,7 +25,7 @@ export class PermissionsService {
}
displayReanalyseBtn(dossier: Dossier): boolean {
return dossier.isActive && this.isApprover(dossier);
return dossier.isActive && this.isApprover(dossier) && this._filesMapService.get(dossier.dossierId).length > 0;
}
canUploadFiles(dossier: Dossier): boolean {

View File

@ -37,6 +37,10 @@ export class UserService extends EntitiesService<User, IUser> {
}
async initialize(): Promise<void> {
if (!this.currentUser) {
return;
}
if (this.currentUser.isUserAdmin || this.currentUser.isUser || this.currentUser.isAdmin) {
await firstValueFrom(this.loadAll());
}

View File

@ -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'),

View File

@ -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,

View File

@ -221,13 +221,25 @@
"it": "",
"fr": ""
},
"dossiers_scroll_up_and_down": {
"dossiers_scroll_up_button": {
"en": "/en/index-en.html?contextId=dossiers_scroll_up_and_down",
"de": "",
"it": "",
"fr": ""
},
"documents_scroll_up_and_down": {
"dossiers_scroll_down_button": {
"en": "/en/index-en.html?contextId=dossiers_scroll_up_and_down",
"de": "",
"it": "",
"fr": ""
},
"documents_scroll_up_button": {
"en": "/en/index-en.html?contextId=documents_scroll_up_and_down",
"de": "",
"it": "",
"fr": ""
},
"documents_scroll_down_button": {
"en": "/en/index-en.html?contextId=documents_scroll_up_and_down",
"de": "",
"it": "",

View File

@ -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",
"archived-dossiers-listing": {
@ -410,6 +411,7 @@
},
"header": "Edit Redaction Reason"
},
"color": "Color",
"comments": {
"add-comment": "Enter comment",
"comments": "{count} {count, plural, one{comment} other{comments}}",
@ -1240,6 +1242,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."
@ -1288,8 +1294,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",
@ -1397,6 +1408,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",
@ -1732,6 +1767,7 @@
"placeholder": "Search documents...",
"this-dossier": "in this dossier"
},
"size": "Size",
"smtp-auth-config": {
"actions": {
"cancel": "Cancel",

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="100px" version="1.1" viewBox="0 0 100 100" width="100px" xmlns="http://www.w3.org/2000/svg">
<g fill="none" fill-rule="evenodd" id="Symbols" stroke="none" stroke-width="1">
<g fill="currentColor" fill-rule="nonzero" id="convert">
<path
d="M83.25,2.5 C91.3253393,2.5 97.5,8.67466071 97.5,16.75 L97.5,16.75 L97.5,83.25 C97.5,91.3253393 91.3253393,97.5 83.25,97.5 L83.25,97.5 L16.75,97.5 C8.67466071,97.5 2.5,91.3253393 2.5,83.25 L2.5,83.25 L2.5,16.75 C2.5,8.67466071 8.67466071,2.5 16.75,2.5 L16.75,2.5 Z M83.25,12 L16.75,12 C13.8998304,12 12,13.8998304 12,16.75 L12,16.75 L12,83.25 C12,86.1001696 13.8998304,88 16.75,88 L16.75,88 L83.25,88 C86.1001696,88 88,86.1001696 88,83.25 L88,83.25 L88,16.75 C88,13.8998304 86.1001696,12 83.25,12 L83.25,12 Z M59.4998304,26.2501696 L78.4998304,45.2501696 L59.4998304,64.2501696 L52.85,57.6003393 L60.4501696,50.0001696 L38.1251696,50.0001696 C34.3255089,50.0001696 31.0001696,53.3255089 31.0001696,57.1251696 C31.0001696,60.9248304 34.3255089,64.2501696 38.1251696,64.2501696 L38.1251696,64.2501696 L45.2501696,64.2501696 L45.2501696,73.7501696 L38.1251696,73.7501696 C29.1003393,73.7501696 21.5001696,66.15 21.5001696,57.1251696 C21.5001696,48.1003393 29.1003393,40.5001696 38.1251696,40.5001696 L38.1251696,40.5001696 L60.4501696,40.5001696 L52.85,32.9 L59.4998304,26.2501696 Z"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -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;
}
}
}

View File

@ -21,3 +21,4 @@ export * from './lib/legal-basis';
export * from './lib/dossier-stats';
export * from './lib/dossier-state';
export * from './lib/trash-dossier';
export * from './lib/text-highlight';

View File

@ -40,6 +40,7 @@ export class File extends Entity<IFile> implements IFile, IRouterPath {
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, IRouterPath {
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);

View File

@ -147,4 +147,9 @@ export interface IFile {
readonly processingStatus: ProcessingFileStatus;
readonly workflowStatus: WorkflowFileStatus;
/**
* Last time the actual file was touched
*/
readonly fileManipulationDate: string;
}

View File

@ -1 +1 @@
export type ViewMode = 'STANDARD' | 'DELTA' | 'REDACTED';
export type ViewMode = 'STANDARD' | 'DELTA' | 'REDACTED' | 'TEXT_HIGHLIGHTS';

View File

@ -0,0 +1,6 @@
import { IRectangle } from '../geometry';
export interface ImportedRedaction {
id: string;
positions: IRectangle[];
}

View File

@ -0,0 +1,5 @@
export * from './imported-redaction';
export * from './text-highlight-operation';
export * from './text-highlight.response';
export * from './text-highlight.request';
export * from './text-highlights-group';

View File

@ -0,0 +1,5 @@
export enum TextHighlightOperation {
REMOVE = 'REMOVE',
CONVERT = 'CONVERT',
INFO = 'INFO',
}

View File

@ -0,0 +1,8 @@
import { TextHighlightOperation } from './text-highlight-operation';
export interface TextHighlightRequest {
dossierId: string;
fileId: string;
operation: TextHighlightOperation;
colors?: string[];
}

View File

@ -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[] };
}

View File

@ -0,0 +1,5 @@
export interface TextHighlightsGroup {
startIdx: number;
color: string;
length: number;
}

View File

@ -1,6 +1,6 @@
{
"name": "redaction",
"version": "3.261.0",
"version": "3.270.0",
"private": true,
"license": "MIT",
"scripts": {

Binary file not shown.