feat(n8n Form Node): Add Hidden Fields (#12803)
Some checks are pending
Test Master / install-and-build (push) Waiting to run
Test Master / Unit tests (18.x) (push) Blocked by required conditions
Test Master / Unit tests (20.x) (push) Blocked by required conditions
Test Master / Unit tests (22.4) (push) Blocked by required conditions
Test Master / Lint (push) Blocked by required conditions
Test Master / Notify Slack on failure (push) Blocked by required conditions
Benchmark Docker Image CI / build (push) Waiting to run

This commit is contained in:
Dana 2025-01-29 16:56:47 +01:00 committed by GitHub
parent 0f345681d9
commit 0da1114981
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 161 additions and 6 deletions

View file

@ -377,6 +377,10 @@
</div>
{{/if}}
{{#if isHidden}}
<input type="hidden" id="{{id}}" name="{{id}}" value="{{hiddenValue}}" />
{{/if}}
{{#if isTextarea}}
<div class='form-group'>
<label class='form-label {{inputRequired}}' for='{{id}}'>{{label}}</label>

View file

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

View file

@ -64,6 +64,11 @@ export const formFields: INodeProperties = {
placeholder: 'e.g. What is your name?',
description: 'Label that appears above the input field',
required: true,
displayOptions: {
hide: {
fieldType: ['hiddenField'],
},
},
},
{
displayName: 'Element Type',
@ -92,6 +97,10 @@ export const formFields: INodeProperties = {
name: 'File',
value: 'file',
},
{
name: 'Hidden Field',
value: 'hiddenField',
},
{
name: 'Number',
value: 'number',
@ -119,7 +128,33 @@ export const formFields: INodeProperties = {
default: '',
displayOptions: {
hide: {
fieldType: ['dropdown', 'date', 'file', 'html'],
fieldType: ['dropdown', 'date', 'file', 'html', 'hiddenField'],
},
},
},
{
displayName: 'Field Name',
name: 'fieldName',
description:
'The name of the field, used in input attributes and referenced by the workflow',
type: 'string',
default: '',
displayOptions: {
show: {
fieldType: ['hiddenField'],
},
},
},
{
displayName: 'Field Value',
name: 'fieldValue',
description:
'Input value can be set here or will be passed as a query parameter via Field Name if no value is set',
type: 'string',
default: '',
displayOptions: {
show: {
fieldType: ['hiddenField'],
},
},
},
@ -242,7 +277,7 @@ export const formFields: INodeProperties = {
'Whether to require the user to enter a value for this field before submitting the form',
displayOptions: {
hide: {
fieldType: ['html'],
fieldType: ['html', 'hiddenField'],
},
},
},

View file

@ -106,7 +106,16 @@ describe('Form Node', () => {
mockWebhookFunctions.getNodeParameter.mockImplementation((paramName: string) => {
if (paramName === 'operation') return 'page';
if (paramName === 'useJson') return false;
if (paramName === 'formFields.values') return [{ fieldLabel: 'test' }];
if (paramName === 'formFields.values')
return [
{ fieldLabel: 'test' },
{
fieldName: 'Powerpuff Girl',
fieldValue: 'Blossom',
fieldType: 'hiddenField',
fieldLabel: '',
},
];
if (paramName === 'options') {
return {
formTitle: 'Form Title',
@ -121,7 +130,42 @@ describe('Form Node', () => {
await form.webhook(mockWebhookFunctions);
expect(mockResponseObject.render).toHaveBeenCalledWith('form-trigger', expect.any(Object));
expect(mockResponseObject.render).toHaveBeenCalledWith('form-trigger', {
appendAttribution: 'test',
buttonLabel: 'Form Button',
formDescription: 'Form Description',
formDescriptionMetadata: 'Form Description',
formFields: [
{
id: 'field-0',
errorId: 'error-field-0',
label: 'test',
inputRequired: '',
defaultValue: '',
isInput: true,
placeholder: undefined,
type: undefined,
},
{
id: 'field-1',
errorId: 'error-field-1',
label: 'Powerpuff Girl',
inputRequired: '',
defaultValue: '',
placeholder: undefined,
hiddenName: 'Powerpuff Girl',
hiddenValue: 'Blossom',
isHidden: true,
},
],
formSubmittedText: 'Your response has been recorded',
formTitle: 'Form Title',
n8nWebsiteLink: 'https://n8n.io/?utm_source=n8n-internal&utm_medium=form-trigger',
testRun: true,
useResponseData: false,
validForm: true,
formSubmittedHeader: undefined,
});
});
it('should return form data for POST request', async () => {
@ -182,6 +226,7 @@ describe('Form Node', () => {
if (paramName === 'completionTitle') return 'Test Title';
if (paramName === 'completionMessage') return 'Test Message';
if (paramName === 'redirectUrl') return '';
if (paramName === 'formFields.values') return [];
return {};
});
mockWebhookFunctions.getParentNodes.mockReturnValue([
@ -225,6 +270,8 @@ describe('Form Node', () => {
if (paramName === 'completionTitle') return 'Test Title';
if (paramName === 'completionMessage') return 'Test Message';
if (paramName === 'redirectUrl') return 'https://n8n.io';
if (paramName === 'formFields.values') return [];
return {};
});
mockWebhookFunctions.getParentNodes.mockReturnValue([

View file

@ -110,6 +110,12 @@ describe('FormTrigger, formWebhook', () => {
html: '<div>Test HTML</div>',
requiredField: false,
},
{
fieldName: 'Powerpuff Girl',
fieldValue: 'Blossom',
fieldType: 'hiddenField',
fieldLabel: '',
},
];
executeFunctions.getNodeParameter.calledWith('formFields.values').mockReturnValue(formFields);
@ -174,6 +180,17 @@ describe('FormTrigger, formWebhook', () => {
html: '<div>Test HTML</div>',
isHtml: true,
},
{
id: 'field-5',
errorId: 'error-field-5',
hiddenName: 'Powerpuff Girl',
hiddenValue: 'Blossom',
label: 'Powerpuff Girl',
isHidden: true,
inputRequired: '',
defaultValue: '',
placeholder: undefined,
},
],
formSubmittedText: 'Your response has been recorded',
formTitle: 'Test Form',
@ -300,9 +317,21 @@ describe('FormTrigger, prepareFormData', () => {
acceptFileTypes: '.jpg,.png',
multipleFiles: true,
},
{
fieldLabel: 'username',
fieldName: 'username',
fieldValue: 'powerpuffgirl125',
fieldType: 'hiddenField',
},
{
fieldLabel: 'villain',
fieldName: 'villain',
fieldValue: 'Mojo Dojo',
fieldType: 'hiddenField',
},
];
const query = { Name: 'John Doe', Email: 'john@example.com' };
const query = { Name: 'John Doe', Email: 'john@example.com', villain: 'princess morbucks' };
const result = prepareFormData({
formTitle: 'Test Form',
@ -368,6 +397,28 @@ describe('FormTrigger, prepareFormData', () => {
acceptFileTypes: '.jpg,.png',
multipleFiles: 'multiple',
},
{
id: 'field-4',
errorId: 'error-field-4',
label: 'username',
inputRequired: '',
defaultValue: '',
placeholder: undefined,
hiddenName: 'username',
hiddenValue: 'powerpuffgirl125',
isHidden: true,
},
{
id: 'field-5',
errorId: 'error-field-5',
label: 'villain',
inputRequired: '',
defaultValue: 'princess morbucks',
placeholder: undefined,
hiddenName: 'villain',
isHidden: true,
hiddenValue: 'princess morbucks',
},
],
useResponseData: true,
appendAttribution: true,

View file

@ -167,6 +167,11 @@ export function prepareFormData({
} else if (fieldType === 'html') {
input.isHtml = true;
input.html = field.html as string;
} else if (fieldType === 'hiddenField') {
input.isHidden = true;
input.hiddenName = field.fieldName as string;
input.hiddenValue =
input.defaultValue === '' ? (field.fieldValue as string) : input.defaultValue;
} else {
input.isInput = true;
input.type = fieldType as 'text' | 'number' | 'date' | 'email';
@ -432,6 +437,9 @@ export async function formWebhook(
if (field.fieldType === 'html') {
field.html = sanitizeHtml(field.html as string);
}
if (field.fieldType === 'hiddenField') {
field.fieldLabel = field.fieldName as string;
}
return field;
},
);

View file

@ -2693,6 +2693,8 @@ export type FormFieldsParameter = Array<{
formatDate?: string;
html?: string;
placeholder?: string;
fieldName?: string;
fieldValue?: string;
}>;
export type FieldTypeMap = {