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 has two modes: test and production.
Use test mode while you build your workflow. 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.
Use production mode to run your workflow automatically. Activate 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 has two modes: test and production.
Use test mode while you build your workflow. 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.
Use production mode to run your workflow automatically. Activate 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:
'Activate 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: 'Date',
value: 'date',
},
{
name: 'Dropdown List',
value: 'dropdown',
},
{
name: 'Number',
value: 'number',
},
{
name: 'Password',
value: 'password',
},
{
name: 'Text',
value: 'text',
},
{
name: 'Textarea',
value: 'textarea',
},
],
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 {
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 = 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)],
};
}
}