AAE-41699 Evaluate variables in form display text (#11620)

This commit is contained in:
David Olson
2026-02-09 15:40:31 -06:00
committed by GitHub
parent 14899931d2
commit 4d99262ba3
9 changed files with 1165 additions and 9 deletions
@@ -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);
});
});
});
@@ -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) {