RED-5546: fix escaped html

This commit is contained in:
Dan Percic 2023-02-11 20:27:17 +02:00
parent 048fc24b6e
commit 32a2607e23
6 changed files with 134 additions and 145 deletions

View File

@ -1,12 +1,5 @@
<section class="dialog">
<div
[translateParams]="{
type: dossierTemplate ? (data.clone ? 'clone' : 'edit') : 'create',
name: dossierTemplate?.name
}"
[translate]="'add-edit-clone-dossier-template.title'"
class="dialog-header heading-l"
></div>
<div [innerHTML]="'add-edit-clone-dossier-template.title' | translate : translateParams" class="dialog-header heading-l"></div>
<form [formGroup]="form">
<div class="dialog-content">

View File

@ -1,14 +1,13 @@
import { Component, Inject } from '@angular/core';
import { AbstractControl, UntypedFormGroup, Validators } from '@angular/forms';
import { AbstractControl, Validators } from '@angular/forms';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { applyIntervalConstraints } from '@utils/date-inputs-utils';
import { downloadTypesTranslations } from '@translations/download-types-translations';
import { DossierTemplatesService } from '@services/dossier-templates/dossier-templates.service';
import { BaseDialogComponent } from '@iqser/common-ui';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { DossierTemplate, DownloadFileType, IDossierTemplate } from '@red/domain';
import { DossierTemplate, IDossierTemplate } from '@red/domain';
import { HttpStatusCode } from '@angular/common/http';
import { firstValueFrom } from 'rxjs';
import dayjs, { Dayjs } from 'dayjs';
import { ROLES } from '@users/roles';
@ -17,6 +16,11 @@ interface EditCloneTemplateData {
clone?: boolean;
}
const downloadTypes = ['ORIGINAL', 'PREVIEW', 'DELTA_PREVIEW', 'REDACTED'].map(type => ({
key: type,
label: downloadTypesTranslations[type],
}));
@Component({
templateUrl: './add-edit-clone-dossier-template-dialog.component.html',
styleUrls: ['./add-edit-clone-dossier-template-dialog.component.scss'],
@ -25,11 +29,7 @@ export class AddEditCloneDossierTemplateDialogComponent extends BaseDialogCompon
readonly roles = ROLES;
hasValidFrom: boolean;
hasValidTo: boolean;
downloadTypesEnum: DownloadFileType[] = ['ORIGINAL', 'PREVIEW', 'DELTA_PREVIEW', 'REDACTED'];
downloadTypes: { key: DownloadFileType; label: string }[] = this.downloadTypesEnum.map(type => ({
key: type,
label: downloadTypesTranslations[type],
}));
readonly downloadTypes = downloadTypes;
readonly dossierTemplate: DossierTemplate;
private _previousValidFrom: Dayjs;
private _previousValidTo: Dayjs;
@ -59,6 +59,13 @@ export class AddEditCloneDossierTemplateDialogComponent extends BaseDialogCompon
return !this.valid;
}
get translateParams() {
return {
type: this.dossierTemplate ? (this.data.clone ? 'clone' : 'edit') : 'create',
name: this.dossierTemplate?.name,
};
}
toggleHasValid(extremity: string) {
if (extremity === 'from') {
this.hasValidFrom = !this.hasValidFrom;
@ -72,20 +79,21 @@ export class AddEditCloneDossierTemplateDialogComponent extends BaseDialogCompon
async save() {
this._loadingService.start();
const dossierTemplate = {
dossierTemplateId: this.dossierTemplate?.dossierTemplateId,
...this.form.getRawValue(),
validFrom: this.hasValidFrom ? this.form.get('validFrom').value : null,
validTo: this.hasValidTo ? this.form.get('validTo').value : null,
} as IDossierTemplate;
try {
const dossierTemplate = {
dossierTemplateId: this.dossierTemplate?.dossierTemplateId,
...this.form.getRawValue(),
validFrom: this.hasValidFrom ? this.form.get('validFrom').value : null,
validTo: this.hasValidTo ? this.form.get('validTo').value : null,
} as IDossierTemplate;
if (this.data?.clone) {
await firstValueFrom(this._dossierTemplatesService.clone(this.dossierTemplate.id, dossierTemplate));
await this._dossierTemplatesService.clone(this.dossierTemplate.id, dossierTemplate);
} else {
await firstValueFrom(this._dossierTemplatesService.createOrUpdate(dossierTemplate));
await this._dossierTemplatesService.createOrUpdate(dossierTemplate);
}
this._dialogRef.close(true);
} catch (error: any) {
} catch (error) {
const message =
error.status === HttpStatusCode.Conflict
? _('add-edit-clone-dossier-template.error.conflict')
@ -105,7 +113,7 @@ export class AddEditCloneDossierTemplateDialogComponent extends BaseDialogCompon
this._lastValidTo = this._previousValidTo || this._lastValidTo;
}
private _getForm(): UntypedFormGroup {
private _getForm() {
return this._formBuilder.group({
name: [this._getCloneName(), Validators.required],
description: [this.dossierTemplate?.description],
@ -122,29 +130,28 @@ export class AddEditCloneDossierTemplateDialogComponent extends BaseDialogCompon
}
private _getCloneName(): string {
if (this.data?.clone) {
const templateName = this.dossierTemplate.name.trim();
let nameOfClonedTemplate: string = templateName.split('Copy of ').filter(n => n)[0];
nameOfClonedTemplate = nameOfClonedTemplate.split(/\(\s*\d+\s*\)$/)[0].trim();
const allTemplatesNames = this._dossierTemplatesService.all.map(t => t.name);
let clonesCount = 0;
for (const name of allTemplatesNames) {
const splitName = name.split(nameOfClonedTemplate);
const suffixRegExp = new RegExp(/^\(\s*\d+\s*\)$/);
if (splitName[0] === 'Copy of ' && (splitName[1].trim().match(suffixRegExp) || splitName[1] === '')) {
clonesCount++;
}
}
if (clonesCount >= 1) {
return `Copy of ${nameOfClonedTemplate} (${clonesCount})`;
}
return `Copy of ${nameOfClonedTemplate}`;
if (!this.data?.clone) {
return this.dossierTemplate?.name;
}
return this.dossierTemplate?.name;
const templateName = this.dossierTemplate.name.trim();
let nameOfClonedTemplate: string = templateName.split('Copy of ').filter(n => n)[0];
nameOfClonedTemplate = nameOfClonedTemplate.split(/\(\s*\d+\s*\)$/)[0].trim();
const allTemplatesNames = this._dossierTemplatesService.all.map(t => t.name);
let clonesCount = 0;
for (const name of allTemplatesNames) {
const splitName = name.split(nameOfClonedTemplate);
const suffixRegExp = new RegExp(/^\(\s*\d+\s*\)$/);
if (splitName[0] === 'Copy of ' && (splitName[1].trim().match(suffixRegExp) || splitName[1] === '')) {
clonesCount++;
}
}
if (clonesCount >= 1) {
return `Copy of ${nameOfClonedTemplate} (${clonesCount})`;
}
return `Copy of ${nameOfClonedTemplate}`;
}
private _requiredIfValidator(predicate) {

View File

@ -1,6 +1,6 @@
<div
*ngIf="(changes$ | async).length && ((isSelected$ | async) === false || (multiSelectService.inactive$ | async))"
[matTooltip]="changesTooltip$ | async"
*ngIf="(noSelection$ | async) && changesTooltip$ | async as changesTooltip"
[matTooltip]="changesTooltip"
class="chip"
matTooltipClass="multiline"
matTooltipPosition="above"
@ -8,34 +8,22 @@
<mat-icon [svgIcon]="'red:redaction-changes'"></mat-icon>
</div>
<ng-container *ngIf="engines$ | async as engines">
<ng-container *ngIf="engines.length && ((isSelected$ | async) === false || (multiSelectService.inactive$ | async))">
<div
#trigger="cdkOverlayOrigin"
(mouseout)="isPopoverOpen = false"
(mouseover)="isPopoverOpen = true"
cdkOverlayOrigin
class="chip"
>
<ng-container *ngFor="let engine of engines">
<mat-icon *ngIf="engine.show" [svgIcon]="engine.icon"></mat-icon>
</ng-container>
</div>
<ng-container *ngIf="(noSelection$ | async) && engines$ | async as engines">
<div #trigger="cdkOverlayOrigin" (mouseout)="isPopoverOpen = false" (mouseover)="isPopoverOpen = true" cdkOverlayOrigin class="chip">
<mat-icon *ngFor="let engine of engines" [svgIcon]="engine.icon"></mat-icon>
</div>
<ng-template
[cdkConnectedOverlayOffsetY]="-8"
[cdkConnectedOverlayOpen]="isPopoverOpen"
[cdkConnectedOverlayOrigin]="trigger"
cdkConnectedOverlay
>
<div class="popover">
<ng-container *ngFor="let engine of engines">
<div *ngIf="engine.show" class="flex-align-items-center">
<mat-icon [svgIcon]="engine.icon"></mat-icon>
<span>{{ engine.description | translate: engine.translateParams }}</span>
</div>
</ng-container>
<ng-template
[cdkConnectedOverlayOffsetY]="-8"
[cdkConnectedOverlayOpen]="isPopoverOpen"
[cdkConnectedOverlayOrigin]="trigger"
cdkConnectedOverlay
>
<div class="popover">
<div *ngFor="let engine of engines" class="flex-align-items-center">
<mat-icon [svgIcon]="engine.icon"></mat-icon>
<span [innerHTML]="engine.description | translate : engine.translateParams"></span>
</div>
</ng-template>
</ng-container>
</div>
</ng-template>
</ng-container>

View File

@ -1,12 +1,11 @@
import { ChangeDetectionStrategy, Component, Input, OnChanges } from '@angular/core';
import { Component, Input, OnChanges } from '@angular/core';
import { AnnotationWrapper } from '@models/file/annotation.wrapper';
import { TranslateService } from '@ngx-translate/core';
import { annotationChangesTranslations } from '@translations/annotation-changes-translations';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import { MultiSelectService } from '../../services/multi-select.service';
import { KeysOf } from '@iqser/common-ui';
import { BehaviorSubject, Observable } from 'rxjs';
import { filter, map, switchMap } from 'rxjs/operators';
import { KeysOf, shareDistinctLast } from '@iqser/common-ui';
import { BehaviorSubject, combineLatest, filter, map, Observable, switchMap } from 'rxjs';
import { AnnotationsListingService } from '../../services/annotations-listing.service';
interface Engine {
@ -24,94 +23,92 @@ const Engines = {
type EngineName = keyof typeof Engines;
function isBasedOn(annotation: AnnotationWrapper, engineName: EngineName) {
return !!annotation.engines?.includes(engineName);
}
const changesProperties: KeysOf<AnnotationWrapper>[] = [
'hasBeenResized',
'hasBeenRecategorized',
'hasLegalBasisChanged',
'hasBeenRemovedByManualOverride',
'hasBeenForcedRedaction',
'hasBeenForcedHint',
];
@Component({
selector: 'redaction-annotation-details [annotation]',
templateUrl: './annotation-details.component.html',
styleUrls: ['./annotation-details.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AnnotationDetailsComponent implements OnChanges {
@Input() annotation: AnnotationWrapper;
readonly isSelected$: Observable<boolean>;
readonly noSelection$: Observable<boolean>;
isPopoverOpen = false;
readonly engines$: Observable<Engine[]>;
readonly changes$: Observable<string[]>;
readonly changesTooltip$: Observable<string>;
private readonly _annotationChanged$ = new BehaviorSubject<AnnotationWrapper>(undefined);
readonly #annotationChanged$ = new BehaviorSubject<AnnotationWrapper>(undefined);
constructor(
private readonly _translateService: TranslateService,
private readonly _listingService: AnnotationsListingService,
readonly multiSelectService: MultiSelectService,
) {
this.isSelected$ = this._annotationChanged$.pipe(switchMap(annotation => this._listingService.isSelected$(annotation)));
const isSelected$ = this.#annotationChanged$.pipe(switchMap(annotation => this._listingService.isSelected$(annotation)));
this.noSelection$ = combineLatest([isSelected$, multiSelectService.inactive$]).pipe(
map(([isSelected, inactive]) => !isSelected || inactive),
shareDistinctLast(),
);
this.engines$ = this.#engines$;
this.changes$ = this.#changes$;
this.changesTooltip$ = this.#changesTooltip;
}
get #engines$(): Observable<Engine[]> {
return this._annotationChanged$.pipe(
return this.#annotationChanged$.pipe(
filter(annotation => !!annotation),
map(annotation =>
[
{
icon: 'red:dictionary',
description: _('annotation-engines.dictionary'),
show: AnnotationDetailsComponent._isBasedOn(annotation, Engines.DICTIONARY),
translateParams: { isHint: this.annotation.hint },
},
{
icon: 'red:ai',
description: _('annotation-engines.ner'),
show: AnnotationDetailsComponent._isBasedOn(annotation, Engines.NER),
},
{
icon: 'red:rule',
description: _('annotation-engines.rule'),
show: AnnotationDetailsComponent._isBasedOn(annotation, Engines.RULE),
translateParams: { rule: this.annotation.legalBasisValue || '' },
},
].filter(engine => engine.show),
),
map(annotation => this.#extractEngines(annotation).filter(engine => engine.show)),
);
}
get #changes$(): Observable<string[]> {
return this._annotationChanged$.pipe(
get #changesTooltip(): Observable<string | undefined> {
return this.#annotationChanged$.pipe(
filter(annotation => !!annotation),
map(annotation => {
const changesProperties: KeysOf<AnnotationWrapper>[] = [
'hasBeenResized',
'hasBeenRecategorized',
'hasLegalBasisChanged',
'hasBeenRemovedByManualOverride',
'hasBeenForcedRedaction',
'hasBeenForcedHint',
];
return changesProperties.filter(key => annotation[key]);
}),
);
}
get #changesTooltip(): Observable<string> {
return this.changes$.pipe(
map(annotation => changesProperties.filter(key => annotation[key])),
map(changes => {
if (!changes.length) {
return;
}
const header = this._translateService.instant(_('annotation-changes.header'));
const details = changes
.map(change => this._translateService.instant(annotationChangesTranslations[change]))
.map(change => `${change}`);
return [header, ...details].join('\n');
const details = changes.map(change => this._translateService.instant(annotationChangesTranslations[change]));
return [header, ...details.map(change => `${change}`)].join('\n');
}),
);
}
private static _isBasedOn(annotation: AnnotationWrapper, engineName: EngineName) {
return !!annotation.engines?.includes(engineName);
}
ngOnChanges() {
this._annotationChanged$.next(this.annotation);
this.#annotationChanged$.next(this.annotation);
}
#extractEngines(annotation: AnnotationWrapper): Engine[] {
return [
{
icon: 'red:dictionary',
description: _('annotation-engines.dictionary'),
show: isBasedOn(annotation, Engines.DICTIONARY),
translateParams: { isHint: annotation.hint },
},
{
icon: 'red:ai',
description: _('annotation-engines.ner'),
show: isBasedOn(annotation, Engines.NER),
},
{
icon: 'red:rule',
description: _('annotation-engines.rule'),
show: isBasedOn(annotation, Engines.RULE),
translateParams: { rule: annotation.legalBasisValue || '' },
},
];
}
}

View File

@ -1,7 +1,9 @@
<section *ngIf="dossier$ | async as dossier" class="dialog">
<div class="dialog-header heading-l" id="editDossierHeader">
{{ 'edit-dossier-dialog.header' | translate : { dossierName: dossier.dossierName } }}
</div>
<div
[innerHTML]="'edit-dossier-dialog.header' | translate : { dossierName: dossier.dossierName }"
class="dialog-header heading-l"
id="editDossierHeader"
></div>
<div class="dialog-content">
<iqser-side-nav [title]="'edit-dossier-dialog.side-nav-title' | translate">

View File

@ -1,7 +1,7 @@
import { EntitiesService, List, mapEach, RequiredParam, Toaster, Validate } from '@iqser/common-ui';
import { DossierTemplate, IDossierTemplate } from '@red/domain';
import { Injectable } from '@angular/core';
import { forkJoin, Observable, of } from 'rxjs';
import { firstValueFrom, forkJoin, Observable, of } from 'rxjs';
import { FileAttributesService } from '../entity-services/file-attributes.service';
import { catchError, map, mapTo, switchMap, tap } from 'rxjs/operators';
import { DossierTemplateStatsService } from '../entity-services/dossier-template-stats.service';
@ -73,12 +73,14 @@ export class DossierTemplatesService extends EntitiesService<IDossierTemplate, D
}
@Validate()
createOrUpdate(@RequiredParam() body: IDossierTemplate) {
return this._post(body).pipe(switchMap(() => this.loadAll()));
async createOrUpdate(@RequiredParam() body: IDossierTemplate) {
await firstValueFrom(this._post(body));
return await firstValueFrom(this.loadAll());
}
clone(dossierTemplateId: string, body: IDossierTemplate) {
return this._post(body, `${this._defaultModelPath}/${dossierTemplateId}/clone`).pipe(switchMap(() => this.loadAll()));
async clone(dossierTemplateId: string, body: IDossierTemplate) {
await firstValueFrom(this._post(body, `${this._defaultModelPath}/${dossierTemplateId}/clone`));
return await firstValueFrom(this.loadAll());
}
refreshDossierTemplate(dossierTemplateId: string): Observable<any> {