fix(n8n Form Node): Resolve expressions in HTML fields (#13755)

This commit is contained in:
Michael Kret
2025-03-11 05:40:58 +02:00
committed by GitHub
parent 4fe249580a
commit de23ae5558
5 changed files with 80 additions and 32 deletions

View File

@@ -336,15 +336,7 @@ export class Form extends Node {
});
}
} else {
fields = (context.getNodeParameter('formFields.values', []) as FormFieldsParameter).map(
(field) => {
if (field.fieldType === 'hiddenField') {
field.fieldLabel = field.fieldName as string;
}
return field;
},
);
fields = context.getNodeParameter('formFields.values', []) as FormFieldsParameter;
}
const method = context.getRequestObject().method;

View File

@@ -6,7 +6,7 @@ import {
type IWebhookResponseData,
} from 'n8n-workflow';
import { renderForm, sanitizeHtml } from './utils';
import { renderForm } from './utils';
export const renderFormNode = async (
context: IWebhookFunctions,
@@ -42,12 +42,6 @@ export const renderFormNode = async (
) as string) || 'Submit';
}
for (const field of fields) {
if (field.fieldType === 'html') {
field.html = sanitizeHtml(field.html as string);
}
}
const appendAttribution = context.evaluateExpression(
`{{ $('${trigger?.name}').params.options?.appendAttribution === false ? false : true }}`,
) as boolean;

View File

@@ -1,4 +1,5 @@
import { type Response } from 'express';
import type { MockProxy } from 'jest-mock-extended';
import { mock } from 'jest-mock-extended';
import {
type FormFieldsParameter,
@@ -9,10 +10,19 @@ import {
import { renderFormNode } from '../formNodeUtils';
describe('formNodeUtils', () => {
let webhookFunctions: MockProxy<IWebhookFunctions>;
beforeEach(() => {
webhookFunctions = mock<IWebhookFunctions>();
});
afterEach(() => {
jest.clearAllMocks();
});
it('should sanitize custom html', async () => {
const executeFunctions = mock<IWebhookFunctions>();
executeFunctions.getNode.mockReturnValue({ typeVersion: 2.1 } as any);
executeFunctions.getNodeParameter.calledWith('options').mockReturnValue({
webhookFunctions.getNode.mockReturnValue({ typeVersion: 2.1 } as any);
webhookFunctions.getNodeParameter.calledWith('options').mockReturnValue({
formTitle: 'Test Title',
formDescription: 'Test Description',
buttonLabel: 'Test Button Label',
@@ -47,12 +57,12 @@ describe('formNodeUtils', () => {
},
];
executeFunctions.getNodeParameter.calledWith('formFields.values').mockReturnValue(formFields);
webhookFunctions.getNodeParameter.calledWith('formFields.values').mockReturnValue(formFields);
const responseMock = mock<Response>({ render: mockRender } as any);
const triggerMock = mock<NodeTypeAndVersion>({ name: 'triggerName' } as any);
await renderFormNode(executeFunctions, responseMock, triggerMock, formFields, 'test');
await renderFormNode(webhookFunctions, responseMock, triggerMock, formFields, 'test');
expect(mockRender).toHaveBeenCalledWith('form-trigger', {
appendAttribution: true,

View File

@@ -17,6 +17,7 @@ import {
isFormConnected,
sanitizeHtml,
validateResponseModeConfiguration,
prepareFormFields,
} from '../utils';
describe('FormTrigger, parseFormDescription', () => {
@@ -994,4 +995,40 @@ describe('validateResponseModeConfiguration', () => {
expect(() => validateResponseModeConfiguration(webhookFunctions)).not.toThrow();
});
describe('prepareFormFields', () => {
it('should resolve expressions in html fields', async () => {
webhookFunctions.evaluateExpression.mockImplementation((expression) => {
if (expression === '{{ $json.formMode }}') {
return 'Title';
}
});
const result = prepareFormFields(webhookFunctions, [
{
fieldLabel: 'Custom HTML',
fieldType: 'html',
elementName: 'test',
html: '<h1>{{ $json.formMode }}</h1>',
},
]);
expect(result[0].html).toBe('<h1>Title</h1>');
});
it('should prepare hiddenField', async () => {
const result = prepareFormFields(webhookFunctions, [
{
fieldLabel: '',
fieldName: 'test',
fieldType: 'hiddenField',
},
]);
expect(result[0]).toEqual({
fieldLabel: 'test',
fieldName: 'test',
fieldType: 'hiddenField',
});
});
});
});

View File

@@ -72,6 +72,28 @@ export function sanitizeHtml(text: string) {
});
}
export const prepareFormFields = (context: IWebhookFunctions, fields: FormFieldsParameter) => {
return fields.map((field) => {
if (field.fieldType === 'html') {
let { html } = field;
if (!html) return field;
for (const resolvable of getResolvables(html)) {
html = html.replace(resolvable, context.evaluateExpression(resolvable) as string);
}
field.html = sanitizeHtml(html as string);
}
if (field.fieldType === 'hiddenField') {
field.fieldLabel = field.fieldName as string;
}
return field;
});
};
export function sanitizeCustomCss(css: string | undefined): string | undefined {
if (!css) return undefined;
@@ -411,6 +433,8 @@ export function renderForm({
} catch (error) {}
}
formFields = prepareFormFields(context, formFields);
const data = prepareFormData({
formTitle,
formDescription,
@@ -476,17 +500,8 @@ export async function formWebhook(
}
const mode = context.getMode() === 'manual' ? 'test' : 'production';
const formFields = (context.getNodeParameter('formFields.values', []) as FormFieldsParameter).map(
(field) => {
if (field.fieldType === 'html') {
field.html = sanitizeHtml(field.html as string);
}
if (field.fieldType === 'hiddenField') {
field.fieldLabel = field.fieldName as string;
}
return field;
},
);
const formFields = context.getNodeParameter('formFields.values', []) as FormFieldsParameter;
const method = context.getRequestObject().method;
validateResponseModeConfiguration(context);