diff --git a/packages/cli/templates/form-trigger.handlebars b/packages/cli/templates/form-trigger.handlebars index 6bad1a02d8..21e10d86a9 100644 --- a/packages/cli/templates/form-trigger.handlebars +++ b/packages/cli/templates/form-trigger.handlebars @@ -377,6 +377,10 @@ {{/if}} + {{#if isHidden}} + + {{/if}} + {{#if isTextarea}}
diff --git a/packages/nodes-base/nodes/Form/Form.node.ts b/packages/nodes-base/nodes/Form/Form.node.ts index 735cbc4065..03f27219fe 100644 --- a/packages/nodes-base/nodes/Form/Form.node.ts +++ b/packages/nodes-base/nodes/Form/Form.node.ts @@ -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; diff --git a/packages/nodes-base/nodes/Form/common.descriptions.ts b/packages/nodes-base/nodes/Form/common.descriptions.ts index 67cf3515d1..9a3fab2e0f 100644 --- a/packages/nodes-base/nodes/Form/common.descriptions.ts +++ b/packages/nodes-base/nodes/Form/common.descriptions.ts @@ -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'], }, }, }, diff --git a/packages/nodes-base/nodes/Form/test/Form.node.test.ts b/packages/nodes-base/nodes/Form/test/Form.node.test.ts index 9dd0e4bbf2..537804dea3 100644 --- a/packages/nodes-base/nodes/Form/test/Form.node.test.ts +++ b/packages/nodes-base/nodes/Form/test/Form.node.test.ts @@ -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([ diff --git a/packages/nodes-base/nodes/Form/test/utils.test.ts b/packages/nodes-base/nodes/Form/test/utils.test.ts index 4d1bf39c36..a6130cfb3c 100644 --- a/packages/nodes-base/nodes/Form/test/utils.test.ts +++ b/packages/nodes-base/nodes/Form/test/utils.test.ts @@ -110,6 +110,12 @@ describe('FormTrigger, formWebhook', () => { html: '
Test HTML
', 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: '
Test HTML
', 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, diff --git a/packages/nodes-base/nodes/Form/utils.ts b/packages/nodes-base/nodes/Form/utils.ts index 2510be56ac..ff10c69d08 100644 --- a/packages/nodes-base/nodes/Form/utils.ts +++ b/packages/nodes-base/nodes/Form/utils.ts @@ -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; }, ); diff --git a/packages/workflow/src/Interfaces.ts b/packages/workflow/src/Interfaces.ts index ff0b1ad973..0d67b07f37 100644 --- a/packages/workflow/src/Interfaces.ts +++ b/packages/workflow/src/Interfaces.ts @@ -2693,6 +2693,8 @@ export type FormFieldsParameter = Array<{ formatDate?: string; html?: string; placeholder?: string; + fieldName?: string; + fieldValue?: string; }>; export type FieldTypeMap = {