From c475838fad77c9802b7afc3bd1cdb1a66c3deebc Mon Sep 17 00:00:00 2001 From: Michael Kret Date: Fri, 7 Mar 2025 13:49:05 +0200 Subject: [PATCH 1/2] fix --- packages/nodes-base/nodes/Form/Form.node.ts | 4 +- .../nodes-base/nodes/Form/formNodeUtils.ts | 22 ++++++++++ .../nodes/Form/test/formNodeUtils.test.ts | 41 ++++++++++++++++--- 3 files changed, 60 insertions(+), 7 deletions(-) diff --git a/packages/nodes-base/nodes/Form/Form.node.ts b/packages/nodes-base/nodes/Form/Form.node.ts index f060e3f81500a..3f7ce9ae6ef85 100644 --- a/packages/nodes-base/nodes/Form/Form.node.ts +++ b/packages/nodes-base/nodes/Form/Form.node.ts @@ -20,7 +20,7 @@ import { import { cssVariables } from './cssVariables'; import { renderFormCompletion } from './formCompletionUtils'; -import { renderFormNode } from './formNodeUtils'; +import { evaluateHtmlExpressions, renderFormNode } from './formNodeUtils'; import { configureWaitTillDate } from '../../utils/sendAndWait/configureWaitTillDate.util'; import { limitWaitTimeProperties } from '../../utils/sendAndWait/descriptions'; import { formDescription, formFields, formTitle } from '../Form/common.descriptions'; @@ -347,6 +347,8 @@ export class Form extends Node { ); } + fields = evaluateHtmlExpressions(context, fields); + const method = context.getRequestObject().method; if (operation === 'completion' && method === 'GET') { diff --git a/packages/nodes-base/nodes/Form/formNodeUtils.ts b/packages/nodes-base/nodes/Form/formNodeUtils.ts index ac8e1d5655caa..dd4a210907c29 100644 --- a/packages/nodes-base/nodes/Form/formNodeUtils.ts +++ b/packages/nodes-base/nodes/Form/formNodeUtils.ts @@ -7,6 +7,7 @@ import { } from 'n8n-workflow'; import { renderForm, sanitizeHtml } from './utils'; +import { getResolvables } from '../../utils/utilities'; export const renderFormNode = async ( context: IWebhookFunctions, @@ -70,3 +71,24 @@ export const renderFormNode = async ( noWebhookResponse: true, }; }; + +export const evaluateHtmlExpressions = ( + 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 = html; + } + + return field; + }); +}; diff --git a/packages/nodes-base/nodes/Form/test/formNodeUtils.test.ts b/packages/nodes-base/nodes/Form/test/formNodeUtils.test.ts index a9bbf936f1750..88cc88e9ef7f5 100644 --- a/packages/nodes-base/nodes/Form/test/formNodeUtils.test.ts +++ b/packages/nodes-base/nodes/Form/test/formNodeUtils.test.ts @@ -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, @@ -6,13 +7,22 @@ import { type NodeTypeAndVersion, } from 'n8n-workflow'; -import { renderFormNode } from '../formNodeUtils'; +import { evaluateHtmlExpressions, renderFormNode } from '../formNodeUtils'; describe('formNodeUtils', () => { + let webhookFunctions: MockProxy; + + beforeEach(() => { + webhookFunctions = mock(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + it('should sanitize custom html', async () => { - const executeFunctions = mock(); - 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({ render: mockRender } as any); const triggerMock = mock({ 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, @@ -110,4 +120,23 @@ describe('formNodeUtils', () => { validForm: true, }); }); + + it('should resolve expressions in html fields', async () => { + webhookFunctions.evaluateExpression.mockImplementation((expression) => { + if (expression === '{{ $json.formMode }}') { + return 'Title'; + } + }); + + const result = evaluateHtmlExpressions(webhookFunctions, [ + { + fieldLabel: 'Custom HTML', + fieldType: 'html', + elementName: 'test', + html: '

{{ $json.formMode }}

', + }, + ]); + + expect(result[0].html).toBe('

Title

'); + }); }); From 84c4024a25abb2e21632d612af3c371780ee5506 Mon Sep 17 00:00:00 2001 From: Michael Kret Date: Mon, 10 Mar 2025 13:51:48 +0200 Subject: [PATCH 2/2] trigger support, refactoring --- packages/nodes-base/nodes/Form/Form.node.ts | 14 +------ .../nodes-base/nodes/Form/formNodeUtils.ts | 30 +-------------- .../nodes/Form/test/formNodeUtils.test.ts | 21 +---------- .../nodes-base/nodes/Form/test/utils.test.ts | 37 +++++++++++++++++++ packages/nodes-base/nodes/Form/utils.ts | 37 +++++++++++++------ 5 files changed, 67 insertions(+), 72 deletions(-) diff --git a/packages/nodes-base/nodes/Form/Form.node.ts b/packages/nodes-base/nodes/Form/Form.node.ts index 3f7ce9ae6ef85..5e77b01ca1eb0 100644 --- a/packages/nodes-base/nodes/Form/Form.node.ts +++ b/packages/nodes-base/nodes/Form/Form.node.ts @@ -20,7 +20,7 @@ import { import { cssVariables } from './cssVariables'; import { renderFormCompletion } from './formCompletionUtils'; -import { evaluateHtmlExpressions, renderFormNode } from './formNodeUtils'; +import { renderFormNode } from './formNodeUtils'; import { configureWaitTillDate } from '../../utils/sendAndWait/configureWaitTillDate.util'; import { limitWaitTimeProperties } from '../../utils/sendAndWait/descriptions'; import { formDescription, formFields, formTitle } from '../Form/common.descriptions'; @@ -336,19 +336,9 @@ 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; } - fields = evaluateHtmlExpressions(context, fields); - const method = context.getRequestObject().method; if (operation === 'completion' && method === 'GET') { diff --git a/packages/nodes-base/nodes/Form/formNodeUtils.ts b/packages/nodes-base/nodes/Form/formNodeUtils.ts index dd4a210907c29..4d33b1365d56a 100644 --- a/packages/nodes-base/nodes/Form/formNodeUtils.ts +++ b/packages/nodes-base/nodes/Form/formNodeUtils.ts @@ -6,8 +6,7 @@ import { type IWebhookResponseData, } from 'n8n-workflow'; -import { renderForm, sanitizeHtml } from './utils'; -import { getResolvables } from '../../utils/utilities'; +import { renderForm } from './utils'; export const renderFormNode = async ( context: IWebhookFunctions, @@ -43,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; @@ -71,24 +64,3 @@ export const renderFormNode = async ( noWebhookResponse: true, }; }; - -export const evaluateHtmlExpressions = ( - 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 = html; - } - - return field; - }); -}; diff --git a/packages/nodes-base/nodes/Form/test/formNodeUtils.test.ts b/packages/nodes-base/nodes/Form/test/formNodeUtils.test.ts index 88cc88e9ef7f5..a21426d1b5eff 100644 --- a/packages/nodes-base/nodes/Form/test/formNodeUtils.test.ts +++ b/packages/nodes-base/nodes/Form/test/formNodeUtils.test.ts @@ -7,7 +7,7 @@ import { type NodeTypeAndVersion, } from 'n8n-workflow'; -import { evaluateHtmlExpressions, renderFormNode } from '../formNodeUtils'; +import { renderFormNode } from '../formNodeUtils'; describe('formNodeUtils', () => { let webhookFunctions: MockProxy; @@ -120,23 +120,4 @@ describe('formNodeUtils', () => { validForm: true, }); }); - - it('should resolve expressions in html fields', async () => { - webhookFunctions.evaluateExpression.mockImplementation((expression) => { - if (expression === '{{ $json.formMode }}') { - return 'Title'; - } - }); - - const result = evaluateHtmlExpressions(webhookFunctions, [ - { - fieldLabel: 'Custom HTML', - fieldType: 'html', - elementName: 'test', - html: '

{{ $json.formMode }}

', - }, - ]); - - expect(result[0].html).toBe('

Title

'); - }); }); diff --git a/packages/nodes-base/nodes/Form/test/utils.test.ts b/packages/nodes-base/nodes/Form/test/utils.test.ts index 432bce51215e9..66b584e2c7e35 100644 --- a/packages/nodes-base/nodes/Form/test/utils.test.ts +++ b/packages/nodes-base/nodes/Form/test/utils.test.ts @@ -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: '

{{ $json.formMode }}

', + }, + ]); + + expect(result[0].html).toBe('

Title

'); + }); + it('should prepare hiddenField', async () => { + const result = prepareFormFields(webhookFunctions, [ + { + fieldLabel: '', + fieldName: 'test', + fieldType: 'hiddenField', + }, + ]); + + expect(result[0]).toEqual({ + fieldLabel: 'test', + fieldName: 'test', + fieldType: 'hiddenField', + }); + }); + }); }); diff --git a/packages/nodes-base/nodes/Form/utils.ts b/packages/nodes-base/nodes/Form/utils.ts index c8b750e6d1e0d..054da42e83e4e 100644 --- a/packages/nodes-base/nodes/Form/utils.ts +++ b/packages/nodes-base/nodes/Form/utils.ts @@ -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);