mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 18:12:04 +00:00
feat(n8n Form Trigger Node): New node (#7130)
Github issue / Community forum post (link here to close automatically): based on https://github.com/joffcom/n8n-nodes-form-trigger --------- Co-authored-by: Giulio Andreini <g.andreini@gmail.com>
This commit is contained in:
18
packages/nodes-base/nodes/Form/FormTrigger.node.json
Normal file
18
packages/nodes-base/nodes/Form/FormTrigger.node.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"node": "n8n-nodes-base.formTrigger",
|
||||
"nodeVersion": "1.0",
|
||||
"codexVersion": "1.0",
|
||||
"categories": ["Core Nodes"],
|
||||
"alias": ["_Form", "form", "table", "submit", "post"],
|
||||
"resources": {
|
||||
"primaryDocumentation": [
|
||||
{
|
||||
"url": "https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.formtrigger/"
|
||||
}
|
||||
],
|
||||
"generic": []
|
||||
},
|
||||
"subcategories": {
|
||||
"Core Nodes": ["Helpers", "Other Trigger Nodes"]
|
||||
}
|
||||
}
|
||||
282
packages/nodes-base/nodes/Form/FormTrigger.node.ts
Normal file
282
packages/nodes-base/nodes/Form/FormTrigger.node.ts
Normal file
@@ -0,0 +1,282 @@
|
||||
import type {
|
||||
IDataObject,
|
||||
INodeType,
|
||||
INodeTypeDescription,
|
||||
IWebhookResponseData,
|
||||
IWebhookFunctions,
|
||||
} from 'n8n-workflow';
|
||||
import { FORM_TRIGGER_PATH_IDENTIFIER, jsonParse } from 'n8n-workflow';
|
||||
|
||||
import type { FormField } from './interfaces';
|
||||
import { prepareFormData } from './utils';
|
||||
|
||||
export class FormTrigger implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'n8n Form Trigger',
|
||||
name: 'formTrigger',
|
||||
icon: 'file:form.svg',
|
||||
group: ['trigger'],
|
||||
version: 1,
|
||||
description: 'Runs the flow when an n8n generated webform is submitted',
|
||||
defaults: {
|
||||
name: 'n8n Form Trigger',
|
||||
},
|
||||
inputs: [],
|
||||
outputs: ['main'],
|
||||
webhooks: [
|
||||
{
|
||||
name: 'setup',
|
||||
httpMethod: 'GET',
|
||||
responseMode: 'onReceived',
|
||||
path: FORM_TRIGGER_PATH_IDENTIFIER,
|
||||
ndvHideUrl: true,
|
||||
},
|
||||
{
|
||||
name: 'default',
|
||||
httpMethod: 'POST',
|
||||
responseMode: '={{$parameter["responseMode"]}}',
|
||||
path: FORM_TRIGGER_PATH_IDENTIFIER,
|
||||
ndvHideMethod: true,
|
||||
},
|
||||
],
|
||||
eventTriggerDescription: 'Waiting for you to submit the form',
|
||||
activationMessage: 'You can now make calls to your production Form URL.',
|
||||
triggerPanel: {
|
||||
header: 'Pull in a test form submission',
|
||||
executionsHelp: {
|
||||
inactive:
|
||||
"Form Trigger have two modes: test and production. <br /> <br /> <b>Use test mode while you build your workflow</b>. Click the 'Test Step' button, then fill out the test form that opens in a popup tab. The executions will show up in the editor.<br /> <br /> <b>Use production mode to run your workflow automatically</b>. <a data-key=\"activate\">Activate</a> the workflow, then make requests to the production URL. Then every time there's a form submission via the Production Form URL, the workflow will execute. These executions will show up in the executions list, but not in the editor.",
|
||||
active:
|
||||
"Form Trigger have two modes: test and production. <br /> <br /> <b>Use test mode while you build your workflow</b>. Click the 'Test Step' button, then fill out the test form that opens in a popup tab. The executions will show up in the editor.<br /> <br /> <b>Use production mode to run your workflow automatically</b>. <a data-key=\"activate\">Activate</a> the workflow, then make requests to the production URL. Then every time there's a form submission via the Production Form URL, the workflow will execute. These executions will show up in the executions list, but not in the editor.",
|
||||
},
|
||||
activationHint: {
|
||||
active:
|
||||
"This node will also trigger automatically on new form submissions (but those executions won't show up here).",
|
||||
inactive:
|
||||
'<a data-key="activate">Activate</a> this workflow to have it also run automatically for new form submissions created via the Production URL.',
|
||||
},
|
||||
},
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Form Title',
|
||||
name: 'formTitle',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: 'e.g. Contact us',
|
||||
required: true,
|
||||
description: 'Shown at the top of the form',
|
||||
},
|
||||
{
|
||||
displayName: 'Form Description',
|
||||
name: 'formDescription',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: "e.g. We'll get back to you soon",
|
||||
description:
|
||||
'Shown underneath the Form Title. Can be used to prompt the user on how to complete the form.',
|
||||
},
|
||||
{
|
||||
displayName: 'Form Fields',
|
||||
name: 'formFields',
|
||||
placeholder: 'Add Form Field',
|
||||
type: 'fixedCollection',
|
||||
default: { values: [{ label: '', fieldType: 'text' }] },
|
||||
typeOptions: {
|
||||
multipleValues: true,
|
||||
sortable: true,
|
||||
},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Values',
|
||||
name: 'values',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Field Label',
|
||||
name: 'fieldLabel',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: 'e.g. What is your name?',
|
||||
description: 'Label appears above the input field',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
displayName: 'Field Type',
|
||||
name: 'fieldType',
|
||||
type: 'options',
|
||||
default: 'text',
|
||||
description: 'The type of field to add to the form',
|
||||
options: [
|
||||
{
|
||||
name: 'Text',
|
||||
value: 'text',
|
||||
},
|
||||
{
|
||||
name: 'Number',
|
||||
value: 'number',
|
||||
},
|
||||
{
|
||||
name: 'Date',
|
||||
value: 'date',
|
||||
},
|
||||
{
|
||||
name: 'Dropdown List',
|
||||
value: 'dropdown',
|
||||
},
|
||||
],
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
displayName: 'Field Options',
|
||||
name: 'fieldOptions',
|
||||
placeholder: 'Add Field Option',
|
||||
description: 'List of options that can be selected from the dropdown',
|
||||
type: 'fixedCollection',
|
||||
default: { values: [{ option: '' }] },
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
fieldType: ['dropdown'],
|
||||
},
|
||||
},
|
||||
typeOptions: {
|
||||
multipleValues: true,
|
||||
sortable: true,
|
||||
},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Values',
|
||||
name: 'values',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Option',
|
||||
name: 'option',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Multiple Choice',
|
||||
name: 'multiselect',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description:
|
||||
'Whether to allow the user to select multiple options from the dropdown list',
|
||||
displayOptions: {
|
||||
show: {
|
||||
fieldType: ['dropdown'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Required Field',
|
||||
name: 'requiredField',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description:
|
||||
'Whether to require the user to enter a value for this field before submitting the form',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Respond When',
|
||||
name: 'responseMode',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Form Is Submitted',
|
||||
value: 'onReceived',
|
||||
description: 'As soon as this node receives the form submission',
|
||||
},
|
||||
{
|
||||
name: 'Workflow Finishes',
|
||||
value: 'lastNode',
|
||||
description: 'When the last node of the workflow is executed',
|
||||
},
|
||||
],
|
||||
default: 'onReceived',
|
||||
description: 'When to respond to the form submission',
|
||||
},
|
||||
{
|
||||
displayName: 'Options',
|
||||
name: 'options',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Option',
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Form Submitted Text',
|
||||
name: 'formSubmittedText',
|
||||
description: 'The text displayed to users after they filled the form',
|
||||
type: 'string',
|
||||
default: 'Your response has been recorded',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
async webhook(this: IWebhookFunctions): Promise<IWebhookResponseData> {
|
||||
const webhookName = this.getWebhookName();
|
||||
const mode = this.getMode() === 'manual' ? 'test' : 'production';
|
||||
const formFields = this.getNodeParameter('formFields.values', []) as FormField[];
|
||||
|
||||
//Show the form on GET request
|
||||
if (webhookName === 'setup') {
|
||||
const formTitle = this.getNodeParameter('formTitle', '') as string;
|
||||
const formDescription = this.getNodeParameter('formDescription', '') as string;
|
||||
const instanceId = await this.getInstanceId();
|
||||
const { formSubmittedText } = this.getNodeParameter('options', {}) as IDataObject;
|
||||
|
||||
const data = prepareFormData(
|
||||
formTitle,
|
||||
formDescription,
|
||||
formSubmittedText as string,
|
||||
formFields,
|
||||
mode === 'test',
|
||||
instanceId,
|
||||
);
|
||||
|
||||
const res = this.getResponseObject();
|
||||
res.render('form-trigger', data);
|
||||
return {
|
||||
noWebhookResponse: true,
|
||||
};
|
||||
}
|
||||
|
||||
const bodyData = (this.getBodyData().data as IDataObject) ?? {};
|
||||
|
||||
const returnData: IDataObject = {};
|
||||
for (const [index, field] of formFields.entries()) {
|
||||
const key = `field-${index}`;
|
||||
let value = bodyData[key] ?? null;
|
||||
|
||||
if (value === null) returnData[field.fieldLabel] = null;
|
||||
|
||||
if (field.fieldType === 'number') {
|
||||
value = Number(value);
|
||||
}
|
||||
if (field.fieldType === 'text') {
|
||||
value = String(value).trim();
|
||||
}
|
||||
if (field.multiselect && typeof value === 'string') {
|
||||
value = jsonParse(value);
|
||||
}
|
||||
|
||||
returnData[field.fieldLabel] = value;
|
||||
}
|
||||
returnData.submittedAt = new Date().toISOString();
|
||||
returnData.formMode = mode;
|
||||
|
||||
const webhookResponse: IDataObject = { status: 200 };
|
||||
|
||||
return {
|
||||
webhookResponse,
|
||||
workflowData: [this.helpers.returnJsonArray(returnData)],
|
||||
};
|
||||
}
|
||||
}
|
||||
5
packages/nodes-base/nodes/Form/form.svg
Normal file
5
packages/nodes-base/nodes/Form/form.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg width="46" height="40" viewBox="0 0 46 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M34.9784 37.7321C34.9784 38.1465 34.8139 38.5439 34.5207 38.8369C34.2278 39.1301 33.8303 39.2946 33.4159 39.2946H6.25967C5.84529 39.2946 5.44784 39.13 5.15484 38.8369C4.86171 38.544 4.69717 38.1465 4.69717 37.7321V9.60712C4.69668 9.20212 4.85359 8.81282 5.13467 8.52122L11.4393 1.98994V7.33372H8.21267C7.79405 7.33372 7.40717 7.55702 7.19788 7.91962C6.98841 8.28222 6.98841 8.72882 7.19788 9.09152C7.40719 9.45412 7.79409 9.67742 8.21267 9.67742H12.6422C12.9531 9.67742 13.2511 9.55382 13.4709 9.33412C13.6906 9.11442 13.8141 8.81642 13.8141 8.50552V0.232178H33.4158C33.8302 0.232178 34.2276 0.396729 34.5206 0.68985C34.8138 0.982807 34.9783 1.38027 34.9783 1.79468L34.9783 12.1222L32.1177 14.9829L23.8657 23.2583C23.5924 23.5323 23.3622 23.7641 23.1373 23.9905C22.8466 24.2831 22.5649 24.5666 22.2112 24.9205L21.8753 25.2565C21.5818 25.55 21.387 25.9276 21.3177 26.3368L20.3004 31.9218C20.1924 32.5604 20.0852 33.0006 20.5114 33.3398C20.9137 33.6602 21.4119 33.5143 22.0504 33.4062L27.4592 32.4783C27.8679 32.4091 28.2451 32.2147 28.5384 31.9218L34.9784 25.4933L34.9784 37.7321ZM10.9472 16.4667C10.9493 16.7769 11.0733 17.0738 11.2927 17.293C11.5119 17.5124 11.8088 17.6364 12.119 17.6385H25.2819C25.7005 17.6385 26.0874 17.4152 26.2967 17.0526C26.5061 16.69 26.5061 16.2434 26.2967 15.8808C26.0873 15.5181 25.7004 15.2948 25.2819 15.2948H12.119C11.8082 15.2948 11.5102 15.4184 11.2904 15.6381C11.0707 15.8578 10.9472 16.1559 10.9472 16.4667ZM18.2421 31.2325C18.2421 30.9216 18.1186 30.6236 17.8988 30.4039C17.6791 30.1842 17.3811 30.0606 17.0702 30.0606H12.119C11.7004 30.0606 11.3135 30.2839 11.1042 30.6465C10.8948 31.0092 10.8948 31.4558 11.1042 31.8184C11.3135 32.181 11.7004 32.4043 12.119 32.4043H17.0702C17.3811 32.4043 17.6791 32.2808 17.8988 32.0611C18.1185 31.8413 18.2421 31.5434 18.2421 31.2325ZM19.1015 23.8417C19.1015 23.5308 18.9779 23.2328 18.7582 23.013C18.5385 22.7933 18.2404 22.6698 17.9296 22.6698H12.1188C11.7002 22.6698 11.3133 22.8931 11.104 23.2557C10.8946 23.6183 10.8946 24.065 11.104 24.4276C11.3133 24.7902 11.7002 25.0135 12.1188 25.0135H17.9296C18.2409 25.0156 18.5402 24.8927 18.7604 24.6725C18.9807 24.4523 19.1036 24.1531 19.1015 23.8417Z" fill="#00B7BC"/>
|
||||
<path d="M33.5319 16.3971L37.821 12.108L41.5789 15.8189L43.196 14.2017L45.4539 16.4595C45.6718 16.6772 45.7948 16.9719 45.7971 17.2799C45.7951 17.5903 45.6718 17.8875 45.4534 18.1081L38.6488 24.9047C38.4318 25.1284 38.1322 25.2527 37.8206 25.2485C37.5094 25.2509 37.2106 25.1267 36.9925 24.9047C36.7749 24.684 36.653 24.3865 36.653 24.0766C36.653 23.7667 36.7749 23.4692 36.9925 23.2485L42.9692 17.2797L41.6567 15.9672L40.2739 17.3813L27.1255 30.5063L22.5084 31.2876L23.2897 26.6705L23.6256 26.3345L26.1881 28.8892C26.3784 29.0902 26.6362 29.211 26.9096 29.2306C26.9449 29.2331 26.9805 29.2339 27.0162 29.233C27.3279 29.238 27.6279 29.1137 27.8444 28.8892C28.062 28.6685 28.1839 28.371 28.1839 28.0611C28.1839 27.7512 28.062 27.4537 27.8444 27.233L25.2819 24.6705L33.5319 16.3971Z" fill="#00B7BC"/>
|
||||
<path d="M44.7359 12.2409C44.7359 12.6536 44.5725 13.0496 44.2815 13.3424L43.3596 14.2564L39.508 10.4283L40.4377 9.49863C40.7305 9.20763 41.1265 9.04422 41.5393 9.04422C41.9521 9.04422 42.3481 9.20763 42.6409 9.49863L44.2815 11.1393C44.5725 11.4321 44.7359 11.8281 44.7359 12.2409Z" fill="#00B7BC"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.4 KiB |
30
packages/nodes-base/nodes/Form/interfaces.ts
Normal file
30
packages/nodes-base/nodes/Form/interfaces.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
export type FormField = {
|
||||
fieldLabel: string;
|
||||
fieldType: string;
|
||||
requiredField: boolean;
|
||||
fieldOptions?: { values: Array<{ option: string }> };
|
||||
multiselect?: boolean;
|
||||
};
|
||||
|
||||
export type FormTriggerInput = {
|
||||
isSelect?: boolean;
|
||||
isMultiSelect?: boolean;
|
||||
isInput?: boolean;
|
||||
labbel: string;
|
||||
id: string;
|
||||
errorId: string;
|
||||
type?: 'text' | 'number' | 'date';
|
||||
inputRequired: 'form-required' | '';
|
||||
selectOptions?: string[];
|
||||
multiSelectOptions?: Array<{ id: string; label: string }>;
|
||||
};
|
||||
|
||||
export type FormTriggerData = {
|
||||
testRun: boolean;
|
||||
validForm: boolean;
|
||||
formTitle: string;
|
||||
formDescription?: string;
|
||||
formSubmittedText?: string;
|
||||
n8nWebsiteLink: string;
|
||||
formFields: FormTriggerInput[];
|
||||
};
|
||||
64
packages/nodes-base/nodes/Form/utils.ts
Normal file
64
packages/nodes-base/nodes/Form/utils.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import type { IDataObject } from 'n8n-workflow';
|
||||
import type { FormField, FormTriggerData, FormTriggerInput } from './interfaces';
|
||||
|
||||
export const prepareFormData = (
|
||||
formTitle: string,
|
||||
formDescription: string,
|
||||
formSubmittedText: string | undefined,
|
||||
formFields: FormField[],
|
||||
testRun: boolean,
|
||||
instanceId?: string,
|
||||
) => {
|
||||
const validForm = formFields.length > 0;
|
||||
const utm_campaign = instanceId ? `&utm_campaign=${instanceId}` : '';
|
||||
const n8nWebsiteLink = `https://n8n.io/?utm_source=n8n-internal&utm_medium=form-trigger${utm_campaign}`;
|
||||
|
||||
if (formSubmittedText === undefined) {
|
||||
formSubmittedText = 'Your response has been recorded';
|
||||
}
|
||||
|
||||
const formData: FormTriggerData = {
|
||||
testRun,
|
||||
validForm,
|
||||
formTitle,
|
||||
formDescription,
|
||||
formSubmittedText,
|
||||
n8nWebsiteLink,
|
||||
formFields: [],
|
||||
};
|
||||
|
||||
if (!validForm) {
|
||||
return formData;
|
||||
}
|
||||
|
||||
for (const [index, field] of formFields.entries()) {
|
||||
const { fieldType, requiredField, multiselect } = field;
|
||||
|
||||
const input: IDataObject = {
|
||||
id: `field-${index}`,
|
||||
errorId: `error-field-${index}`,
|
||||
label: field.fieldLabel,
|
||||
inputRequired: requiredField ? 'form-required' : '',
|
||||
};
|
||||
|
||||
if (multiselect) {
|
||||
input.isMultiSelect = true;
|
||||
input.multiSelectOptions =
|
||||
field.fieldOptions?.values.map((e, i) => ({
|
||||
id: `option${i}`,
|
||||
label: e.option,
|
||||
})) ?? [];
|
||||
} else if (fieldType === 'dropdown') {
|
||||
input.isSelect = true;
|
||||
const fieldOptions = field.fieldOptions?.values ?? [];
|
||||
input.selectOptions = fieldOptions.map((e) => e.option);
|
||||
} else {
|
||||
input.isInput = true;
|
||||
input.type = fieldType as 'text' | 'number' | 'date';
|
||||
}
|
||||
|
||||
formData.formFields.push(input as FormTriggerInput);
|
||||
}
|
||||
|
||||
return formData;
|
||||
};
|
||||
Reference in New Issue
Block a user