Merge branch 'master' into VM/RED-7340

This commit is contained in:
Valentin Mihai 2024-09-27 17:00:59 +03:00
commit 40d9eb15cf
25 changed files with 350 additions and 357 deletions

View File

@ -2,67 +2,74 @@
<div [translate]="'reports-screen.title'" class="heading-xl"></div>
<div [translate]="'reports-screen.setup'" class="description"></div>
<div *ngIf="!isDocumine" [translate]="'reports-screen.description'" class="description"></div>
<div *ngIf="!isDocumine && placeholders$ | async as placeholders" class="placeholders">
<div [translate]="'reports-screen.table-header.placeholders'" class="all-caps-label"></div>
<div [translate]="'reports-screen.table-header.description'" class="all-caps-label"></div>
<ng-container *ngFor="let placeholder of placeholders">
<div class="placeholder">{{ placeholder.placeholder }}</div>
<div
[innerHTML]="placeholder.descriptionTranslation | translate: { attribute: placeholder.attributeName }"
class="description"
></div>
</ng-container>
</div>
@if (!isDocumine) {
<div [translate]="'reports-screen.description'" class="description"></div>
}
@if (!isDocumine && placeholders$ | async; as placeholders) {
<div class="placeholders">
<div [translate]="'reports-screen.table-header.placeholders'" class="all-caps-label"></div>
<div [translate]="'reports-screen.table-header.description'" class="all-caps-label"></div>
@for (placeholder of placeholders; track placeholder.placeholder) {
<div class="placeholder">{{ placeholder.placeholder }}</div>
<div
[innerHTML]="placeholder.descriptionTranslation | translate: { attribute: placeholder.attributeName }"
class="description"
></div>
}
</div>
}
</div>
<div *ngIf="availableTemplates$ | async as availableTemplates" class="right-container" iqserHasScrollbar>
<div class="header">
<div [translate]="'reports-screen.report-documents'" class="heading"></div>
<iqser-circle-button
(action)="fileInput.click()"
*allow="roles.reportTemplates.upload; if: currentUser.isAdmin"
[tooltip]="'reports-screen.upload-document' | translate"
[attr.help-mode-key]="'upload_report'"
[buttonId]="'upload_report'"
icon="iqser:upload"
></iqser-circle-button>
</div>
<div
(click)="fileInput.click()"
*allow="roles.reportTemplates.upload; if: currentUser.isAdmin && !availableTemplates?.length"
[translate]="'reports-screen.upload-document'"
class="template upload-button"
></div>
<div *ngFor="let template of availableTemplates" class="template">
<div class="name">
{{ template.fileName }} {{ template.multiFileReport ? ('reports-screen.multi-file-report' | translate) : '' }}
</div>
<div class="actions">
<iqser-circle-button
(action)="download(template)"
*allow="roles.reportTemplates.download"
[buttonId]="(template.fileName | snakeCase) + '-download-button'"
[iconSize]="12"
[size]="18"
icon="iqser:download"
></iqser-circle-button>
@if (availableTemplates$ | async; as availableTemplates) {
<div class="right-container" iqserHasScrollbar>
<div class="header">
<div [translate]="'reports-screen.report-documents'" class="heading"></div>
<iqser-circle-button
(action)="deleteTemplate(template)"
*allow="roles.reportTemplates.delete; if: currentUser.isAdmin"
[buttonId]="(template.fileName | snakeCase) + '-delete-button'"
[iconSize]="12"
[size]="18"
icon="iqser:trash"
(action)="fileInput.click()"
*allow="roles.reportTemplates.upload; if: currentUser.isAdmin"
[tooltip]="'reports-screen.upload-document' | translate"
[attr.help-mode-key]="'upload_report'"
[buttonId]="'upload_report'"
icon="iqser:upload"
></iqser-circle-button>
</div>
<div
(click)="fileInput.click()"
*allow="roles.reportTemplates.upload; if: currentUser.isAdmin && !availableTemplates?.length"
[translate]="'reports-screen.upload-document'"
class="template upload-button"
></div>
@for (template of availableTemplates; track template.templateId) {
<div [id]="template.fileName | snakeCase" class="template">
<div class="name">
{{ template.fileName }}
</div>
<div class="actions">
<iqser-circle-button
(action)="download(template)"
*allow="roles.reportTemplates.download"
[buttonId]="(template.fileName | snakeCase) + '-download-button'"
[iconSize]="12"
[size]="18"
icon="iqser:download"
></iqser-circle-button>
<iqser-circle-button
(action)="deleteTemplate(template)"
*allow="roles.reportTemplates.delete; if: currentUser.isAdmin"
[buttonId]="(template.fileName | snakeCase) + '-delete-button'"
[iconSize]="12"
[size]="18"
icon="iqser:trash"
></iqser-circle-button>
</div>
</div>
}
</div>
</div>
}
<input #fileInput (change)="uploadTemplate($event)" hidden type="file" />

View File

@ -1,4 +1,4 @@
import { ChangeDetectionStrategy, Component, ElementRef, OnInit, ViewChild } from '@angular/core';
import { ChangeDetectionStrategy, Component, ElementRef, inject, OnInit, viewChild } from '@angular/core';
import { DOSSIER_TEMPLATE_ID, IPlaceholdersResponse, IReportTemplate, User } from '@red/domain';
import { download } from '@utils/file-download-utils';
import {
@ -23,8 +23,8 @@ import { BehaviorSubject, firstValueFrom } from 'rxjs';
import { Roles } from '@users/roles';
import { getCurrentUser } from '@iqser/common-ui/lib/users';
import { getParam } from '@iqser/common-ui/lib/utils';
import { AsyncPipe, NgForOf, NgIf } from '@angular/common';
import { TranslateModule } from '@ngx-translate/core';
import { AsyncPipe } from '@angular/common';
import { TranslateModule, TranslateService } from '@ngx-translate/core';
import { SnakeCasePipe } from '@common-ui/pipes/snake-case.pipe';
interface Placeholder {
@ -42,15 +42,16 @@ const placeholderTypes: PlaceholderType[] = ['generalPlaceholders', 'fileAttribu
styleUrls: ['./reports-screen.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [HasScrollbarDirective, NgIf, NgForOf, AsyncPipe, TranslateModule, CircleButtonComponent, IqserAllowDirective, SnakeCasePipe],
imports: [HasScrollbarDirective, AsyncPipe, TranslateModule, CircleButtonComponent, IqserAllowDirective, SnakeCasePipe],
})
export default class ReportsScreenComponent implements OnInit {
@ViewChild('fileInput') private readonly _fileInput: ElementRef;
readonly placeholders$ = new BehaviorSubject<Placeholder[]>([]);
readonly availableTemplates$ = new BehaviorSubject<IReportTemplate[]>([]);
readonly currentUser = getCurrentUser<User>();
readonly roles = Roles;
readonly isDocumine = getConfig().IS_DOCUMINE;
readonly #translateService = inject(TranslateService);
private readonly _fileInput = viewChild<ElementRef>('fileInput');
readonly #dossierTemplateId = getParam(DOSSIER_TEMPLATE_ID);
constructor(
@ -63,8 +64,8 @@ export default class ReportsScreenComponent implements OnInit {
async ngOnInit() {
this._loadingService.start();
await this._loadReportTemplates();
await this._loadPlaceholders();
await this.#loadReportTemplates();
await this.#loadPlaceholders();
this._loadingService.stop();
}
@ -84,14 +85,14 @@ export default class ReportsScreenComponent implements OnInit {
deleteTemplate(template: IReportTemplate) {
this._dialogService.openDialog('confirm', null, () => {
this._loadingService.loadWhile(this._deleteTemplate(template));
this._loadingService.loadWhile(this.#deleteTemplate(template));
});
}
uploadTemplate($event) {
const file: File = $event.target.files[0];
if (!this._isValidFile(file)) {
if (!this.#isValidFile(file)) {
this._toaster.error(_('reports-screen.invalid-upload'));
return;
}
@ -115,27 +116,31 @@ export default class ReportsScreenComponent implements OnInit {
template => template.fileName === file.name && template.multiFileReport === multiFileReport,
)
) {
await this._openOverwriteConfirmationDialog(file, multiFileReport);
await this.#openOverwriteConfirmationDialog(file, multiFileReport);
} else {
await this._uploadTemplateForm(file, multiFileReport);
await this.#uploadTemplateForm(file, multiFileReport);
}
}
});
this._fileInput.nativeElement.value = null;
this._fileInput().nativeElement.value = null;
}
private _getAttributeName(placeholder: string): string {
#getAttributeName(placeholder: string): string {
return removeBraces(placeholder).split('.').pop();
}
private _getPlaceholderDescriptionTranslation(type: PlaceholderType, placeholder: string): string {
#getPlaceholderDescriptionTranslation(type: PlaceholderType, placeholder: string): string {
return type === 'generalPlaceholders'
? generalPlaceholdersDescriptionsTranslations[removeBraces(placeholder)]
: placeholdersDescriptionsTranslations[type];
}
private async _openOverwriteConfirmationDialog(file: File, multiFileReport: boolean): Promise<void> {
#getTemplateFilename(template: IReportTemplate): string {
return `${template.fileName} ${template.multiFileReport ? this.#translateService.instant(_('reports-screen.multi-file-report')) : ''}`.trim();
}
async #openOverwriteConfirmationDialog(file: File, multiFileReport: boolean): Promise<void> {
const data: IConfirmationDialogData = {
title: _('confirmation-dialog.report-template-same-name.title'),
question: _('confirmation-dialog.report-template-same-name.question'),
@ -148,29 +153,34 @@ export default class ReportsScreenComponent implements OnInit {
this._dialogService.openDialog('confirm', data, null, async result => {
if (result) {
await this._uploadTemplateForm(file, multiFileReport);
await this.#uploadTemplateForm(file, multiFileReport);
}
});
}
private async _uploadTemplateForm(file: File, multiFileReport: boolean): Promise<void> {
async #uploadTemplateForm(file: File, multiFileReport: boolean): Promise<void> {
this._loadingService.start();
await firstValueFrom(this._reportTemplateService.uploadTemplateForm(this.#dossierTemplateId, multiFileReport, file));
await this._loadReportTemplates();
await this.#loadReportTemplates();
this._loadingService.stop();
}
private async _deleteTemplate(template: IReportTemplate) {
async #deleteTemplate(template: IReportTemplate) {
await firstValueFrom(this._reportTemplateService.delete(template.dossierTemplateId, template.templateId));
await this._loadReportTemplates();
await this.#loadReportTemplates();
}
private async _loadReportTemplates() {
async #loadReportTemplates() {
const reportTemplates = await this._reportTemplateService.getAvailableReportTemplates(this.#dossierTemplateId);
this.availableTemplates$.next(reportTemplates);
this.availableTemplates$.next(
reportTemplates.map(template => ({
...template,
fileName: this.#getTemplateFilename(template),
})),
);
}
private async _loadPlaceholders() {
async #loadPlaceholders() {
const placeholdersResponse: IPlaceholdersResponse = await firstValueFrom(
this._reportTemplateService.getAvailablePlaceholders(this.#dossierTemplateId),
);
@ -178,25 +188,25 @@ export default class ReportsScreenComponent implements OnInit {
placeholderTypes.flatMap(type =>
placeholdersResponse[type].map(placeholder => ({
placeholder,
descriptionTranslation: this._getPlaceholderDescriptionTranslation(type, placeholder),
attributeName: this._getAttributeName(placeholder),
descriptionTranslation: this.#getPlaceholderDescriptionTranslation(type, placeholder),
attributeName: this.#getAttributeName(placeholder),
})),
),
);
}
private _isValidFile(file: File): boolean {
return this._isExcelFile(file) || this._isWordFile(file);
#isValidFile(file: File): boolean {
return this.#isExcelFile(file) || this.#isWordFile(file);
}
private _isExcelFile(file: File): boolean {
#isExcelFile(file: File): boolean {
return (
file.type?.toLowerCase() === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' ||
file.name.toLowerCase().endsWith('.xlsx')
);
}
private _isWordFile(file: File): boolean {
#isWordFile(file: File): boolean {
return (
file.type?.toLowerCase() === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' ||
file.name.toLowerCase().endsWith('.docx')

View File

@ -8,7 +8,7 @@ import {
IconButtonTypes,
IqserDialogComponent,
} from '@iqser/common-ui';
import { Dictionary, Dossier, SuperTypes } from '@red/domain';
import { Dictionary, Dossier } from '@red/domain';
import { ActiveDossiersService } from '@services/dossiers/active-dossiers.service';
import { DictionaryService } from '@services/entity-services/dictionary.service';
import { Roles } from '@users/roles';
@ -47,12 +47,12 @@ export class EditAnnotationDialogComponent
extends IqserDialogComponent<EditAnnotationDialogComponent, EditRedactionData, EditRedactResult>
implements OnInit
{
readonly #dossier: Dossier;
readonly roles = Roles;
readonly iconButtonTypes = IconButtonTypes;
readonly redactedTexts: string[];
dictionaries: Dictionary[] = [];
form: UntypedFormGroup;
readonly #dossier: Dossier;
constructor(
private readonly _activeDossiersService: ActiveDossiersService,
@ -60,7 +60,7 @@ export class EditAnnotationDialogComponent
private readonly _formBuilder: FormBuilder,
) {
super();
this.#dossier = _activeDossiersService.find(this.data.dossierId);
this.#dossier = this._activeDossiersService.find(this.data.dossierId);
const annotations = this.data.annotations;
this.redactedTexts = annotations.map(annotation => annotation.value);
this.form = this.#getForm();
@ -83,10 +83,6 @@ export class EditAnnotationDialogComponent
this.#setTypes();
}
reasonChanged() {
this.form.patchValue({ reason: this.dictionaries.find(d => d.type === SuperTypes.ManualRedaction) });
}
save(): void {
const value = this.form.value;
this.dialogRef.close({
@ -106,8 +102,4 @@ export class EditAnnotationDialogComponent
type: [sameType ? this.data.annotations[0].type : null],
});
}
#allRectangles() {
return this.data.annotations.reduce((acc, a) => acc && a.AREA, true);
}
}

View File

@ -6,8 +6,8 @@
class="dialog-header heading-l"
></div>
<div class="dialog-content redaction" [class.fixed-height]="isRedacted && isImage">
<div class="iqser-input-group" *ngIf="!isImage && redactedTexts.length && !allRectangles">
<div [class.fixed-height]="isRedacted && isImage" class="dialog-content redaction">
<div *ngIf="!isImage && redactedTexts.length && !allRectangles" class="iqser-input-group">
<redaction-selected-annotations-table
[columns]="tableColumns"
[data]="tableData"
@ -15,7 +15,13 @@
></redaction-selected-annotations-table>
</div>
<div *ngIf="!isManualRedaction" class="iqser-input-group w-450" [class.required]="!form.controls.type.disabled">
<iqser-details-radio
*ngIf="!isImage && annotations.length === 1"
[options]="options"
formControlName="option"
></iqser-details-radio>
<div *ngIf="!isManualRedaction" [class.required]="!form.controls.type.disabled" class="iqser-input-group w-450">
<label [translate]="'edit-redaction.dialog.content.type'"></label>
<mat-form-field>

View File

@ -5,7 +5,6 @@ import { MatDialogClose } from '@angular/material/dialog';
import { MatFormField } from '@angular/material/form-field';
import { MatOption, MatSelect, MatSelectTrigger } from '@angular/material/select';
import { MatTooltip } from '@angular/material/tooltip';
import { DetailsRadioOption } from '@common-ui/inputs/details-radio/details-radio-option';
import {
CircleButtonComponent,
HasScrollbarDirective,
@ -26,10 +25,11 @@ import {
SelectedAnnotationsTableComponent,
ValueColumn,
} from '../../components/selected-annotations-table/selected-annotations-table.component';
import { DialogHelpModeKeys } from '../../utils/constants';
import { getEditRedactionOptions } from '../../utils/dialog-options';
import { EditRedactionData, EditRedactResult, RedactOrHintOption } from '../../utils/dialog-types';
import { EditRedactionData, EditRedactionOption, EditRedactResult } from '../../utils/dialog-types';
import { LegalBasisOption } from '../manual-redaction-dialog/manual-annotation-dialog.component';
import { DetailsRadioComponent } from '@common-ui/inputs/details-radio/details-radio.component';
import { DetailsRadioOption } from '@common-ui/inputs/details-radio/details-radio-option';
interface TypeSelectOptions {
type: string;
@ -58,18 +58,16 @@ interface TypeSelectOptions {
HelpButtonComponent,
MatDialogClose,
HasScrollbarDirective,
DetailsRadioComponent,
],
})
export class EditRedactionDialogComponent
extends IqserDialogComponent<EditRedactionDialogComponent, EditRedactionData, EditRedactResult>
implements OnInit
{
readonly #dossier = inject(ActiveDossiersService).find(this.data.dossierId);
readonly #applyToAllDossiers = this.data.applyToAllDossiers;
protected readonly roles = Roles;
readonly ignoredKeys = ['option', 'comment'];
readonly annotations = this.data.annotations;
readonly iconButtonTypes = IconButtonTypes;
readonly isModifyDictionary = this.annotations.every(annotation => annotation.isModifyDictionary);
readonly isImage = this.annotations.reduce((acc, next) => acc && next.isImage, true);
readonly redactedTexts = !this.isImage ? this.annotations.map(annotation => annotation.value).filter(value => !!value) : null;
readonly isManualRedaction = this.annotations.some(annotation => annotation.type === SuperTypes.ManualRedaction);
@ -82,13 +80,15 @@ export class EditRedactionDialogComponent
{ label: redaction.value, bold: true },
{ label: redaction.typeLabel },
]);
options: DetailsRadioOption<RedactOrHintOption>[] | undefined;
options = getEditRedactionOptions();
legalOptions: LegalBasisOption[] = [];
dictionaries: Dictionary[] = [];
typeSelectOptions: TypeSelectOptions[] = [];
readonly form = this.#getForm();
hasTypeChanged = false;
initialReasonDisabled = this.someSkipped;
protected readonly roles = Roles;
readonly #dossier = inject(ActiveDossiersService).find(this.data.dossierId);
constructor(
private readonly _justificationsService: JustificationsService,
@ -144,16 +144,6 @@ export class EditRedactionDialogComponent
return this.annotations.length > 1;
}
get helpButtonKey() {
if (this.isHint) {
return DialogHelpModeKeys.HINT_EDIT;
}
if (this.someSkipped) {
return DialogHelpModeKeys.SKIPPED_EDIT;
}
return DialogHelpModeKeys.REDACTION_EDIT;
}
get sameType() {
return this.annotations.every(annotation => annotation.type === this.annotations[0].type);
}
@ -179,7 +169,6 @@ export class EditRedactionDialogComponent
typeChanged() {
const selectedDictionaryType = this.form.controls.type.value;
this.#setOptions(selectedDictionaryType);
const initialReason = this.form.get('type').value === this.initialFormValue.type && !this.initialReasonDisabled;
if (this.redactBasedTypes.includes(selectedDictionaryType) || initialReason) {
@ -193,7 +182,7 @@ export class EditRedactionDialogComponent
} else {
this.form.controls.reason.disable();
}
this.form.patchValue({ reason: null, option: null });
this.form.patchValue({ reason: null });
}
save() {
@ -206,6 +195,7 @@ export class EditRedactionDialogComponent
comment: value.comment,
type: value.type,
value: this.allRectangles ? value.value : null,
option: value.option.value,
});
}
@ -230,22 +220,6 @@ export class EditRedactionDialogComponent
}
}
#setOptions(type: string, reasonChanged = false) {
const selectedDictionary = this.dictionaries.find(d => d.type === type);
this.options = getEditRedactionOptions(
this.#dossier.dossierName,
this.#applyToAllDossiers,
!!selectedDictionary?.dossierDictionaryOnly,
this.isModifyDictionary,
);
this.form.patchValue(
{
option: !this.isModifyDictionary || reasonChanged ? this.options[0] : this.options[1],
},
{ emitEvent: false },
);
}
#getForm() {
const sameSection = this.annotations.every(annotation => annotation.section === this.annotations[0].section);
return new FormGroup({
@ -256,7 +230,7 @@ export class EditRedactionDialogComponent
disabled: this.isImported,
}),
section: new FormControl<string>({ value: sameSection ? this.annotations[0].section : null, disabled: this.isImported }),
option: new FormControl<LegalBasisOption>(null),
option: new FormControl<DetailsRadioOption<EditRedactionOption>>(this.options[0]),
value: new FormControl<string>(this.allRectangles ? this.annotations[0].value : null),
});
}

View File

@ -24,7 +24,6 @@ import {
SelectedAnnotationsTableComponent,
ValueColumn,
} from '../../components/selected-annotations-table/selected-annotations-table.component';
import { DialogHelpModeKeys } from '../../utils/constants';
import { getRemoveRedactionOptions } from '../../utils/dialog-options';
import {
RemoveRedactionData,

View File

@ -9,6 +9,7 @@ import {
DictionaryEntryTypes,
EarmarkOperation,
type IBulkLocalRemoveRequest,
IBulkRecategorizationRequest,
ILegalBasisChangeRequest,
IRecategorizationRequest,
IRectangle,
@ -34,6 +35,7 @@ import {
EditRedactionData,
EditRedactResult,
ForceAnnotationOptions,
RedactOrHintOptions,
RemoveRedactionData,
RemoveRedactionOptions,
RemoveRedactionPermissions,
@ -108,13 +110,11 @@ export class AnnotationActionsService {
}
async editRedaction(annotations: AnnotationWrapper[]) {
const { dossierId, dossierTemplateId, fileId, file } = this._state;
const { dossierId, fileId } = this._state;
const includeUnprocessed = annotations.every(annotation => this.#includeUnprocessed(annotation, true));
const dossierTemplate = this._dossierTemplatesService.find(dossierTemplateId);
const data = {
annotations,
dossierId,
applyToAllDossiers: dossierTemplate.applyDictionaryUpdatesToAllDossiersByDefault,
};
const result = await this.#getEditRedactionDialog(data).result();
@ -122,22 +122,38 @@ export class AnnotationActionsService {
return;
}
const recategorizeBody: List<IRecategorizationRequest> = annotations.map(annotation => {
const body: IRecategorizationRequest = {
annotationId: annotation.id,
type: result.type ?? annotation.type,
comment: result.comment,
};
if (!this.#isDocumine) {
return {
...body,
legalBasis: result.legalBasis,
section: result.section ?? annotation.section,
value: result.value ?? annotation.value,
let recategorizeBody: List<IRecategorizationRequest> | IBulkRecategorizationRequest;
if (result.option === RedactOrHintOptions.ONLY_HERE) {
recategorizeBody = annotations.map(annotation => {
const body: IRecategorizationRequest = {
annotationId: annotation.id,
type: result.type ?? annotation.type,
comment: result.comment,
};
}
return body;
});
if (!this.#isDocumine) {
return {
...body,
legalBasis: result.legalBasis,
section: result.section ?? annotation.section,
value: result.value ?? annotation.value,
};
}
return body;
});
} else {
const originTypes = annotations.map(a => a.type);
const originLegalBases = annotations.map(a => a.legalBasis);
recategorizeBody = {
value: annotations[0].value,
type: result.type,
legalBasis: result.legalBasis,
section: result.section,
originTypes,
originLegalBases,
rectangle: false,
};
}
await this.#processObsAndEmit(
this._manualRedactionService
@ -147,6 +163,7 @@ export class AnnotationActionsService {
fileId,
this.#getChangedFields(annotations, result),
includeUnprocessed,
result.option === RedactOrHintOptions.IN_DOCUMENT,
)
.pipe(log()),
);

View File

@ -9,6 +9,7 @@ import type {
DictionaryActions,
IAddRedactionRequest,
IBulkLocalRemoveRequest,
IBulkRecategorizationRequest,
ILegalBasisChangeRequest,
IManualAddResponse,
IRecategorizationRequest,
@ -71,15 +72,16 @@ export class ManualRedactionService extends GenericService<IManualAddResponse> {
}
recategorizeRedactions(
body: List<IRecategorizationRequest>,
body: List<IRecategorizationRequest> | IBulkRecategorizationRequest,
dossierId: string,
fileId: string,
successMessageParameters?: {
[key: string]: string;
},
includeUnprocessed = false,
bulkLocal = false,
) {
return this.recategorize(body, dossierId, fileId, includeUnprocessed).pipe(
return this.#recategorize(body, dossierId, fileId, includeUnprocessed, bulkLocal).pipe(
this.#showToast('recategorize-annotation', false, successMessageParameters),
);
}
@ -118,7 +120,7 @@ export class ManualRedactionService extends GenericService<IManualAddResponse> {
includeUnprocessed = false,
bulkLocal = false,
) {
return this.remove(body, dossierId, fileId, includeUnprocessed, bulkLocal).pipe(
return this.#remove(body, dossierId, fileId, includeUnprocessed, bulkLocal).pipe(
this.#showToast(!isHint ? 'remove' : 'remove-hint', removeFromDictionary),
);
}
@ -141,30 +143,11 @@ export class ManualRedactionService extends GenericService<IManualAddResponse> {
return this._post(bulkLocal ? body[0] : body, `${bulkPath}/add/${dossierId}/${fileId}`).pipe(this.#log('Add', body));
}
recategorize(body: List<IRecategorizationRequest>, dossierId: string, fileId: string, includeUnprocessed = false) {
return this._post(body, `${this.#bulkRedaction}/recategorize/${dossierId}/${fileId}?includeUnprocessed=${includeUnprocessed}`).pipe(
this.#log('Recategorize', body),
);
}
undo(annotationIds: List, dossierId: string, fileId: string) {
const url = `${this._defaultModelPath}/bulk/undo/${dossierId}/${fileId}`;
return super.delete(annotationIds, url).pipe(this.#log('Undo', annotationIds));
}
remove(
body: List<IRemoveRedactionRequest> | IBulkLocalRemoveRequest,
dossierId: string,
fileId: string,
includeUnprocessed = false,
bulkLocal = false,
) {
const bulkPath = bulkLocal ? this.#bulkLocal : this.#bulkRedaction;
return this._post(body, `${bulkPath}/remove/${dossierId}/${fileId}?includeUnprocessed=${includeUnprocessed}`).pipe(
this.#log('Remove', body),
);
}
forceRedaction(body: List<ILegalBasisChangeRequest>, dossierId: string, fileId: string) {
return this._post(body, `${this.#bulkRedaction}/force/${dossierId}/${fileId}`).pipe(this.#log('Force redaction', body));
}
@ -175,6 +158,32 @@ export class ManualRedactionService extends GenericService<IManualAddResponse> {
);
}
#recategorize(
body: List<IRecategorizationRequest> | IBulkRecategorizationRequest,
dossierId: string,
fileId: string,
includeUnprocessed = false,
bulkLocal = false,
) {
const bulkPath = bulkLocal ? this.#bulkLocal : this.#bulkRedaction;
return this._post(body, `${bulkPath}/recategorize/${dossierId}/${fileId}?includeUnprocessed=${includeUnprocessed}`).pipe(
this.#log('Recategorize', body),
);
}
#remove(
body: List<IRemoveRedactionRequest> | IBulkLocalRemoveRequest,
dossierId: string,
fileId: string,
includeUnprocessed = false,
bulkLocal = false,
) {
const bulkPath = bulkLocal ? this.#bulkLocal : this.#bulkRedaction;
return this._post(body, `${bulkPath}/remove/${dossierId}/${fileId}?includeUnprocessed=${includeUnprocessed}`).pipe(
this.#log('Remove', body),
);
}
#log(action: string, body: unknown) {
return tap(response => {
this._logger.info(`[MANUAL-REDACTIONS] ${action} `, body, response);

View File

@ -19,16 +19,6 @@ export const ActionsHelpModeKeys = {
'hint-image': 'hint',
} as const;
export const DialogHelpModeKeys = {
REDACTION_EDIT: 'redaction_edit',
REDACTION_REMOVE: 'redaction_remove',
SKIPPED_EDIT: 'skipped_edit',
SKIPPED_REMOVE: 'skipped_remove',
RECOMMENDATION_REMOVE: 'recommendation_remove',
HINT_EDIT: 'hint_edit',
HINT_REMOVE: 'hint_remove',
} as const;
export const ALL_HOTKEYS: List = ['Escape', 'F', 'f', 'ArrowUp', 'ArrowDown', 'H', 'h'] as const;
export const HeaderElements = {

View File

@ -7,6 +7,7 @@ import { removeAnnotationTranslations } from '@translations/remove-annotation-tr
import { removeRedactionTranslations } from '@translations/remove-redaction-translations';
import { resizeRedactionTranslations } from '@translations/resize-redaction-translations';
import {
EditRedactionOption,
ForceAnnotationOption,
RectangleRedactOption,
RectangleRedactOptions,
@ -25,36 +26,21 @@ const DOCUMENT_ICON = 'iqser:document';
const FOLDER_ICON = 'red:folder';
const REMOVE_FROM_DICT_ICON = 'red:remove-from-dict';
export const getEditRedactionOptions = (
dossierName: string,
applyToAllDossiers: boolean,
dossierDictionaryOnly: boolean,
isModifyDictionary: boolean,
): DetailsRadioOption<RedactOrHintOption>[] => {
const options: DetailsRadioOption<RedactOrHintOption>[] = [
export const getEditRedactionOptions = (): DetailsRadioOption<EditRedactionOption>[] => {
return [
{
label: editRedactionTranslations.onlyHere.label,
description: editRedactionTranslations.onlyHere.description,
icon: PIN_ICON,
value: ResizeOptions.ONLY_HERE,
value: RedactOrHintOptions.ONLY_HERE,
},
{
label: editRedactionTranslations.inDocument.label,
description: editRedactionTranslations.inDocument.description,
icon: DOCUMENT_ICON,
value: RedactOrHintOptions.IN_DOCUMENT,
},
];
if (isModifyDictionary) {
options.push({
label: editRedactionTranslations.inDossier.label,
description: editRedactionTranslations.inDossier.description,
descriptionParams: { dossierName: dossierName },
icon: FOLDER_ICON,
value: ResizeOptions.IN_DOSSIER,
additionalCheck: {
label: editRedactionTranslations.inDossier.extraOptionLabel,
checked: applyToAllDossiers,
hidden: dossierDictionaryOnly,
disabled: true,
},
});
}
return options;
};
export const getRedactOrHintOptions = (
@ -74,7 +60,7 @@ export const getRedactOrHintOptions = (
label: translations.onlyHere.label,
description: translations.onlyHere.description,
icon: PIN_ICON,
value: ResizeOptions.ONLY_HERE,
value: RedactOrHintOptions.ONLY_HERE,
});
}
@ -87,7 +73,7 @@ export const getRedactOrHintOptions = (
label: redactTextTranslations.inDocument.label,
description: redactTextTranslations.inDocument.description,
icon: DOCUMENT_ICON,
value: ResizeOptions.IN_DOCUMENT,
value: RedactOrHintOptions.IN_DOCUMENT,
});
}
@ -96,7 +82,7 @@ export const getRedactOrHintOptions = (
description: translations.inDossier.description,
descriptionParams: { dossierName: dossier.dossierName },
icon: FOLDER_ICON,
value: ResizeOptions.IN_DOSSIER,
value: RedactOrHintOptions.IN_DOSSIER,
disabled: isPageExcluded,
additionalCheck: {
label: translations.inDossier.extraOptionLabel,
@ -145,7 +131,7 @@ export const getResizeRedactionOptions = (
label: translations.onlyHere.label,
description: translations.onlyHere.description,
icon: PIN_ICON,
value: RedactOrHintOptions.ONLY_HERE,
value: ResizeOptions.ONLY_HERE,
},
];
@ -161,8 +147,8 @@ export const getResizeRedactionOptions = (
disabled: !dictBasedType || redaction.hasBeenRecategorized,
tooltip: !dictBasedType ? translations.inDossier.tooltip : null,
icon: FOLDER_ICON,
value: RedactOrHintOptions.IN_DOSSIER,
additionalCheck: {
value: ResizeOptions.IN_DOSSIER,
extraOption: {
label: translations.inDossier.extraOptionLabel,
checked: applyToAllDossiers,
hidden: !isApprover,

View File

@ -3,6 +3,13 @@ import { AnnotationWrapper } from '@models/file/annotation.wrapper';
import { ManualRedactionEntryWrapper } from '@models/file/manual-redaction-entry.wrapper';
import { Dictionary, Dossier, File, IAddRedactionRequest, IManualRedactionEntry } from '@red/domain';
export const EditRedactionOptions = {
ONLY_HERE: 'ONLY_HERE',
IN_DOCUMENT: 'IN_DOCUMENT',
} as const;
export type EditRedactionOption = keyof typeof EditRedactionOptions;
export const RedactOrHintOptions = {
ONLY_HERE: 'ONLY_HERE',
IN_DOCUMENT: 'IN_DOCUMENT',
@ -52,7 +59,6 @@ export interface RedactTextData {
export interface EditRedactionData {
annotations: AnnotationWrapper[];
dossierId: string;
applyToAllDossiers: boolean;
isApprover?: boolean;
}
@ -65,7 +71,9 @@ export interface RedactTextResult {
bulkLocal?: boolean;
}
export type RedactRecommendationData = EditRedactionData;
export type RedactRecommendationData = EditRedactionData & {
applyToAllDossiers: boolean;
};
export interface RedactRecommendationResult {
redaction: IAddRedactionRequest;
@ -79,6 +87,7 @@ export interface EditRedactResult {
comment: string;
type: string;
value: string;
option: EditRedactionOption;
}
export type AddHintResult = RedactTextResult;

View File

@ -1,7 +1,7 @@
<div *ngIf="dictionaries && selectedDictionary" class="dictionary-content">
<div class="dictionaries">
<div
(click)="selectDictionary(dictionary)"
(click)="selectDictionary(dictionary, undefined, true)"
*ngFor="let dictionary of dictionaries"
[class.active]="dictionary.label === selectedDictionary.label"
class="dictionary"
@ -33,44 +33,16 @@
</div>
</div>
<div class="entries">
<div class="header-wrapper">
<div class="header-left">
<div class="heading">
<div class="flex-align-items-center">
{{ selectedDictionary?.label }}
<iqser-circle-button
(action)="openEditDictionaryModal()"
*ngIf="selectedDictionary.dossierDictionaryOnly && selectedDictionary.hasDictionary"
[size]="20"
[tooltip]="'edit-dossier-dialog.dictionary.edit-button-tooltip' | translate"
class="p-left-8"
icon="iqser:edit"
></iqser-circle-button>
</div>
<div class="small-label stats-subtitle">
<div>
<mat-icon svgIcon="red:entries"></mat-icon>
<ng-container *ngIf="activeEntryType === entryTypes.ENTRY || selectedDictionary.hint">
{{
'edit-dossier-dialog.dictionary.entries'
| translate: { length: entriesToDisplay.length, hint: selectedDictionary.hint }
}}
</ng-container>
<ng-container *ngIf="activeEntryType === entryTypes.FALSE_POSITIVE && !selectedDictionary.hint">
{{
'edit-dossier-dialog.dictionary.false-positive-entries' | translate: { length: entriesToDisplay.length }
}}
</ng-container>
<ng-container *ngIf="activeEntryType === entryTypes.FALSE_RECOMMENDATION && !selectedDictionary.hint">
{{
'edit-dossier-dialog.dictionary.false-recommendation-entries'
| translate: { length: entriesToDisplay.length }
}}
</ng-container>
</div>
</div>
</div>
</div>
<div class="heading flex-align-items-center">
{{ selectedDictionary?.label }}
<iqser-circle-button
(action)="openEditDictionaryModal()"
*ngIf="selectedDictionary.dossierDictionaryOnly && selectedDictionary.hasDictionary"
[size]="20"
[tooltip]="'edit-dossier-dialog.dictionary.edit-button-tooltip' | translate"
class="p-left-8"
icon="iqser:edit"
></iqser-circle-button>
</div>
<redaction-dictionary-manager
@ -85,14 +57,14 @@
[withFloatingActions]="false"
>
<ng-container slot="typeSwitch">
<div *ngIf="!selectedDictionary.hint" [class.read-only]="!canEdit" class="header-right flex">
<div class="flex">
<iqser-icon-button
(click)="selectEntryType(entryTypes.ENTRY)"
(click)="selectEntryType(entryTypes.ENTRY, true)"
[active]="activeEntryType === entryTypes.ENTRY"
[label]="'edit-dossier-dialog.dictionary.to-redact' | translate: { count: selectedDictionary.entries.length }"
></iqser-icon-button>
<iqser-icon-button
(click)="selectEntryType(entryTypes.FALSE_POSITIVE)"
(click)="selectEntryType(entryTypes.FALSE_POSITIVE, true)"
[active]="activeEntryType === entryTypes.FALSE_POSITIVE"
[label]="
'edit-dossier-dialog.dictionary.false-positives'
@ -100,7 +72,7 @@
"
></iqser-icon-button>
<iqser-icon-button
(click)="selectEntryType(entryTypes.FALSE_RECOMMENDATION)"
(click)="selectEntryType(entryTypes.FALSE_RECOMMENDATION, true)"
[active]="activeEntryType === entryTypes.FALSE_RECOMMENDATION"
[label]="
'edit-dossier-dialog.dictionary.false-recommendations'

View File

@ -11,13 +11,14 @@
.dictionaries {
border-right: 1px solid var(--iqser-separator);
overflow-y: scroll;
overflow-y: auto;
width: 200px;
@include common-mixins.scroll-bar;
.dictionary {
height: 40px;
padding: 15px;
border: 1px solid var(--iqser-separator);
border-bottom: 1px solid var(--iqser-separator);
display: flex;
gap: 10px;
cursor: pointer;
@ -43,33 +44,7 @@
.entries {
flex-grow: 1;
padding: 16px 0 16px 12px;
.header-wrapper {
display: flex;
justify-content: space-between;
.header-left {
display: flex;
.iqser-input-group {
margin-left: 24px;
}
}
.read-only {
padding-right: 100px;
}
.display-name {
display: flex;
align-items: center;
margin-bottom: 24px;
> div {
font-weight: 600;
}
}
}
overflow: hidden;
}
}

View File

@ -1,5 +1,12 @@
import { Component, Input, OnInit, ViewChild } from '@angular/core';
import { CircleButtonComponent, IconButtonComponent, IqserDialog, LoadingService } from '@iqser/common-ui';
import {
CircleButtonComponent,
ConfirmationDialogService,
ConfirmOptions,
IconButtonComponent,
IqserDialog,
LoadingService,
} from '@iqser/common-ui';
import { List } from '@iqser/common-ui/lib/utils';
import { Dictionary, DictionaryEntryType, DictionaryEntryTypes, Dossier } from '@red/domain';
import { DictionaryService } from '@services/entity-services/dictionary.service';
@ -42,6 +49,14 @@ export class EditDossierDictionaryComponent implements OnInit {
readonly entryTypes = DictionaryEntryTypes;
@ViewChild(DictionaryManagerComponent, { static: false }) private readonly _dictionaryManager: DictionaryManagerComponent;
constructor(
private readonly _dictionaryService: DictionaryService,
private readonly _permissionsService: PermissionsService,
private readonly _loadingService: LoadingService,
private readonly _iqserDialog: IqserDialog,
private readonly _confirmationDialogService: ConfirmationDialogService,
) {}
get changed(): boolean {
return this._dictionaryManager?.editor.hasChanges;
}
@ -54,13 +69,6 @@ export class EditDossierDictionaryComponent implements OnInit {
return this._dictionaryManager?.editor.hasChanges;
}
constructor(
private readonly _dictionaryService: DictionaryService,
private readonly _permissionsService: PermissionsService,
private readonly _loadingService: LoadingService,
private readonly _iqserDialog: IqserDialog,
) {}
async ngOnInit() {
this._loadingService.start();
this.canEdit = this._permissionsService.canEditDossierDictionary(this.dossier);
@ -95,16 +103,23 @@ export class EditDossierDictionaryComponent implements OnInit {
this._dictionaryManager.revert();
}
selectDictionary(dictionary: Dictionary, entryType?: DictionaryEntryType) {
async selectDictionary(dictionary: Dictionary, entryType?: DictionaryEntryType, checkForChanges = false) {
if (checkForChanges && !(await this._checkForChanges()).continue) {
return;
}
this.selectedDictionary = dictionary;
this.selectEntryType(entryType);
await this.selectEntryType(entryType);
}
selectEntryType(selectedEntryType: DictionaryEntryType) {
this.activeEntryType = selectedEntryType ?? this.activeEntryType;
const entryType = this.selectedDictionary.hint ? DictionaryEntryTypes.ENTRY : this.activeEntryType;
async selectEntryType(selectedEntryType: DictionaryEntryType, checkForChanges = false) {
if (checkForChanges && !(await this._checkForChanges()).continue) {
return;
}
switch (entryType) {
this.activeEntryType = selectedEntryType ?? this.activeEntryType;
switch (this.activeEntryType) {
case DictionaryEntryTypes.ENTRY: {
this.entriesToDisplay = this.selectedDictionary.entries;
break;
@ -141,13 +156,32 @@ export class EditDossierDictionaryComponent implements OnInit {
await this.#retrieveDictionaries();
}
private async _checkForChanges(): Promise<{ continue: boolean }> {
if (this.changed) {
const dialogRef = this._confirmationDialogService.open({ disableConfirm: !this.valid });
const result = await firstValueFrom(dialogRef.afterClosed());
if (result === ConfirmOptions.CONFIRM) {
this._loadingService.start();
const { success } = await this.save();
this._loadingService.stop();
if (!success) {
return { continue: false };
}
} else if (!result) {
return { continue: false };
}
}
return { continue: true };
}
async #updateDossierDictionary() {
await this.#retrieveDictionaries();
let dictionaryToSelect = this.dictionaries[0];
if (this.selectedDictionary) {
dictionaryToSelect = this.dictionaries.find(d => d.type === this.selectedDictionary.type);
}
this.selectDictionary(dictionaryToSelect, this.activeEntryType);
await this.selectDictionary(dictionaryToSelect, this.activeEntryType);
}
async #retrieveDictionaries() {

View File

@ -13,7 +13,8 @@
></iqser-circle-button>
<iqser-circle-button
(action)="editor.openFindPanel()"
(action)="editor.toggleFindPanel()"
[attr.aria-expanded]="_isSearchOpen()"
[matTooltip]="'dictionary-overview.search' | translate"
class="ml-8"
icon="iqser:search"
@ -92,6 +93,7 @@
<div class="editor-container">
<redaction-editor
[(isSearchOpen)]="_isSearchOpen"
[canEdit]="canEdit"
[diffEditorText]="diffEditorText"
[initialEntries]="initialEntries"

View File

@ -1,4 +1,15 @@
import { ChangeDetectorRef, Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges, ViewChild } from '@angular/core';
import {
ChangeDetectorRef,
Component,
EventEmitter,
Input,
OnChanges,
OnInit,
Output,
signal,
SimpleChanges,
ViewChild,
} from '@angular/core';
import { CircleButtonComponent, IconButtonComponent, IconButtonTypes, LoadingService } from '@iqser/common-ui';
import { Dictionary, DictionaryEntryType, DictionaryEntryTypes, DictionaryType, Dossier, DossierTemplate, IDictionary } from '@red/domain';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
@ -77,6 +88,7 @@ export class DictionaryManagerComponent implements OnChanges, OnInit {
selectDossierTemplate = { name: _('dictionary-overview.compare.select-dossier-template') } as DossierTemplate;
compare = false;
dictionaries: List<Dictionary> = this.#dictionaries;
protected readonly _isSearchOpen = signal(false);
protected initialDossierTemplateId: string;
readonly #currentTab = window.location.href.split('/').pop();
#dossierTemplate = this.dossierTemplatesService.all[0];

View File

@ -1,4 +1,4 @@
import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core';
import { Component, Input, model, OnChanges, OnInit, SimpleChanges, untracked } from '@angular/core';
import { LoadingService } from '@iqser/common-ui';
import { EditorThemeService } from '@services/editor-theme.service';
import { Subject } from 'rxjs';
@ -39,6 +39,7 @@ export class EditorComponent implements OnInit, OnChanges {
@Input() diffEditorText: string;
@Input() @OnChange<List, EditorComponent>('revert') initialEntries: List;
@Input() canEdit = false;
readonly isSearchOpen = model.required<boolean>();
/**
* Used as [modified] input on diff editor
* Shouldn't be updated when editing in diff editor.
@ -84,9 +85,14 @@ export class EditorComponent implements OnInit, OnChanges {
return this.currentEntries.length;
}
async openFindPanel(): Promise<void> {
async toggleFindPanel(): Promise<void> {
const isFindPanelOpen = untracked(this.isSearchOpen);
const editor = this.showDiffEditor ? this._diffEditor.getOriginalEditor() : this.codeEditor;
await editor.getAction('actions.find').run();
if (isFindPanelOpen) {
await (editor.getContribution('editor.contrib.findController') as any).closeFindWidget();
} else {
await editor.getAction('actions.find').run();
}
}
onPaste(event: ClipboardEvent) {
@ -127,11 +133,13 @@ export class EditorComponent implements OnInit, OnChanges {
this._diffEditor.getModifiedEditor().onDidChangeModelContent(() => {
this.value = this._diffEditor.getModel().modified.getValue();
});
this._initializeFindWidget(editor.getOriginalEditor());
this.#setTheme();
}
onCodeEditorInit(editor: MonacoStandaloneCodeEditor): void {
this.codeEditor = editor;
this._initializeFindWidget(editor);
this.#setTheme();
}
@ -143,6 +151,15 @@ export class EditorComponent implements OnInit, OnChanges {
this._editorTextChanged$.next(this.value);
}
private _initializeFindWidget(editor: MonacoStandaloneCodeEditor): void {
this.isSearchOpen.set(false);
(editor.getContribution('editor.contrib.findController') as any).getState().onFindReplaceStateChange(event => {
if (event.isRevealed) {
this.isSearchOpen.update(v => !v);
}
});
}
#getDecorations(newText: string) {
const currentEntries = newText.split('\n');
const newDecorations: IModelDeltaDecoration[] = [];

View File

@ -26,20 +26,13 @@ export const redactTextTranslations: Record<'onlyHere' | 'inDocument' | 'inDossi
},
} as const;
export const editRedactionTranslations: Record<'onlyHere' | 'inDossier', DialogOption> = {
export const editRedactionTranslations: Record<'onlyHere' | 'inDocument', DialogOption> = {
onlyHere: {
label: _('edit-redaction.dialog.content.options.only-here.label'),
description: _('edit-redaction.dialog.content.options.only-here.description'),
},
inDossier: {
label: _('edit-redaction.dialog.content.options.in-dossier.label'),
description: _('edit-redaction.dialog.content.options.in-dossier.description'),
extraOptionLabel: _('edit-redaction.dialog.content.options.in-dossier.extraOptionLabel'),
inDocument: {
label: _('edit-redaction.dialog.content.options.in-document.label'),
description: _('edit-redaction.dialog.content.options.in-document.description'),
},
} as const;
export const editRedactionLabelsTranslations = {
redactedText: _('edit-redaction.dialog.content.redacted-text'),
customRectangle: _('edit-redaction.dialog.content.custom-rectangle'),
imported: _('edit-redaction.dialog.content.imported'),
} as const;

View File

@ -27,8 +27,6 @@ export function mainGuard(): AsyncGuard {
const logger = inject(NGXLogger);
logger.info('[ROUTES] Main resolver started...');
console.log('main guard');
const router = inject(Router);
inject(FeaturesService).loadConfig();
if (inject(IqserPermissionsService).has(Roles.dossiers.read)) {

View File

@ -1234,11 +1234,8 @@
"save": "Speichern",
"title": "{label} bearbeiten"
},
"entries": "{length} {length, plural, one{Eintrag} other{Einträge}}",
"entries-count": "",
"false-positive-entries": "{length} {length, plural, one{Falsch-Positiver} other{Falsch-Positive}}",
"false-positives": "Falsch-Positive ({count})",
"false-recommendation-entries": "{length} {length, plural, one{falsche Empfehlung} other{falsche Empfehlungen}}",
"false-recommendations": "Falsche Empfehlungen ({count})",
"to-redact": "Schwärzungen ({count})"
},
@ -1284,14 +1281,11 @@
"content": {
"comment": "Kommentar",
"comment-placeholder": "Bemerkungen oder Notizen hinzufügen...",
"custom-rectangle": "Bereichsschwärzung",
"imported": "Importierte Schwärzung",
"legal-basis": "Rechtsgrundlage",
"options": {
"in-dossier": {
"description": "Schwärzung in jedem Dokument in {dossierName} bearbeiten.",
"extraOptionLabel": "In alle aktiven und zukünftigen Dossiers übernehmen",
"label": "Typ in Dossier ändern"
"in-document": {
"description": "",
"label": ""
},
"only-here": {
"description": "Bearbeiten Sie die Schwärzung nur an dieser Stelle im Dokument.",

View File

@ -1234,11 +1234,8 @@
"save": "Save",
"title": "Edit {label}"
},
"entries": "{length} {length, plural, one{entry} other{entries}} to redact",
"entries-count": "{count} {count, plural, one{entry} other{entries}}",
"false-positive-entries": "{length} false positive {length, plural, one{entry} other{entries}}",
"false-positives": "False positives ({count})",
"false-recommendation-entries": "{length} false recommendation {length, plural, one{entry} other{entries}}",
"false-recommendations": "False recommendations ({count})",
"to-redact": "Entries ({count})"
},
@ -1284,18 +1281,15 @@
"content": {
"comment": "Comment",
"comment-placeholder": "Add remarks or notes...",
"custom-rectangle": "Custom Rectangle",
"imported": "Imported Redaction",
"legal-basis": "Legal basis",
"options": {
"in-dossier": {
"description": "Edit redaction in every document in {dossierName}.",
"extraOptionLabel": "Apply to all active and future dossiers",
"label": "Change type in dossier"
"in-document": {
"description": "Edit redaction of all linked occurrences of the term in this document.",
"label": "Change in document"
},
"only-here": {
"description": "Edit redaction only at this position in this document.",
"label": "Change type only here"
"label": "Change only here"
}
},
"reason": "Reason",

View File

@ -1234,11 +1234,8 @@
"save": "",
"title": ""
},
"entries": "{length} {length, plural, one{entry} other{entries}} to {hint, select, true{annotate} other{redact}}",
"entries-count": "",
"false-positive-entries": "{length} false positive {length, plural, one{entry} other{entries}}",
"false-positives": "False positives ({count})",
"false-recommendation-entries": "{length} false recommendation {length, plural, one{entry} other{entries}}",
"false-recommendations": "False recommendations ({count})",
"to-redact": "To redact ({count})"
},
@ -1284,13 +1281,10 @@
"content": {
"comment": "Comment",
"comment-placeholder": "Add remarks or mentions...",
"custom-rectangle": "",
"imported": "",
"legal-basis": "",
"options": {
"in-dossier": {
"in-document": {
"description": "",
"extraOptionLabel": "",
"label": ""
},
"only-here": {

View File

@ -1234,11 +1234,8 @@
"save": "",
"title": ""
},
"entries": "{length} {length, plural, one{entry} other{entries}} to {hint, select, true{annotate} other{redact}}",
"entries-count": "{count} {count, plural, one{entry} other{entries}}",
"false-positive-entries": "{length} false positive {length, plural, one{entry} other{entries}}",
"false-positives": "False positives ({count})",
"false-recommendation-entries": "{length} false recommendation {length, plural, one{entry} other{entries}}",
"false-recommendations": "False recommendations ({count})",
"to-redact": "To redact ({count})"
},
@ -1284,13 +1281,10 @@
"content": {
"comment": "Comment",
"comment-placeholder": "Add remarks or mentions...",
"custom-rectangle": "",
"imported": "",
"legal-basis": "",
"options": {
"in-dossier": {
"in-document": {
"description": "",
"extraOptionLabel": "",
"label": ""
},
"only-here": {

@ -1 +1 @@
Subproject commit 835cb7820e2100ff1125939f4c2766f06e9c09a6
Subproject commit 34387d49d29ba6449c1311cc1c5434b540398660

View File

@ -6,3 +6,18 @@ export interface IRecategorizationRequest {
readonly section?: string;
readonly value?: string;
}
export interface IBulkRecategorizationRequest {
readonly value: string;
readonly type: string;
readonly legalBasis: string;
readonly section: string;
readonly originTypes: string[];
readonly originLegalBases: string[];
readonly rectangle: boolean;
readonly position?: {
readonly rectangle: number[];
readonly pageNumber: number;
};
readonly pageNumbers?: number[];
}