Merge branch 'master' into VM/RED-8748

This commit is contained in:
Valentin Mihai 2024-04-12 13:52:03 +03:00
commit c024d06806
50 changed files with 3105 additions and 2930 deletions

View File

@ -28,7 +28,7 @@ export const canRemoveFromDictionary = (annotation: AnnotationWrapper, autoAnaly
(annotation.isRedacted || annotation.isSkipped || annotation.isHint || (annotation.isIgnoredHint && !annotation.isRuleBased)) &&
(autoAnalysisDisabled || !annotation.pending) &&
!annotation.hasBeenResizedLocally &&
(!annotation.hasBeenForcedHint || !annotation.hasBeenForcedRedaction);
!(annotation.hasBeenForcedHint || annotation.hasBeenForcedRedaction);
export const canRemoveRedaction = (annotation: AnnotationWrapper, permissions: AnnotationPermissions) =>
(!annotation.isIgnoredHint || !annotation.isRuleBased) &&
@ -58,6 +58,12 @@ export const canResizeAnnotation = (
(!annotation.isHint && hasDictionary && annotation.isRuleBased))) ||
annotation.isRecommendation;
export const canResizeInDictionary = (annotation: AnnotationWrapper, permissions: AnnotationPermissions) =>
permissions.canResizeAnnotation &&
annotation.isModifyDictionary &&
!annotation.hasBeenResizedLocally &&
!(annotation.hasBeenForcedHint || annotation.hasBeenForcedRedaction);
export const canEditAnnotation = (annotation: AnnotationWrapper) => (annotation.isRedacted || annotation.isSkipped) && !annotation.isImage;
export const canEditHint = (annotation: AnnotationWrapper) =>

View File

@ -16,6 +16,7 @@ import {
canRemoveOnlyHere,
canRemoveRedaction,
canResizeAnnotation,
canResizeInDictionary,
canUndo,
} from './annotation-permissions.utils';
import { AnnotationWrapper } from './annotation.wrapper';
@ -30,6 +31,7 @@ export class AnnotationPermissions {
canForceRedaction = true;
canChangeLegalBasis = true;
canResizeAnnotation = true;
canResizeInDictionary = true;
canRecategorizeAnnotation = true;
canForceHint = true;
canEditAnnotations = true;
@ -69,6 +71,7 @@ export class AnnotationPermissions {
autoAnalysisDisabled,
annotationEntity?.hasDictionary,
);
permissions.canResizeInDictionary = canResizeInDictionary(annotation, permissions);
permissions.canEditAnnotations = canEditAnnotation(annotation);
permissions.canEditHints = canEditHint(annotation);
permissions.canEditImages = canEditImage(annotation);
@ -80,6 +83,7 @@ export class AnnotationPermissions {
static reduce(permissions: AnnotationPermissions[]): AnnotationPermissions {
const result = new AnnotationPermissions();
result.canResizeAnnotation = permissions.length === 1 && permissions[0].canResizeAnnotation;
result.canResizeInDictionary = permissions.length === 1 && permissions[0].canResizeInDictionary;
result.canChangeLegalBasis = permissions.reduce((acc, next) => acc && next.canChangeLegalBasis, true);
result.canRecategorizeAnnotation = permissions.reduce((acc, next) => acc && next.canRecategorizeAnnotation, true);
result.canRemoveFromDictionary = permissions.reduce((acc, next) => acc && next.canRemoveFromDictionary, true);

View File

@ -23,9 +23,3 @@ form {
font-size: 16px;
opacity: 0.7;
}
.item-info {
background: var(--iqser-light);
border: 1px solid var(--iqser-grey-1);
padding: 20px;
}

View File

@ -3,7 +3,7 @@ import { LicenseService } from '@services/license.service';
import { map } from 'rxjs/operators';
import { ChartDataset } from 'chart.js';
import { ChartBlue, ChartGreen, ChartRed } from '../../utils/constants';
import { getLabelsFromLicense, getLineConfig } from '../../utils/functions';
import { getDataUntilCurrentMonth, getLabelsFromLicense, getLineConfig } from '../../utils/functions';
import { TranslateService } from '@ngx-translate/core';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { size } from '@iqser/common-ui/lib/utils';
@ -48,7 +48,7 @@ export class LicenseAnalysisCapacityUsageComponent {
},
{
data: monthlyData.map(
data: getDataUntilCurrentMonth(monthlyData).map(
(month, monthIndex) =>
month.analysedFilesBytes + monthlyData.slice(0, monthIndex).reduce((acc, curr) => acc + curr.analysedFilesBytes, 0),
),

View File

@ -3,7 +3,7 @@ import { LicenseService } from '@services/license.service';
import { map } from 'rxjs/operators';
import { ChartDataset } from 'chart.js';
import { ChartBlue, ChartGreen, ChartRed } from '../../utils/constants';
import { getLabelsFromLicense, getLineConfig } from '../../utils/functions';
import { getDataUntilCurrentMonth, getLabelsFromLicense, getLineConfig } from '../../utils/functions';
import { TranslateService } from '@ngx-translate/core';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { addPaddingToArray } from '@utils/functions';
@ -58,7 +58,7 @@ export class LicensePageUsageComponent {
order: 1,
},
{
data: monthlyData.map(
data: getDataUntilCurrentMonth(monthlyData).map(
(month, monthIndex) =>
month.numberOfAnalyzedPages +
monthlyData.slice(0, monthIndex).reduce((acc, curr) => acc + curr.numberOfAnalyzedPages, 0),

View File

@ -6,7 +6,7 @@ import { map } from 'rxjs/operators';
import type { DonutChartConfig, ILicenseReport } from '@red/domain';
import { ChartDataset } from 'chart.js';
import { ChartBlack, ChartBlue, ChartGreen, ChartGrey, ChartRed } from '../../utils/constants';
import { getLabelsFromLicense, getLineConfig } from '../../utils/functions';
import { getDataUntilCurrentMonth, getLabelsFromLicense, getLineConfig } from '../../utils/functions';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
@Component({
@ -60,7 +60,7 @@ export class LicenseRetentionCapacityComponent {
}
#getDatasets(license: ILicenseReport): ChartDataset[] {
const monthlyData = license.monthlyData;
const monthlyData = getDataUntilCurrentMonth(license.monthlyData);
return [
{

View File

@ -1,19 +1,19 @@
import { inject, NgModule } from '@angular/core';
import { LicenseScreenComponent } from './license-screen/license-screen.component';
import { LicenseSelectComponent } from './components/license-select/license-select.component';
import { RouterModule, Routes } from '@angular/router';
import { TranslateModule } from '@ngx-translate/core';
import { MatSelectModule } from '@angular/material/select';
import { IqserHelpModeModule, IqserListingModule, SizePipe } from '@iqser/common-ui';
import { FormsModule } from '@angular/forms';
import { CommonModule } from '@angular/common';
import { inject, NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { MatSelectModule } from '@angular/material/select';
import { RouterModule, Routes } from '@angular/router';
import { IqserHelpModeModule, IqserListingModule, SizePipe } from '@iqser/common-ui';
import { TranslateModule } from '@ngx-translate/core';
import { LicenseService } from '@services/license.service';
import { ChartComponent } from './components/chart/chart.component';
import { NgChartsModule } from 'ng2-charts';
import { LicenseRetentionCapacityComponent } from './components/license-retention-capacity-usage/license-retention-capacity.component';
import { DonutChartComponent } from '@shared/components/donut-chart/donut-chart.component';
import { LicensePageUsageComponent } from './components/license-page-usage/license-page-usage.component';
import { BaseChartDirective, provideCharts, withDefaultRegisterables } from 'ng2-charts';
import { ChartComponent } from './components/chart/chart.component';
import { LicenseAnalysisCapacityUsageComponent } from './components/license-analysis-capacity-usage/license-analysis-capacity-usage.component';
import { LicensePageUsageComponent } from './components/license-page-usage/license-page-usage.component';
import { LicenseRetentionCapacityComponent } from './components/license-retention-capacity-usage/license-retention-capacity.component';
import { LicenseSelectComponent } from './components/license-select/license-select.component';
import { LicenseScreenComponent } from './license-screen/license-screen.component';
const routes: Routes = [
{
@ -43,8 +43,9 @@ const routes: Routes = [
IqserListingModule,
IqserHelpModeModule,
SizePipe,
NgChartsModule,
DonutChartComponent,
BaseChartDirective,
],
providers: [provideCharts(withDefaultRegisterables())],
})
export class LicenseModule {}

View File

@ -1,10 +1,11 @@
import dayjs, { Dayjs } from 'dayjs';
import { FillTarget } from 'chart.js';
import { addPaddingToArray, hexToRgba } from '@utils/functions';
import { ILicenseReport } from '@red/domain';
import { ILicenseData, ILicenseReport } from '@red/domain';
import { ComplexFillTarget } from 'chart.js/dist/types';
const monthNames = dayjs.monthsShort();
const currentMonth = dayjs(Date.now()).month() + 1;
export const verboseDate = (date: Dayjs) => `${monthNames[date.month()]} ${date.year()}`;
@ -49,3 +50,7 @@ export const getLabelsFromLicense = (license: ILicenseReport) => {
return result;
};
export const getDataUntilCurrentMonth = (monthlyData: ILicenseData[]) => {
return monthlyData.slice(0, currentMonth);
};

View File

@ -1,5 +1,3 @@
@use 'variables';
.right-container {
display: flex;
width: 353px;
@ -15,21 +13,19 @@
}
}
::ng-deep .page-header .actions > *:not(:last-child) {
margin-right: 6px;
::ng-deep .page-header .actions {
gap: 6px;
}
.action-buttons > div {
display: flex;
}
.cell {
&.no-role-alignment {
flex-direction: row;
justify-content: start;
align-items: center;
color: variables.$primary;
}
.cell.no-role-alignment {
flex-direction: row;
justify-content: start;
align-items: center;
color: var(--iqser-primary);
}
.opacity-1 {

View File

@ -148,7 +148,7 @@ export class AnnotationActionsComponent implements OnChanges {
acceptResize() {
if (this.resized) {
return this.annotationActionsService.acceptResize(this.#annotations[0]);
return this.annotationActionsService.acceptResize(this.#annotations[0], this.annotationPermissions);
}
}

View File

@ -14,7 +14,7 @@ interface Engine {
readonly translateParams?: Record<string, any>;
}
const Engines = {
export const Engines = {
DICTIONARY: 'DICTIONARY',
NER: 'NER',
RULE: 'RULE',
@ -54,7 +54,7 @@ export class AnnotationDetailsComponent implements OnChanges {
getChangesTooltip(): string | undefined {
const changes = changesProperties.filter(key => this.annotation.item[key]);
if (!changes.length) {
if (!changes.length || !this.annotation.item.engines?.includes(Engines.MANUAL)) {
return;
}
@ -93,11 +93,6 @@ export class AnnotationDetailsComponent implements OnChanges {
description: _('annotation-engines.imported'),
show: isBasedOn(annotation, Engines.IMPORTED),
},
{
icon: 'red:redaction-changes',
description: _('annotation-engines.manual'),
show: isBasedOn(annotation, Engines.MANUAL),
},
];
}
}

View File

@ -0,0 +1,5 @@
<cdk-virtual-scroll-viewport [itemSize]="LIST_ITEM_SIZE" [ngStyle]="{ height: redactedTextsAreaHeight + 'px' }">
<ul>
<li *cdkVirtualFor="let value of values">{{ value }}</li>
</ul>
</cdk-virtual-scroll-viewport>

View File

@ -0,0 +1,21 @@
@use 'common-mixins';
cdk-virtual-scroll-viewport {
@include common-mixins.scroll-bar;
}
:host ::ng-deep .cdk-virtual-scroll-orientation-vertical .cdk-virtual-scroll-content-wrapper {
max-width: 100% !important;
}
ul {
padding-left: 16px;
li {
white-space: nowrap;
text-overflow: ellipsis;
list-style-position: inside;
overflow: hidden;
padding-right: 10px;
}
}

View File

@ -0,0 +1,22 @@
import { Component, Input } from '@angular/core';
import { CdkFixedSizeVirtualScroll, CdkVirtualForOf, CdkVirtualScrollViewport } from '@angular/cdk/scrolling';
import { NgStyle } from '@angular/common';
const LIST_ITEM_SIZE = 16;
const MAX_ITEMS_DISPLAY = 5;
@Component({
selector: 'redaction-selected-annotations-list',
standalone: true,
imports: [CdkFixedSizeVirtualScroll, CdkVirtualForOf, CdkVirtualScrollViewport, NgStyle],
templateUrl: './selected-annotations-list.component.html',
styleUrl: './selected-annotations-list.component.scss',
})
export class SelectedAnnotationsListComponent {
@Input({ required: true }) values: string[];
protected readonly LIST_ITEM_SIZE = LIST_ITEM_SIZE;
get redactedTextsAreaHeight() {
return this.values.length <= MAX_ITEMS_DISPLAY ? LIST_ITEM_SIZE * this.values.length : LIST_ITEM_SIZE * MAX_ITEMS_DISPLAY;
}
}

View File

@ -0,0 +1,16 @@
<table>
<thead>
<tr>
<th *ngFor="let column of columns" [ngClass]="{ hide: !column.show }">
<label>{{ column.label }}</label>
</th>
</tr>
</thead>
<tbody [ngStyle]="{ height: redactedTextsAreaHeight + 'px' }">
<tr *ngFor="let row of data">
<td *ngFor="let cell of row" [ngClass]="{ hide: !cell.show, bold: cell.bold }">
{{ cell.label }}
</td>
</tr>
</tbody>
</table>

View File

@ -0,0 +1,63 @@
@use 'common-mixins';
table {
padding: 0 13px;
max-width: 100%;
min-width: 100%;
border-spacing: 0;
tbody {
padding-top: 2px;
overflow-y: auto;
display: block;
@include common-mixins.scroll-bar;
}
tr {
max-width: 100%;
min-width: 100%;
display: table;
th {
label {
opacity: 0.7;
font-weight: normal;
}
}
th,
td {
max-width: 0;
width: 25%;
text-align: start;
white-space: nowrap;
text-overflow: ellipsis;
list-style-position: inside;
overflow: hidden;
padding-right: 8px;
}
th:last-child,
td:last-child {
max-width: 0;
width: 50%;
padding-right: 0;
}
}
}
tbody tr:nth-child(odd) {
td {
background-color: var(--iqser-alt-background);
}
}
.hide {
visibility: hidden;
}
.bold {
font-weight: bold;
}

View File

@ -0,0 +1,27 @@
import { Component, Input } from '@angular/core';
import { NgClass, NgForOf, NgStyle } from '@angular/common';
export interface ValueColumn {
label: string;
show: boolean;
bold?: boolean;
}
const TABLE_ROW_SIZE = 18;
const MAX_ITEMS_DISPLAY = 10;
@Component({
selector: 'redaction-selected-annotations-table',
standalone: true,
imports: [NgForOf, NgClass, NgStyle],
templateUrl: './selected-annotations-table.component.html',
styleUrl: './selected-annotations-table.component.scss',
})
export class SelectedAnnotationsTableComponent {
@Input({ required: true }) columns: ValueColumn[];
@Input({ required: true }) data: ValueColumn[][];
get redactedTextsAreaHeight() {
return this.data.length <= MAX_ITEMS_DISPLAY ? TABLE_ROW_SIZE * this.data.length : TABLE_ROW_SIZE * MAX_ITEMS_DISPLAY;
}
}

View File

@ -5,14 +5,7 @@
<div class="dialog-content redaction">
<div class="iqser-input-group" *ngIf="showList">
<label [translate]="'edit-redaction.dialog.content.redacted-text'" class="selected-text"></label>
<cdk-virtual-scroll-viewport
[itemSize]="16"
[ngStyle]="{ height: redactedTexts.length <= 5 ? 16 * redactedTexts.length + 'px' : 80 + 'px' }"
>
<ul *cdkVirtualFor="let text of redactedTexts">
<li>{{ text }}</li>
</ul>
</cdk-virtual-scroll-viewport>
<redaction-selected-annotations-list [values]="redactedTexts"></redaction-selected-annotations-list>
</div>
<div class="iqser-input-group required w-450">

View File

@ -1,21 +0,0 @@
@use 'common-mixins';
cdk-virtual-scroll-viewport {
@include common-mixins.scroll-bar;
}
:host ::ng-deep .cdk-virtual-scroll-orientation-vertical .cdk-virtual-scroll-content-wrapper {
max-width: 100% !important;
}
ul {
padding-left: 16px;
}
li {
white-space: nowrap;
text-overflow: ellipsis;
list-style-position: inside;
overflow: hidden;
padding-right: 10px;
}

View File

@ -7,16 +7,8 @@
></div>
<div class="dialog-content redaction" [class.fixed-height]="isRedacted && isImage">
<div class="iqser-input-group" *ngIf="!isImage">
<cdk-virtual-scroll-viewport
*ngIf="!!redactedTexts.length && !allRectangles"
[itemSize]="16"
[ngStyle]="{ height: redactedTexts.length <= 5 ? 16 * redactedTexts.length + 'px' : 80 + 'px' }"
>
<ul *cdkVirtualFor="let text of redactedTexts">
<li>{{ text }}</li>
</ul>
</cdk-virtual-scroll-viewport>
<div class="iqser-input-group" *ngIf="!isImage && redactedTexts.length && !allRectangles">
<redaction-selected-annotations-list [values]="redactedTexts"></redaction-selected-annotations-list>
</div>
<div *ngIf="!isManualRedaction" class="iqser-input-group w-450" [class.required]="!form.controls.type.disabled">

View File

@ -6,23 +6,3 @@
overflow-y: auto;
}
}
cdk-virtual-scroll-viewport {
@include common-mixins.scroll-bar;
}
:host ::ng-deep .cdk-virtual-scroll-orientation-vertical .cdk-virtual-scroll-content-wrapper {
max-width: 100% !important;
}
ul {
padding-left: 16px;
}
li {
white-space: nowrap;
text-overflow: ellipsis;
list-style-position: inside;
overflow: hidden;
padding-right: 10px;
}

View File

@ -4,6 +4,7 @@
<div *ngIf="isHintDialog" class="dialog-header heading-l" [translate]="'manual-annotation.dialog.header.force-hint'"></div>
<div class="dialog-content">
<redaction-selected-annotations-table [columns]="tableColumns" [data]="tableData"></redaction-selected-annotations-table>
<div *ngIf="!isHintDialog && !isDocumine" class="iqser-input-group required w-400">
<label [translate]="'manual-annotation.dialog.content.reason'"></label>
<mat-form-field>

View File

@ -1,3 +1,7 @@
.full-width {
width: 100%;
}
.dialog-content {
padding-top: 8px;
}

View File

@ -7,6 +7,7 @@ import { Dossier, ILegalBasisChangeRequest } from '@red/domain';
import { firstValueFrom } from 'rxjs';
import { AnnotationWrapper } from '@models/file/annotation.wrapper';
import { Roles } from '@users/roles';
import { ValueColumn } from '../../components/selected-annotations-table/selected-annotations-table.component';
export interface LegalBasisOption {
label?: string;
@ -23,8 +24,24 @@ const DOCUMINE_LEGAL_BASIS = 'n-a.';
})
export class ForceAnnotationDialogComponent extends BaseDialogComponent implements OnInit {
readonly isDocumine = getConfig().IS_DOCUMINE;
protected readonly roles = Roles;
readonly tableColumns = [
{
label: 'Value',
show: true,
},
{
label: 'Type',
show: true,
},
];
readonly tableData: ValueColumn[][] = this._data.annotations.map(redaction => [
{ label: redaction.value, show: true, bold: true },
{ label: redaction.typeLabel, show: true },
]);
legalOptions: LegalBasisOption[] = [];
protected readonly roles = Roles;
constructor(
private readonly _justificationsService: JustificationsService,
@ -33,7 +50,7 @@ export class ForceAnnotationDialogComponent extends BaseDialogComponent implemen
private readonly _data: { readonly dossier: Dossier; readonly hint: boolean; annotations: AnnotationWrapper[] },
) {
super(_dialogRef);
this.form = this._getForm();
this.form = this.#getForm();
}
get isHintDialog() {
@ -66,17 +83,17 @@ export class ForceAnnotationDialogComponent extends BaseDialogComponent implemen
}
save() {
this._dialogRef.close(this._createForceRedactionRequest());
this._dialogRef.close(this.#createForceRedactionRequest());
}
private _getForm(): UntypedFormGroup {
#getForm(): UntypedFormGroup {
return this._formBuilder.group({
reason: this._data.hint ? ['Forced Hint'] : [null, !this.isDocumine ? Validators.required : null],
comment: [null],
});
}
private _createForceRedactionRequest(): ILegalBasisChangeRequest {
#createForceRedactionRequest(): ILegalBasisChangeRequest {
const request: ILegalBasisChangeRequest = {};
request.legalBasis = !this.isDocumine ? this.form.get('reason').value.legalBasis : DOCUMINE_LEGAL_BASIS;

View File

@ -3,9 +3,8 @@
<div [translate]="'redact-text.dialog.title'" class="dialog-header heading-l"></div>
<div class="dialog-content redaction">
<div *ngIf="form.controls.selectedText.value as selectedText" class="iqser-input-group w-450">
<label [translate]="'redact-text.dialog.content.selected-text'" class="selected-text"></label>
{{ selectedText }}
<div class="iqser-input-group">
<redaction-selected-annotations-list [values]="selectedValues"></redaction-selected-annotations-list>
</div>
<iqser-details-radio

View File

@ -30,6 +30,7 @@ export class RedactRecommendationDialogComponent
dictionaryRequest = false;
legalOptions: LegalBasisOption[] = [];
dictionaries: Dictionary[] = [];
readonly selectedValues = this.data.annotations.map(annotation => annotation.value);
readonly form = inject(FormBuilder).group({
selectedText: this.isMulti ? null : this.firstEntry.value,
comment: [null],

View File

@ -8,7 +8,11 @@
[class.fixed-height-36]="dictionaryRequest"
[ngClass]="isEditingSelectedText ? 'flex relative' : 'flex-align-items-center'"
>
<span *ngIf="!isEditingSelectedText" [innerHTML]="form.controls.selectedText.value"></span>
<ul>
<li>
<span *ngIf="!isEditingSelectedText" [innerHTML]="form.controls.selectedText.value"></span>
</li>
</ul>
<textarea
*ngIf="isEditingSelectedText"

View File

@ -7,24 +7,10 @@
<div [ngStyle]="{ height: dialogContentHeight + redactedTextsAreaHeight + 'px' }" class="dialog-content redaction">
<div class="iqser-input-group">
<table>
<thead>
<tr>
<th *ngFor="let column of columns()" [ngClass]="{ hide: !column.show }">
<label>{{ column.label }}</label>
</th>
</tr>
</thead>
<tbody [ngStyle]="{ height: redactedTextsAreaHeight + 'px' }">
<tr *ngFor="let text of redactedTexts; let idx = index">
<td>
<b>{{ text }}</b>
</td>
<td>{{ data.redactions[idx].typeLabel }}</td>
<td [ngClass]="{ hide: !isFalsePositive() }">{{ data.falsePositiveContext[idx] }}</td>
</tr>
</tbody>
</table>
<redaction-selected-annotations-table
[columns]="tableColumns()"
[data]="tableData()"
></redaction-selected-annotations-table>
</div>
<iqser-details-radio [options]="options" formControlName="option"></iqser-details-radio>

View File

@ -1,64 +1,4 @@
@use 'common-mixins';
.dialog-content {
padding-top: 8px;
padding-bottom: 35px;
}
table {
padding: 0 13px;
max-width: 100%;
min-width: 100%;
border-spacing: 0;
tbody {
padding-top: 2px;
overflow-y: auto;
display: block;
@include common-mixins.scroll-bar;
}
tr {
max-width: 100%;
min-width: 100%;
display: table;
th {
label {
opacity: 0.7;
font-weight: normal;
}
}
th,
td {
max-width: 0;
width: 25%;
text-align: start;
white-space: nowrap;
text-overflow: ellipsis;
list-style-position: inside;
overflow: hidden;
padding-right: 8px;
}
th:last-child,
td:last-child {
max-width: 0;
width: 50%;
padding-right: 0;
}
}
}
tbody tr:nth-child(odd) {
td {
background-color: var(--iqser-alt-background);
}
}
.hide {
visibility: hidden;
}

View File

@ -7,11 +7,7 @@ import { Roles } from '@users/roles';
import { DialogHelpModeKeys } from '../../utils/constants';
import { toSignal } from '@angular/core/rxjs-interop';
import { map } from 'rxjs/operators';
interface ValuesColumns {
label: string;
show: boolean;
}
import { ValueColumn } from '../../components/selected-annotations-table/selected-annotations-table.component';
@Component({
templateUrl: './remove-redaction-dialog.component.html',
@ -37,7 +33,7 @@ export class RemoveRedactionDialogComponent extends IqserDialogComponent<
readonly selectedOption = toSignal(this.form.get('option').valueChanges.pipe(map(value => value.value)));
readonly isFalsePositive = computed(() => this.selectedOption() === RemoveRedactionOptions.FALSE_POSITIVE);
readonly columns = computed<ValuesColumns[]>(() => [
readonly tableColumns = computed<ValueColumn[]>(() => [
{
label: 'Value',
show: true,
@ -52,6 +48,14 @@ export class RemoveRedactionDialogComponent extends IqserDialogComponent<
},
]);
readonly tableData = computed<ValueColumn[][]>(() =>
this.data.redactions.map((redaction, index) => [
{ label: redaction.value, show: true, bold: true },
{ label: redaction.typeLabel, show: true },
{ label: this.data.falsePositiveContext[index], show: this.isFalsePositive() },
]),
);
constructor(private readonly _formBuilder: FormBuilder) {
super();
}

View File

@ -7,14 +7,18 @@
<div class="dialog-content redaction">
<ng-container *ngIf="!redaction.isImage && !redaction.AREA">
<div class="iqser-input-group w-450">
<label [translate]="'resize-redaction.dialog.content.original-text'" class="selected-text"></label>
<span>{{ redaction.value }}</span>
<div class="flex-start">
<label [translate]="'resize-redaction.dialog.content.original-text'"></label>
<span class="multi-line-ellipsis"
><b>{{ redaction.value }}</b></span
>
</div>
<div class="iqser-input-group w-450">
<label [translate]="'resize-redaction.dialog.content.resized-text'" class="selected-text"></label>
<span>{{ data.text }}</span>
<div class="flex-start">
<label [translate]="'resize-redaction.dialog.content.resized-text'"></label>
<span class="multi-line-ellipsis"
><b>{{ data.text }}</b></span
>
</div>
</ng-container>

View File

@ -0,0 +1,20 @@
.multi-line-ellipsis {
-webkit-box-orient: vertical;
display: -webkit-box;
-webkit-line-clamp: 2;
overflow: hidden;
text-overflow: ellipsis;
white-space: normal;
}
.flex-start {
display: flex;
align-items: start;
padding: 0 13px;
label {
opacity: 0.7;
font-weight: normal;
min-width: 15%;
}
}

View File

@ -8,6 +8,7 @@ import { ResizeRedactionData, ResizeRedactionResult } from '../../utils/dialog-t
@Component({
templateUrl: './resize-redaction-dialog.component.html',
styleUrls: ['./resize-redaction-dialog.component.scss'],
})
export class ResizeRedactionDialogComponent extends IqserDialogComponent<
ResizeRedactionDialogComponent,
@ -29,7 +30,14 @@ export class ResizeRedactionDialogComponent extends IqserDialogComponent<
constructor(private readonly _formBuilder: FormBuilder) {
super();
this.options = getResizeRedactionOptions(this.redaction, this.#dossier, false, this.#applyToAllDossiers, this.data.isApprover);
this.options = getResizeRedactionOptions(
this.redaction,
this.#dossier,
false,
this.#applyToAllDossiers,
this.data.isApprover,
this.data.permissions.canResizeInDictionary,
);
this.form = this.#getForm();
}

View File

@ -70,6 +70,8 @@ import { DocumentUnloadedGuard } from './services/document-unloaded.guard';
import { FilePreviewDialogService } from './services/file-preview-dialog.service';
import { ManualRedactionService } from './services/manual-redaction.service';
import { TablesService } from './services/tables.service';
import { SelectedAnnotationsTableComponent } from './components/selected-annotations-table/selected-annotations-table.component';
import { SelectedAnnotationsListComponent } from './components/selected-annotations-list/selected-annotations-list.component';
import { FileHeaderComponent } from './components/file-header/file-header.component';
import { DocumineExportComponent } from './components/documine-export/documine-export.component';
import { StructuredComponentManagementComponent } from './components/structured-component-management/structured-component-management.component';
@ -158,6 +160,8 @@ const components = [
LogPipe,
ReplaceNbspPipe,
DisableStopPropagationDirective,
SelectedAnnotationsTableComponent,
SelectedAnnotationsListComponent,
],
providers: [FilePreviewDialogService, ManualRedactionService, DocumentUnloadedGuard, TablesService],
})

View File

@ -16,7 +16,7 @@ import {
import { CommentsApiService } from '@services/comments-api.service';
import { DossierTemplatesService } from '@services/dossier-templates/dossier-templates.service';
import { PermissionsService } from '@services/permissions.service';
import { firstValueFrom, Observable, zip } from 'rxjs';
import { firstValueFrom, Observable } from 'rxjs';
import { getFirstRelevantTextPart } from '../../../utils';
import { AnnotationDrawService } from '../../pdf-viewer/services/annotation-draw.service';
import { REDAnnotationManager } from '../../pdf-viewer/services/annotation-manager.service';
@ -32,6 +32,7 @@ import { ResizeRedactionDialogComponent } from '../dialogs/resize-redaction-dial
import { RemoveRedactionOptions } from '../utils/dialog-options';
import {
EditRedactionData,
EditRedactResult,
RemoveRedactionData,
RemoveRedactionPermissions,
RemoveRedactionResult,
@ -101,47 +102,34 @@ export class AnnotationActionsService {
};
const result = await this.#getEditRedactionDialog(data).result();
const requests: Observable<unknown>[] = [];
if (!result) {
return;
}
if (
!this.#isDocumine &&
(!annotations.every(annotation => annotation.legalBasis === result.legalBasis) ||
!annotations.every(annotation => annotation.section === result.section))
) {
const changeLegalBasisBody = annotations.map(annotation => ({
annotationId: annotation.id,
legalBasis: result.legalBasis,
section: result.section ?? annotation.section,
value: result.value ?? annotation.value,
}));
requests.push(
this._manualRedactionService.changeLegalBasis(
changeLegalBasisBody,
dossierId,
fileId,
file().excludedFromAutomaticAnalysis && isUnprocessed,
),
);
}
if (result.type && !annotations.every(annotation => annotation.type === result.type)) {
const recategorizeBody: List<IRecategorizationRequest> = annotations.map(annotation => ({
annotationId: annotation.id,
type: result.type ?? annotation.type,
}));
requests.push(
this._manualRedactionService.recategorizeRedactions(
const recategorizeBody: List<IRecategorizationRequest> = annotations.map(annotation => {
const body = { annotationId: annotation.id, type: result.type ?? annotation.type };
if (!this.#isDocumine) {
return {
...body,
legalBasis: result.legalBasis,
section: result.section ?? annotation.section,
value: result.value ?? annotation.value,
};
}
return body;
});
await this.#processObsAndEmit(
this._manualRedactionService
.recategorizeRedactions(
recategorizeBody,
dossierId,
fileId,
this.#getChangedFields(annotations, result),
file().excludedFromAutomaticAnalysis && isUnprocessed,
),
);
}
)
.pipe(log()),
);
if (result.comment) {
try {
@ -152,11 +140,6 @@ export class AnnotationActionsService {
this._toaster.rawError(error.error.message);
}
}
if (!requests.length) {
return;
}
await this.#processObsAndEmit(zip(requests).pipe(log()));
}
async removeRedaction(redactions: AnnotationWrapper[], permissions: AnnotationPermissions) {
@ -240,7 +223,7 @@ export class AnnotationActionsService {
this._annotationManager.select(viewerAnnotation);
}
async acceptResize(annotation: AnnotationWrapper): Promise<void> {
async acceptResize(annotation: AnnotationWrapper, permissions: AnnotationPermissions): Promise<void> {
const textAndPositions = await this.#extractTextAndPositions(annotation.id);
if (annotation.isRecommendation) {
const recommendation = {
@ -263,12 +246,16 @@ export class AnnotationActionsService {
const dossierTemplate = this._dossierTemplatesService.find(this._state.dossierTemplateId);
const isUnprocessed = annotation.pending;
const data = {
const data: ResizeRedactionData = {
redaction: annotation,
text,
applyToAllDossiers: isApprover && dossierTemplate.applyDictionaryUpdatesToAllDossiersByDefault,
isApprover,
dossierId: dossier.dossierId,
permissions: {
canResizeAnnotation: permissions.canResizeAnnotation,
canResizeInDictionary: permissions.canResizeInDictionary,
},
};
const result = await this.#getResizeRedactionDialog(data).result();
@ -498,4 +485,23 @@ export class AnnotationActionsService {
isApprover,
};
}
#getChangedFields(annotations: AnnotationWrapper[], result: EditRedactResult) {
const changedFields = [];
if (result.type && !annotations.every(annotation => annotation.type === result.type)) {
changedFields.push('type');
}
if (this.#isDocumine) {
return { changes: changedFields.join(', ') };
}
if (result.legalBasis && !annotations.every(annotation => annotation.legalBasis === result.legalBasis)) {
changedFields.push('reason');
}
if (typeof result.section === 'string' && !annotations.every(annotation => annotation.section === result.section)) {
changedFields.push('paragraph/location');
}
return { changes: changedFields.join(', ') };
}
}

View File

@ -19,6 +19,7 @@ import {
} from '../utils/sort-by-page-rotation.utils';
import { FileDataService } from './file-data.service';
import { FilePreviewStateService } from './file-preview-state.service';
import { Engines } from '../components/annotation-details/annotation-details.component';
@Injectable()
export class AnnotationProcessingService {
@ -50,7 +51,8 @@ export class AnnotationProcessingService {
label: _('filter-menu.redaction-changes'),
checked: false,
topLevelFilter: true,
checker: (annotation: AnnotationWrapper) => annotation?.hasRedactionChanges,
checker: (annotation: AnnotationWrapper) =>
annotation?.hasRedactionChanges && annotation?.engines?.includes(Engines.MANUAL),
},
{
id: 'unseen-pages',

View File

@ -18,7 +18,7 @@ import type {
import { dictionaryActionsTranslations, manualRedactionActionsTranslations } from '@translations/annotation-actions-translations';
import { Roles } from '@users/roles';
import { NGXLogger } from 'ngx-logger';
import { EMPTY, of, OperatorFunction, pipe } from 'rxjs';
import { EMPTY, of, pipe } from 'rxjs';
import { catchError, tap } from 'rxjs/operators';
function getResponseType(error: boolean, isConflict: boolean) {
@ -62,12 +62,18 @@ export class ManualRedactionService extends GenericService<IManualAddResponse> {
return this.addAnnotation(recommendations, dossierId, fileId);
}
changeLegalBasis(body: List<ILegalBasisChangeRequest>, dossierId: string, fileId: string, includeUnprocessed = false) {
return this.legalBasisChange(body, dossierId, fileId, includeUnprocessed).pipe(this.#showToast('change-legal-basis'));
}
recategorizeRedactions(body: List<IRecategorizationRequest>, dossierId: string, fileId: string, includeUnprocessed = false) {
return this.recategorize(body, dossierId, fileId, includeUnprocessed).pipe(this.#showToast('change-type'));
recategorizeRedactions(
body: List<IRecategorizationRequest>,
dossierId: string,
fileId: string,
successMessageParameters?: {
[key: string]: string;
},
includeUnprocessed = false,
) {
return this.recategorize(body, dossierId, fileId, includeUnprocessed).pipe(
this.#showToast('recategorize-annotation', false, successMessageParameters),
);
}
addAnnotation(
@ -131,13 +137,6 @@ export class ManualRedactionService extends GenericService<IManualAddResponse> {
);
}
legalBasisChange(body: List<ILegalBasisChangeRequest>, dossierId: string, fileId: string, includeUnprocessed = false) {
return this._post(
body,
`${this.#bulkRedaction}/legalBasisChange/${dossierId}/${fileId}?includeUnprocessed=${includeUnprocessed}`,
).pipe(this.#log('Legal basis change', 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));
@ -165,7 +164,11 @@ export class ManualRedactionService extends GenericService<IManualAddResponse> {
});
}
#showToast(action: ManualRedactionActions | DictionaryActions, isDictionary = false) {
#showToast(
action: ManualRedactionActions | DictionaryActions,
isDictionary = false,
successMessageParameters?: { [key: string]: string },
) {
return pipe(
catchError((error: unknown) => {
const isConflict = (error as HttpErrorResponse).status === HttpStatusCode.Conflict;
@ -175,7 +178,12 @@ export class ManualRedactionService extends GenericService<IManualAddResponse> {
});
return EMPTY;
}),
tap(() => this._toaster.success(getMessage(action, isDictionary), { positionClass: 'toast-file-preview' })),
tap(() =>
this._toaster.success(getMessage(action, isDictionary), {
params: successMessageParameters,
positionClass: 'toast-file-preview',
}),
),
);
}

View File

@ -36,7 +36,7 @@ export class PdfAnnotationActionsService {
let acceptResizeButton: IHeaderElement;
if (this.#annotationManager.annotationHasBeenResized) {
acceptResizeButton = this.#getButton('check', _('annotation-actions.resize-accept.label'), () =>
this.#annotationActionsService.acceptResize(firstAnnotation),
this.#annotationActionsService.acceptResize(firstAnnotation, permissions),
);
} else {
acceptResizeButton = this.#getDisabledCheckButton();

View File

@ -113,6 +113,7 @@ export const getResizeRedactionOptions = (
isRss: boolean,
applyToAllDossiers: boolean,
isApprover: boolean,
canResizeInDictionary: boolean,
): DetailsRadioOption<ResizeRedactionOption>[] => {
const translations = resizeRedactionTranslations;
const options: DetailsRadioOption<ResizeRedactionOption>[] = [
@ -127,7 +128,7 @@ export const getResizeRedactionOptions = (
if (isRss) {
return options;
}
if (!redaction.hasBeenResizedLocally) {
if (canResizeInDictionary) {
const dictBasedType = redaction.isModifyDictionary;
options.push({
label: translations.inDossier.label,

View File

@ -46,6 +46,11 @@ export interface EditRedactResult {
export type AddHintResult = RedactTextResult;
export type AddAnnotationResult = RedactTextResult;
export interface ResizeRedactionPermissions {
canResizeAnnotation: boolean;
canResizeInDictionary: boolean;
}
export interface ResizeAnnotationData {
redaction: AnnotationWrapper;
text: string;
@ -55,6 +60,7 @@ export interface ResizeAnnotationData {
export interface ResizeRedactionData extends ResizeAnnotationData {
applyToAllDossiers?: boolean;
isApprover?: boolean;
permissions: ResizeRedactionPermissions;
}
export interface ResizeAnnotationResult {

View File

@ -26,7 +26,7 @@ export class EntityLogService extends GenericService<unknown> {
#filterInvalidEntries(entityLogEntry: IEntityLogEntry[]) {
return entityLogEntry.filter(entry => {
entry.positions = entry.positions.filter(p => !!p.rectangle?.length);
entry.positions = entry.positions?.filter(p => !!p.rectangle?.length);
const hasPositions = !!entry.positions?.length;
const isRemoved = entry.state === EntryStates.REMOVED;
if (!hasPositions) {

View File

@ -28,10 +28,6 @@ export const manualRedactionActionsTranslations: Record<ManualRedactionActions,
error: _('annotation-actions.message.manual-redaction.add.error'),
success: _('annotation-actions.message.manual-redaction.add.success'),
},
'change-legal-basis': {
error: _('annotation-actions.message.manual-redaction.change-legal-basis.error'),
success: _('annotation-actions.message.manual-redaction.change-legal-basis.success'),
},
'force-redaction': {
error: _('annotation-actions.message.manual-redaction.force-redaction.error'),
success: _('annotation-actions.message.manual-redaction.force-redaction.success'),
@ -44,9 +40,9 @@ export const manualRedactionActionsTranslations: Record<ManualRedactionActions,
error: _('annotation-actions.message.manual-redaction.recategorize-image.error'),
success: _('annotation-actions.message.manual-redaction.recategorize-image.success'),
},
'change-type': {
error: _('annotation-actions.message.manual-redaction.change-type.error'),
success: _('annotation-actions.message.manual-redaction.change-type.success'),
'recategorize-annotation': {
error: _('annotation-actions.message.manual-redaction.recategorize-annotation.error'),
success: _('annotation-actions.message.manual-redaction.recategorize-annotation.success'),
},
undo: {
error: _('annotation-actions.message.manual-redaction.undo.error'),

View File

@ -288,14 +288,6 @@
"error": "Fehler beim Speichern der Schwärzung: {error}",
"success": "Schwärzung hinzugefügt!"
},
"change-legal-basis": {
"error": "Fehler beim Bearbeiten der in der Anmerkung genannten Begründung: {error}",
"success": "In der Anmerkung genannte Begründung wurde bearbeitet."
},
"change-type": {
"error": "Failed to edit type: {error}",
"success": "Type was edited."
},
"force-hint": {
"error": "Failed to save hint: {error}",
"success": "Hint added!"
@ -304,6 +296,10 @@
"error": "Die Schwärzung konnte nicht gespeichert werden!",
"success": "Schwärzung eingefügt!"
},
"recategorize-annotation": {
"error": "Failed to edit type: {error}",
"success": "Annotation was edited: Changed {changes}."
},
"recategorize-image": {
"error": "Rekategorisierung des Bildes gescheitert: {error}",
"success": "Bild wurde einer neuen Kategorie zugeordnet."
@ -355,7 +351,7 @@
"annotation-engines": {
"dictionary": "{isHint, select, true{Hint} other{Redaction}} basierend auf Wörterbuch",
"imported": "Imported",
"manual": "",
"manual": "Manual",
"ner": "Redaktion basierend auf KI",
"rule": "Schwärzung basierend auf Regel {rule}"
},
@ -1985,7 +1981,6 @@
"reason": "Reason",
"reason-placeholder": "Select a reason...",
"revert-text": "Revert to selected text",
"selected-text": "Selected text:",
"type": "Type",
"type-placeholder": "Select type..."
},

View File

@ -288,14 +288,6 @@
"error": "Failed to save redaction: {error}",
"success": "Redaction added!"
},
"change-legal-basis": {
"error": "Failed to edit annotation reason: {error}",
"success": "Annotation reason was edited."
},
"change-type": {
"error": "Failed to edit type: {error}",
"success": "Type was edited."
},
"force-hint": {
"error": "Failed to save hint: {error}",
"success": "Hint added!"
@ -304,6 +296,10 @@
"error": "Failed to save redaction: {error}",
"success": "Redaction added!"
},
"recategorize-annotation": {
"error": "Failed to edit type: {error}",
"success": "Annotation was edited: Changed {changes}."
},
"recategorize-image": {
"error": "Failed to recategorize image: {error}",
"success": "Image recategorized."
@ -355,7 +351,6 @@
"annotation-engines": {
"dictionary": "Based on dictionary",
"imported": "Imported",
"manual": "Manual",
"ner": "Based on AI",
"rule": "Based on rule"
},
@ -1985,7 +1980,6 @@
"reason": "Reason",
"reason-placeholder": "Select a reason...",
"revert-text": "Revert to selected text",
"selected-text": "Selected text:",
"type": "Type",
"type-placeholder": "Select type..."
},

View File

@ -288,14 +288,6 @@
"error": "Fehler beim Speichern der Schwärzung: {error}",
"success": "Schwärzung hinzugefügt!"
},
"change-legal-basis": {
"error": "Fehler beim Bearbeiten der in der Anmerkung genannten Begründung: {error}",
"success": "In der Anmerkung genannte Begründung wurde bearbeitet."
},
"change-type": {
"error": "",
"success": ""
},
"force-hint": {
"error": "Failed to save hint: {error}",
"success": "Hint added!"
@ -304,6 +296,10 @@
"error": "Die Schwärzung konnte nicht gespeichert werden!",
"success": "Schwärzung eingefügt!"
},
"recategorize-annotation": {
"error": "",
"success": ""
},
"recategorize-image": {
"error": "Rekategorisierung des Bildes gescheitert: {error}",
"success": "Bild wurde einer neuen Kategorie zugeordnet."
@ -355,7 +351,7 @@
"annotation-engines": {
"dictionary": "{isHint, select, true{Hint} other{Redaction}} basierend auf Wörterbuch",
"imported": "Annotation is imported",
"manual": "",
"manual": "Manual",
"ner": "Redaktion basierend auf KI",
"rule": "Schwärzung basierend auf Regel {rule}"
},
@ -1985,7 +1981,6 @@
"reason": "Reason",
"reason-placeholder": "Select a reasons...",
"revert-text": "",
"selected-text": "Selected text:",
"type": "Type",
"type-placeholder": "Select type..."
},

View File

@ -288,14 +288,6 @@
"error": "Failed to save annotation: {error}",
"success": "Annotation added!"
},
"change-legal-basis": {
"error": "Failed to edit annotation reason: {error}",
"success": "Annotation reason was edited."
},
"change-type": {
"error": "",
"success": ""
},
"force-hint": {
"error": "Failed to save hint: {error}",
"success": "Hint added!"
@ -304,6 +296,10 @@
"error": "Failed to save annotation: {error}",
"success": "Annotation added!"
},
"recategorize-annotation": {
"error": "",
"success": ""
},
"recategorize-image": {
"error": "Failed to recategorize image: {error}",
"success": "Image recategorized."
@ -355,7 +351,6 @@
"annotation-engines": {
"dictionary": "{isHint, select, true{Hint} other{Annotation}} based on dictionary",
"imported": "Annotation is imported",
"manual": "Manual",
"ner": "Annotation based on AI",
"rule": "Annotation based on rule {rule}"
},
@ -1985,7 +1980,6 @@
"reason": "Reason",
"reason-placeholder": "Select a reasons...",
"revert-text": "",
"selected-text": "Selected text:",
"type": "Type",
"type-placeholder": "Select type..."
},

View File

@ -6,12 +6,11 @@ export type ManualRedactionActions =
| 'add'
| 'remove'
| 'remove-hint'
| 'change-legal-basis'
| 'recategorize-image'
| 'undo'
| 'force-redaction'
| 'force-hint'
| 'change-type';
| 'recategorize-annotation';
export const AnnotationIconTypes = {
square: 'square',

View File

@ -2,4 +2,7 @@ export interface IRecategorizationRequest {
readonly annotationId?: string;
readonly comment?: string;
readonly type?: string;
readonly legalBasis?: string;
readonly section?: string;
readonly value?: string;
}

View File

@ -19,84 +19,84 @@
"*.{ts,js,html}": "eslint --fix"
},
"dependencies": {
"@angular/animations": "17.0.5",
"@angular/cdk": "17.0.1",
"@angular/common": "17.0.5",
"@angular/compiler": "17.0.5",
"@angular/core": "17.0.5",
"@angular/forms": "17.0.5",
"@angular/material": "17.0.1",
"@angular/platform-browser": "17.0.5",
"@angular/platform-browser-dynamic": "17.0.5",
"@angular/router": "17.0.5",
"@angular/service-worker": "17.0.5",
"@angular/animations": "17.3.4",
"@angular/cdk": "17.3.4",
"@angular/common": "17.3.4",
"@angular/compiler": "17.3.4",
"@angular/core": "17.3.4",
"@angular/forms": "17.3.4",
"@angular/material": "17.3.4",
"@angular/platform-browser": "17.3.4",
"@angular/platform-browser-dynamic": "17.3.4",
"@angular/router": "17.3.4",
"@angular/service-worker": "17.3.4",
"@materia-ui/ngx-monaco-editor": "^6.0.0",
"@messageformat/core": "^3.3.0",
"@ngx-translate/core": "15.0.0",
"@ngx-translate/http-loader": "8.0.0",
"@pdftron/webviewer": "10.5.0",
"chart.js": "4.4.0",
"@pdftron/webviewer": "10.8.0",
"chart.js": "4.4.2",
"dayjs": "1.11.10",
"file-saver": "^2.0.5",
"jszip": "^3.10.1",
"jwt-decode": "^4.0.0",
"keycloak-angular": "15.0.0",
"keycloak-angular": "15.1.0",
"keycloak-js": "23.0.1",
"lodash-es": "^4.17.21",
"monaco-editor": "0.44.0",
"ng2-charts": "5.0.3",
"monaco-editor": "0.47.0",
"ng2-charts": "6.0.0",
"ngx-color-picker": "16.0.0",
"ngx-logger": "^5.0.11",
"ngx-toastr": "18.0.0",
"ngx-translate-messageformat-compiler": "6.5.0",
"ngx-translate-messageformat-compiler": "7.0.0",
"object-hash": "^3.0.0",
"papaparse": "^5.4.0",
"rxjs": "7.8.1",
"sass": "1.69.5",
"sass": "1.75.0",
"scroll-into-view-if-needed": "3.1.0",
"streamsaver": "^2.0.5",
"tslib": "2.6.2",
"zone.js": "0.14.2"
"zone.js": "0.14.4"
},
"devDependencies": {
"@angular-devkit/build-angular": "17.0.4",
"@angular-devkit/core": "17.0.4",
"@angular-devkit/schematics": "17.0.4",
"@angular-eslint/builder": "17.1.0",
"@angular-eslint/eslint-plugin": "17.1.0",
"@angular-eslint/eslint-plugin-template": "17.1.0",
"@angular-eslint/schematics": "17.1.0",
"@angular-eslint/template-parser": "17.1.0",
"@angular/cli": "17.0.4",
"@angular/compiler-cli": "17.0.5",
"@angular/language-service": "17.0.5",
"@angular-devkit/build-angular": "17.3.4",
"@angular-devkit/core": "17.3.4",
"@angular-devkit/schematics": "17.3.4",
"@angular-eslint/builder": "17.3.0",
"@angular-eslint/eslint-plugin": "17.3.0",
"@angular-eslint/eslint-plugin-template": "17.3.0",
"@angular-eslint/schematics": "17.3.0",
"@angular-eslint/template-parser": "17.3.0",
"@angular/cli": "17.3.4",
"@angular/compiler-cli": "17.3.4",
"@angular/language-service": "17.3.4",
"@bartholomej/ngx-translate-extract": "^8.0.2",
"@localazy/ts-api": "^1.0.0",
"@schematics/angular": "17.0.4",
"@schematics/angular": "17.3.4",
"@types/file-saver": "^2.0.7",
"@types/jest": "29.5.10",
"@types/jest": "29.5.12",
"@types/lodash-es": "4.17.12",
"@types/node": "20.10.0",
"@typescript-eslint/eslint-plugin": "^6.10.0",
"@typescript-eslint/parser": "^6.10.0",
"axios": "1.6.2",
"eslint": "^8.53.0",
"eslint-config-prettier": "9.0.0",
"eslint-plugin-prettier": "5.0.1",
"@types/node": "20.12.7",
"@typescript-eslint/eslint-plugin": "^7.2.0",
"@typescript-eslint/parser": "^7.2.0",
"axios": "1.6.8",
"eslint": "^8.57.0",
"eslint-config-prettier": "9.1.0",
"eslint-plugin-prettier": "5.1.3",
"eslint-plugin-rxjs": "^5.0.2",
"google-translate-api-browser": "^4.1.9",
"husky": "^8.0.3",
"google-translate-api-browser": "^5.0.0",
"husky": "^9.0.11",
"jest": "29.7.0",
"jest-environment-jsdom": "29.7.0",
"jest-extended": "4.0.2",
"jest-preset-angular": "13.1.4",
"lint-staged": "15.1.0",
"prettier": "3.1.0",
"sonarqube-scanner": "3.3.0",
"ts-node": "10.9.1",
"typescript": "5.2.2",
"webpack": "5.89.0",
"webpack-bundle-analyzer": "4.10.1",
"xliff": "^6.1.0"
"jest-preset-angular": "14.0.3",
"lint-staged": "15.2.2",
"prettier": "3.2.5",
"sonarqube-scanner": "3.4.0",
"ts-node": "10.9.2",
"typescript": "5.4.5",
"webpack": "5.91.0",
"webpack-bundle-analyzer": "4.10.2",
"xliff": "^6.2.1"
}
}

5215
yarn.lock

File diff suppressed because it is too large Load Diff