mirror of
https://github.com/Alfresco/alfresco-ng2-components.git
synced 2026-09-09 18:03:21 +00:00
AAE-41699 Evaluate variables in form display text (#11620)
This commit is contained in:
+327
-3
@@ -20,13 +20,15 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { DisplayRichTextWidgetComponent, RICH_TEXT_PARSER_TOKEN } from './display-rich-text.widget';
|
||||
import { RichTextParserService } from '../../../services/rich-text-parser.service';
|
||||
import { FormFieldModel, FormModel } from '@alfresco/adf-core';
|
||||
import { ADF_DISPLAY_TEXT_SETTINGS, FormFieldModel, FormModel, FormService } from '@alfresco/adf-core';
|
||||
import { of } from 'rxjs';
|
||||
|
||||
describe('DisplayRichTextWidgetComponent', () => {
|
||||
let widget: DisplayRichTextWidgetComponent;
|
||||
let fixture: ComponentFixture<DisplayRichTextWidgetComponent>;
|
||||
let debugEl: DebugElement;
|
||||
let mockRichTextParserService: jasmine.SpyObj<RichTextParserService>;
|
||||
let formService: FormService;
|
||||
|
||||
const cssSelector = {
|
||||
parsedHTML: '.adf-display-rich-text-widget-parsed-html'
|
||||
@@ -89,11 +91,12 @@ describe('DisplayRichTextWidgetComponent', () => {
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [DisplayRichTextWidgetComponent],
|
||||
providers: [{ provide: RICH_TEXT_PARSER_TOKEN, useValue: mockRichTextParserService }]
|
||||
providers: [FormService, { provide: RICH_TEXT_PARSER_TOKEN, useValue: mockRichTextParserService }]
|
||||
});
|
||||
fixture = TestBed.createComponent(DisplayRichTextWidgetComponent);
|
||||
widget = fixture.componentInstance;
|
||||
debugEl = fixture.debugElement;
|
||||
formService = TestBed.inject(FormService);
|
||||
widget.field = fakeFormField;
|
||||
});
|
||||
|
||||
@@ -119,7 +122,7 @@ describe('DisplayRichTextWidgetComponent', () => {
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(mockRichTextParserService.parse).toHaveBeenCalledWith(fakeFormField.value);
|
||||
expect(mockRichTextParserService.parse).toHaveBeenCalledTimes(1);
|
||||
expect(mockRichTextParserService.parse).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should parse editorjs data to html', async () => {
|
||||
@@ -142,4 +145,325 @@ describe('DisplayRichTextWidgetComponent', () => {
|
||||
const parsedHtmlEl = debugEl.query(By.css(cssSelector.parsedHTML));
|
||||
expect(parsedHtmlEl.nativeElement.innerHTML.includes('<img src="x" onerror="alert(\'XSS\')">')).toBe(false);
|
||||
});
|
||||
|
||||
describe('expression evaluation', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.resetTestingModule();
|
||||
TestBed.configureTestingModule({
|
||||
imports: [DisplayRichTextWidgetComponent],
|
||||
providers: [
|
||||
FormService,
|
||||
{ provide: RICH_TEXT_PARSER_TOKEN, useValue: mockRichTextParserService },
|
||||
{
|
||||
provide: ADF_DISPLAY_TEXT_SETTINGS,
|
||||
useValue: { enableExpressionEvaluation: true }
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
fixture = TestBed.createComponent(DisplayRichTextWidgetComponent);
|
||||
widget = fixture.componentInstance;
|
||||
debugEl = fixture.debugElement;
|
||||
formService = TestBed.inject(FormService);
|
||||
mockRichTextParserService = TestBed.inject(RICH_TEXT_PARSER_TOKEN) as jasmine.SpyObj<RichTextParserService>;
|
||||
});
|
||||
|
||||
it('should resolve field expressions in rich text blocks', () => {
|
||||
const form = new FormModel({
|
||||
fields: [
|
||||
{
|
||||
id: 'richText1',
|
||||
type: 'display-rich-text',
|
||||
value: {
|
||||
time: 1658154611110,
|
||||
blocks: [
|
||||
{
|
||||
id: '1',
|
||||
type: 'paragraph',
|
||||
data: {
|
||||
text: 'Hello ${field.name}'
|
||||
}
|
||||
}
|
||||
],
|
||||
version: 1
|
||||
}
|
||||
},
|
||||
{ id: 'name', type: 'text', value: 'John' }
|
||||
]
|
||||
});
|
||||
|
||||
widget.field = form.getFieldById('richText1');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(widget.field.value.blocks[0].data.text).toBe('Hello John');
|
||||
});
|
||||
|
||||
it('should resolve expressions in multiple blocks', () => {
|
||||
const form = new FormModel({
|
||||
fields: [
|
||||
{
|
||||
id: 'richText1',
|
||||
type: 'display-rich-text',
|
||||
value: {
|
||||
time: 1658154611110,
|
||||
blocks: [
|
||||
{
|
||||
id: '1',
|
||||
type: 'header',
|
||||
data: {
|
||||
text: 'User: ${field.firstName}',
|
||||
level: 1
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
type: 'paragraph',
|
||||
data: {
|
||||
text: 'Status: ${variable.status}'
|
||||
}
|
||||
}
|
||||
],
|
||||
version: 1
|
||||
}
|
||||
},
|
||||
{ id: 'firstName', type: 'text', value: 'Jane' }
|
||||
],
|
||||
variables: [{ id: 'status', name: 'status', type: 'string', value: 'Active' }]
|
||||
});
|
||||
|
||||
widget.field = form.getFieldById('richText1');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(widget.field.value.blocks[0].data.text).toBe('User: Jane');
|
||||
expect(widget.field.value.blocks[1].data.text).toBe('Status: Active');
|
||||
});
|
||||
|
||||
it('should update rich text when dependent field value changes', (done) => {
|
||||
const form = new FormModel({
|
||||
fields: [
|
||||
{
|
||||
id: 'richText1',
|
||||
type: 'display-rich-text',
|
||||
value: {
|
||||
time: 1658154611110,
|
||||
blocks: [
|
||||
{
|
||||
id: '1',
|
||||
type: 'paragraph',
|
||||
data: {
|
||||
text: 'Hello ${field.name}'
|
||||
}
|
||||
}
|
||||
],
|
||||
version: 1
|
||||
}
|
||||
},
|
||||
{ id: 'name', type: 'text', value: 'John' }
|
||||
]
|
||||
});
|
||||
|
||||
widget.field = form.getFieldById('richText1');
|
||||
const nameField = form.getFieldById('name');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(widget.field.value.blocks[0].data.text).toBe('Hello John');
|
||||
|
||||
nameField.value = 'Jane';
|
||||
formService.formRulesEvent.next({ type: 'fieldValueChanged', field: nameField } as any);
|
||||
|
||||
setTimeout(() => {
|
||||
expect(widget.field.value.blocks[0].data.text).toBe('Hello Jane');
|
||||
done();
|
||||
}, 350);
|
||||
});
|
||||
|
||||
it('should preserve original value structure for re-evaluation', () => {
|
||||
const form = new FormModel({
|
||||
fields: [
|
||||
{
|
||||
id: 'richText1',
|
||||
type: 'display-rich-text',
|
||||
value: {
|
||||
time: 1658154611110,
|
||||
blocks: [
|
||||
{
|
||||
id: '1',
|
||||
type: 'paragraph',
|
||||
data: {
|
||||
text: 'Hello ${field.name}'
|
||||
}
|
||||
}
|
||||
],
|
||||
version: 1
|
||||
}
|
||||
},
|
||||
{ id: 'name', type: 'text', value: 'John' }
|
||||
]
|
||||
});
|
||||
|
||||
widget.field = form.getFieldById('richText1');
|
||||
fixture.detectChanges();
|
||||
|
||||
const originalValue = JSON.parse(widget['originalFieldValue']);
|
||||
expect(originalValue.blocks[0].data.text).toBe('Hello ${field.name}');
|
||||
});
|
||||
|
||||
it('should handle missing field references with empty string', () => {
|
||||
const form = new FormModel({
|
||||
fields: [
|
||||
{
|
||||
id: 'richText1',
|
||||
type: 'display-rich-text',
|
||||
value: {
|
||||
time: 1658154611110,
|
||||
blocks: [
|
||||
{
|
||||
id: '1',
|
||||
type: 'paragraph',
|
||||
data: {
|
||||
text: 'Hello ${field.nonExistent}'
|
||||
}
|
||||
}
|
||||
],
|
||||
version: 1
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
widget.field = form.getFieldById('richText1');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(widget.field.value.blocks[0].data.text).toBe('Hello ');
|
||||
});
|
||||
|
||||
it('should not resolve expressions when enableExpressionEvaluation is false', () => {
|
||||
TestBed.resetTestingModule();
|
||||
TestBed.configureTestingModule({
|
||||
imports: [DisplayRichTextWidgetComponent],
|
||||
providers: [
|
||||
FormService,
|
||||
{ provide: RICH_TEXT_PARSER_TOKEN, useValue: mockRichTextParserService },
|
||||
{
|
||||
provide: ADF_DISPLAY_TEXT_SETTINGS,
|
||||
useValue: { enableExpressionEvaluation: false }
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
fixture = TestBed.createComponent(DisplayRichTextWidgetComponent);
|
||||
widget = fixture.componentInstance;
|
||||
|
||||
const form = new FormModel({
|
||||
fields: [
|
||||
{
|
||||
id: 'richText1',
|
||||
type: 'display-rich-text',
|
||||
value: {
|
||||
time: 1658154611110,
|
||||
blocks: [
|
||||
{
|
||||
id: '1',
|
||||
type: 'paragraph',
|
||||
data: {
|
||||
text: 'Hello ${field.name}'
|
||||
}
|
||||
}
|
||||
],
|
||||
version: 1
|
||||
}
|
||||
},
|
||||
{ id: 'name', type: 'text', value: 'John' }
|
||||
]
|
||||
});
|
||||
|
||||
widget.field = form.getFieldById('richText1');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(widget.field.value.blocks[0].data.text).toBe('Hello ${field.name}');
|
||||
});
|
||||
|
||||
it('should re-parse HTML after expressions are evaluated', () => {
|
||||
mockRichTextParserService.parse.and.returnValue('<p>Test HTML</p>');
|
||||
|
||||
const form = new FormModel({
|
||||
fields: [
|
||||
{
|
||||
id: 'richText1',
|
||||
type: 'display-rich-text',
|
||||
value: {
|
||||
time: 1658154611110,
|
||||
blocks: [
|
||||
{
|
||||
id: '1',
|
||||
type: 'paragraph',
|
||||
data: {
|
||||
text: 'Hello ${field.name}'
|
||||
}
|
||||
}
|
||||
],
|
||||
version: 1
|
||||
}
|
||||
},
|
||||
{ id: 'name', type: 'text', value: 'John' }
|
||||
]
|
||||
});
|
||||
|
||||
widget.field = form.getFieldById('richText1');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(mockRichTextParserService.parse).toHaveBeenCalled();
|
||||
const lastCall = mockRichTextParserService.parse.calls.mostRecent();
|
||||
expect(lastCall.args[0].blocks[0].data.text).toBe('Hello John');
|
||||
});
|
||||
|
||||
it('should support observable settings', (done) => {
|
||||
TestBed.resetTestingModule();
|
||||
TestBed.configureTestingModule({
|
||||
imports: [DisplayRichTextWidgetComponent],
|
||||
providers: [
|
||||
FormService,
|
||||
{ provide: RICH_TEXT_PARSER_TOKEN, useValue: mockRichTextParserService },
|
||||
{
|
||||
provide: ADF_DISPLAY_TEXT_SETTINGS,
|
||||
useValue: of({ enableExpressionEvaluation: true })
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
fixture = TestBed.createComponent(DisplayRichTextWidgetComponent);
|
||||
widget = fixture.componentInstance;
|
||||
|
||||
const form = new FormModel({
|
||||
fields: [
|
||||
{
|
||||
id: 'richText1',
|
||||
type: 'display-rich-text',
|
||||
value: {
|
||||
time: 1658154611110,
|
||||
blocks: [
|
||||
{
|
||||
id: '1',
|
||||
type: 'paragraph',
|
||||
data: {
|
||||
text: 'Hello ${field.name}'
|
||||
}
|
||||
}
|
||||
],
|
||||
version: 1
|
||||
}
|
||||
},
|
||||
{ id: 'name', type: 'text', value: 'John' }
|
||||
]
|
||||
});
|
||||
|
||||
widget.field = form.getFieldById('richText1');
|
||||
fixture.detectChanges();
|
||||
|
||||
setTimeout(() => {
|
||||
expect(widget.field.value.blocks[0].data.text).toBe('Hello John');
|
||||
done();
|
||||
}, 100);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+49
-3
@@ -17,9 +17,10 @@
|
||||
|
||||
/* eslint-disable @angular-eslint/component-selector */
|
||||
|
||||
import { Component, inject, InjectionToken, OnInit, SecurityContext, ViewEncapsulation } from '@angular/core';
|
||||
import { WidgetComponent } from '@alfresco/adf-core';
|
||||
import { Component, inject, InjectionToken, OnDestroy, OnInit, SecurityContext, ViewEncapsulation } from '@angular/core';
|
||||
import { BaseDisplayTextWidgetComponent } from '@alfresco/adf-core';
|
||||
import { DomSanitizer } from '@angular/platform-browser';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { RichTextParserService } from '../../../services/rich-text-parser.service';
|
||||
|
||||
export const RICH_TEXT_PARSER_TOKEN = new InjectionToken<RichTextParserService>('RichTextParserService', {
|
||||
@@ -43,13 +44,58 @@ export const RICH_TEXT_PARSER_TOKEN = new InjectionToken<RichTextParserService>(
|
||||
},
|
||||
encapsulation: ViewEncapsulation.None
|
||||
})
|
||||
export class DisplayRichTextWidgetComponent extends WidgetComponent implements OnInit {
|
||||
export class DisplayRichTextWidgetComponent extends BaseDisplayTextWidgetComponent implements OnInit, OnDestroy {
|
||||
parsedHTML: string | Error;
|
||||
|
||||
private readonly richTextParserService = inject(RICH_TEXT_PARSER_TOKEN);
|
||||
private readonly sanitizer = inject(DomSanitizer);
|
||||
private fieldChangedSubscription?: Subscription;
|
||||
|
||||
ngOnInit(): void {
|
||||
this.parseAndSanitize();
|
||||
|
||||
// Re-parse when field changes (after expressions are evaluated)
|
||||
this.fieldChangedSubscription = this.fieldChanged.subscribe(() => {
|
||||
this.parseAndSanitize();
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.fieldChangedSubscription?.unsubscribe();
|
||||
}
|
||||
|
||||
protected storeOriginalValue(): void {
|
||||
if (this.field) {
|
||||
this.originalFieldValue = JSON.stringify(this.field.value);
|
||||
}
|
||||
}
|
||||
|
||||
protected evaluateExpressions(): void {
|
||||
if (!this.field) {
|
||||
return;
|
||||
}
|
||||
|
||||
const value = JSON.parse(JSON.stringify(this.field.value));
|
||||
this.applyExpressionsToBlocks(value);
|
||||
}
|
||||
|
||||
protected reevaluateExpressions(): void {
|
||||
if (!this.field || !this.originalFieldValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
const value = JSON.parse(this.originalFieldValue);
|
||||
this.applyExpressionsToBlocks(value);
|
||||
}
|
||||
|
||||
private applyExpressionsToBlocks(value: any): void {
|
||||
for (const block of value.blocks) {
|
||||
block.data.text = this.resolveExpressions(block.data.text);
|
||||
}
|
||||
this.field.value = value;
|
||||
}
|
||||
|
||||
private parseAndSanitize(): void {
|
||||
this.parsedHTML = this.richTextParserService.parse(this.field.value);
|
||||
|
||||
if (this.parsedHTML instanceof Error) {
|
||||
|
||||
Reference in New Issue
Block a user