feat(n8n Form Trigger Node): Improvements (#10092)

Co-authored-by: कारतोफ्फेलस्क्रिप्ट™ <netroy@users.noreply.github.com>
Co-authored-by: Shireen Missi <94372015+ShireenMissi@users.noreply.github.com>
This commit is contained in:
Michael Kret
2024-07-29 15:58:03 +03:00
committed by GitHub
parent 7a30d845e9
commit 711b667ebe
12 changed files with 1015 additions and 147 deletions

View File

@@ -11,12 +11,13 @@ export class FormTrigger extends VersionedNodeType {
icon: 'file:form.svg',
group: ['trigger'],
description: 'Runs the flow when an n8n generated webform is submitted',
defaultVersion: 2,
defaultVersion: 2.1,
};
const nodeVersions: IVersionedNodeType['nodeVersions'] = {
1: new FormTriggerV1(baseDescription),
2: new FormTriggerV2(baseDescription),
2.1: new FormTriggerV2(baseDescription),
};
super(nodeVersions, baseDescription);

View File

@@ -28,6 +28,9 @@ export const formDescription: INodeProperties = {
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.',
typeOptions: {
rows: 2,
},
};
export const formFields: INodeProperties = {
@@ -69,6 +72,14 @@ export const formFields: INodeProperties = {
name: 'Dropdown List',
value: 'dropdown',
},
{
name: 'Email',
value: 'email',
},
{
name: 'File',
value: 'file',
},
{
name: 'Number',
value: 'number',
@@ -88,6 +99,17 @@ export const formFields: INodeProperties = {
],
required: true,
},
{
displayName: 'Placeholder',
name: 'placeholder',
type: 'string',
default: '',
displayOptions: {
hide: {
fieldType: ['dropdown', 'date', 'file'],
},
},
},
{
displayName: 'Field Options',
name: 'fieldOptions',
@@ -133,6 +155,48 @@ export const formFields: INodeProperties = {
},
},
},
{
displayName: 'Multiple Files',
name: 'multipleFiles',
type: 'boolean',
default: true,
description:
'Whether to allow the user to select multiple files from the file input or just one',
displayOptions: {
show: {
fieldType: ['file'],
},
},
},
{
displayName: 'Accept File Types',
name: 'acceptFileTypes',
type: 'string',
default: '',
description: 'List of file types that can be uploaded, separated by commas',
hint: 'Leave empty to allow all file types',
placeholder: 'e.g. .jpg, .png',
displayOptions: {
show: {
fieldType: ['file'],
},
},
},
{
displayName: 'Format Date As',
name: 'formatDate',
type: 'string',
default: '',
description:
'Returns a string representation of this field formatted according to the specified format string. For a table of tokens and their interpretations, see <a href="https://moment.github.io/luxon/#/formatting?ID=table-of-tokens" target="_blank">here</a>.',
placeholder: 'e.g. dd/mm/yyyy',
hint: 'Leave empty to use the default format',
displayOptions: {
show: {
fieldType: ['date'],
},
},
},
{
displayName: 'Required Field',
name: 'requiredField',

View File

@@ -4,20 +4,29 @@ export type FormField = {
requiredField: boolean;
fieldOptions?: { values: Array<{ option: string }> };
multiselect?: boolean;
multipleFiles?: boolean;
acceptFileTypes?: string;
formatDate?: string;
placeholder?: string;
};
export type FormTriggerInput = {
isSelect?: boolean;
isMultiSelect?: boolean;
isTextarea?: boolean;
isFileInput?: boolean;
isInput?: boolean;
labbel: string;
label: string;
defaultValue?: string;
id: string;
errorId: string;
type?: 'text' | 'number' | 'date';
inputRequired: 'form-required' | '';
selectOptions?: string[];
multiSelectOptions?: Array<{ id: string; label: string }>;
acceptFileTypes?: string;
multipleFiles?: 'multiple' | '';
placeholder?: string;
};
export type FormTriggerData = {
@@ -31,4 +40,7 @@ export type FormTriggerData = {
formFields: FormTriggerInput[];
useResponseData?: boolean;
appendAttribution?: boolean;
customAttribution?: string;
};
export const FORM_TRIGGER_AUTHENTICATION_PROPERTY = 'authentication';

View File

@@ -0,0 +1,370 @@
import { mock } from 'jest-mock-extended';
import type { IWebhookFunctions } from 'n8n-workflow';
import type { FormField } from '../interfaces';
import { formWebhook, prepareFormData } from '../utils';
describe('FormTrigger, formWebhook', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should call response render', async () => {
const executeFunctions = mock<IWebhookFunctions>();
const mockRender = jest.fn();
const formFields: FormField[] = [
{ fieldLabel: 'Name', fieldType: 'text', requiredField: true },
{ fieldLabel: 'Age', fieldType: 'number', requiredField: false },
{
fieldLabel: 'Gender',
fieldType: 'select',
requiredField: true,
fieldOptions: { values: [{ option: 'Male' }, { option: 'Female' }] },
},
{
fieldLabel: 'Resume',
fieldType: 'file',
requiredField: true,
acceptFileTypes: '.pdf,.doc',
multipleFiles: false,
},
];
executeFunctions.getNode.mockReturnValue({ typeVersion: 2.1 } as any);
executeFunctions.getNodeParameter.calledWith('options').mockReturnValue({});
executeFunctions.getNodeParameter.calledWith('formTitle').mockReturnValue('Test Form');
executeFunctions.getNodeParameter
.calledWith('formDescription')
.mockReturnValue('Test Description');
executeFunctions.getNodeParameter.calledWith('responseMode').mockReturnValue('onReceived');
executeFunctions.getNodeParameter.calledWith('formFields.values').mockReturnValue(formFields);
executeFunctions.getResponseObject.mockReturnValue({ render: mockRender } as any);
executeFunctions.getRequestObject.mockReturnValue({ method: 'GET', query: {} } as any);
executeFunctions.getMode.mockReturnValue('manual');
executeFunctions.getInstanceId.mockReturnValue('instanceId');
executeFunctions.getBodyData.mockReturnValue({ data: {}, files: {} });
executeFunctions.getChildNodes.mockReturnValue([]);
await formWebhook(executeFunctions);
expect(mockRender).toHaveBeenCalledWith('form-trigger', {
appendAttribution: true,
customAttribution: undefined,
formDescription: 'Test Description',
formFields: [
{
defaultValue: '',
errorId: 'error-field-0',
id: 'field-0',
inputRequired: 'form-required',
isInput: true,
label: 'Name',
placeholder: undefined,
type: 'text',
},
{
defaultValue: '',
errorId: 'error-field-1',
id: 'field-1',
inputRequired: '',
isInput: true,
label: 'Age',
placeholder: undefined,
type: 'number',
},
{
defaultValue: '',
errorId: 'error-field-2',
id: 'field-2',
inputRequired: 'form-required',
isInput: true,
label: 'Gender',
placeholder: undefined,
type: 'select',
},
{
acceptFileTypes: '.pdf,.doc',
defaultValue: '',
errorId: 'error-field-3',
id: 'field-3',
inputRequired: 'form-required',
isFileInput: true,
label: 'Resume',
multipleFiles: '',
placeholder: undefined,
},
],
formSubmittedText: 'Your response has been recorded',
formTitle: 'Test Form',
n8nWebsiteLink:
'https://n8n.io/?utm_source=n8n-internal&utm_medium=form-trigger&utm_campaign=instanceId',
testRun: true,
useResponseData: false,
validForm: true,
});
});
it('should return workflowData on POST request', async () => {
const executeFunctions = mock<IWebhookFunctions>();
const mockStatus = jest.fn();
const mockEnd = jest.fn();
const formFields: FormField[] = [
{ fieldLabel: 'Name', fieldType: 'text', requiredField: true },
{ fieldLabel: 'Age', fieldType: 'number', requiredField: false },
];
const bodyData = {
'field-0': 'John Doe',
'field-1': '30',
};
executeFunctions.getNode.mockReturnValue({ typeVersion: 2.1 } as any);
executeFunctions.getNodeParameter.calledWith('options').mockReturnValue({});
executeFunctions.getNodeParameter.calledWith('responseMode').mockReturnValue('onReceived');
executeFunctions.getChildNodes.mockReturnValue([]);
executeFunctions.getNodeParameter.calledWith('formFields.values').mockReturnValue(formFields);
executeFunctions.getResponseObject.mockReturnValue({ status: mockStatus, end: mockEnd } as any);
executeFunctions.getRequestObject.mockReturnValue({ method: 'POST' } as any);
executeFunctions.getMode.mockReturnValue('manual');
executeFunctions.getInstanceId.mockReturnValue('instanceId');
executeFunctions.getBodyData.mockReturnValue({ data: bodyData, files: {} });
const result = await formWebhook(executeFunctions);
expect(result).toEqual({
webhookResponse: { status: 200 },
workflowData: [
[
{
json: {
Name: 'John Doe',
Age: 30,
submittedAt: expect.any(String),
formMode: 'test',
},
},
],
],
});
});
});
describe('FormTrigger, prepareFormData', () => {
it('should return valid form data with given parameters', () => {
const formFields: FormField[] = [
{
fieldLabel: 'Name',
fieldType: 'text',
requiredField: true,
placeholder: 'Enter your name',
},
{
fieldLabel: 'Email',
fieldType: 'email',
requiredField: true,
placeholder: 'Enter your email',
},
{
fieldLabel: 'Gender',
fieldType: 'dropdown',
requiredField: false,
fieldOptions: { values: [{ option: 'Male' }, { option: 'Female' }] },
},
{
fieldLabel: 'Files',
fieldType: 'file',
requiredField: false,
acceptFileTypes: '.jpg,.png',
multipleFiles: true,
},
];
const query = { Name: 'John Doe', Email: 'john@example.com' };
const result = prepareFormData({
formTitle: 'Test Form',
formDescription: 'This is a test form',
formSubmittedText: 'Thank you for your submission',
redirectUrl: 'https://example.com/thank-you',
formFields,
testRun: false,
query,
instanceId: 'test-instance',
useResponseData: true,
});
expect(result).toEqual({
testRun: false,
validForm: true,
formTitle: 'Test Form',
formDescription: 'This is a test form',
formSubmittedText: 'Thank you for your submission',
n8nWebsiteLink:
'https://n8n.io/?utm_source=n8n-internal&utm_medium=form-trigger&utm_campaign=test-instance',
formFields: [
{
id: 'field-0',
errorId: 'error-field-0',
label: 'Name',
inputRequired: 'form-required',
defaultValue: 'John Doe',
placeholder: 'Enter your name',
isInput: true,
type: 'text',
},
{
id: 'field-1',
errorId: 'error-field-1',
label: 'Email',
inputRequired: 'form-required',
defaultValue: 'john@example.com',
placeholder: 'Enter your email',
isInput: true,
type: 'email',
},
{
id: 'field-2',
errorId: 'error-field-2',
label: 'Gender',
inputRequired: '',
defaultValue: '',
placeholder: undefined,
isSelect: true,
selectOptions: ['Male', 'Female'],
},
{
id: 'field-3',
errorId: 'error-field-3',
label: 'Files',
inputRequired: '',
defaultValue: '',
placeholder: undefined,
isFileInput: true,
acceptFileTypes: '.jpg,.png',
multipleFiles: 'multiple',
},
],
useResponseData: true,
appendAttribution: true,
customAttribution: undefined,
redirectUrl: 'https://example.com/thank-you',
});
});
it('should handle missing optional fields gracefully', () => {
const formFields: FormField[] = [
{
fieldLabel: 'Name',
fieldType: 'text',
requiredField: true,
placeholder: 'Enter your name',
},
];
const result = prepareFormData({
formTitle: 'Test Form',
formDescription: 'This is a test form',
formSubmittedText: undefined,
redirectUrl: undefined,
formFields,
testRun: true,
query: {},
});
expect(result).toEqual({
testRun: true,
validForm: true,
formTitle: 'Test Form',
formDescription: 'This is a test form',
formSubmittedText: 'Your response has been recorded',
n8nWebsiteLink: 'https://n8n.io/?utm_source=n8n-internal&utm_medium=form-trigger',
formFields: [
{
id: 'field-0',
errorId: 'error-field-0',
label: 'Name',
inputRequired: 'form-required',
defaultValue: '',
placeholder: 'Enter your name',
isInput: true,
type: 'text',
},
],
useResponseData: undefined,
appendAttribution: true,
customAttribution: undefined,
});
});
it('should set redirectUrl with http if protocol is missing', () => {
const formFields: FormField[] = [
{
fieldLabel: 'Name',
fieldType: 'text',
requiredField: true,
placeholder: 'Enter your name',
},
];
const query = { Name: 'John Doe' };
const result = prepareFormData({
formTitle: 'Test Form',
formDescription: 'This is a test form',
formSubmittedText: undefined,
redirectUrl: 'example.com/thank-you',
formFields,
testRun: true,
query,
});
expect(result.redirectUrl).toBe('http://example.com/thank-you');
});
it('should return invalid form data when formFields are empty', () => {
const result = prepareFormData({
formTitle: 'Test Form',
formDescription: 'This is a test form',
formSubmittedText: undefined,
redirectUrl: undefined,
formFields: [],
testRun: true,
query: {},
});
expect(result.validForm).toBe(false);
expect(result.formFields).toEqual([]);
});
it('should correctly handle multiselect fields', () => {
const formFields: FormField[] = [
{
fieldLabel: 'Favorite Colors',
fieldType: 'text',
requiredField: true,
multiselect: true,
fieldOptions: { values: [{ option: 'Red' }, { option: 'Blue' }, { option: 'Green' }] },
},
];
const query = { 'Favorite Colors': 'Red,Blue' };
const result = prepareFormData({
formTitle: 'Test Form',
formDescription: 'This is a test form',
formSubmittedText: 'Thank you',
redirectUrl: 'example.com',
formFields,
testRun: false,
query,
});
expect(result.formFields[0].isMultiSelect).toBe(true);
expect(result.formFields[0].multiSelectOptions).toEqual([
{ id: 'option0', label: 'Red' },
{ id: 'option1', label: 'Blue' },
{ id: 'option2', label: 'Green' },
]);
});
});

View File

@@ -1,22 +1,45 @@
import {
NodeOperationError,
jsonParse,
type IDataObject,
type IWebhookFunctions,
import type {
INodeExecutionData,
MultiPartFormData,
IDataObject,
IWebhookFunctions,
} from 'n8n-workflow';
import type { FormField, FormTriggerData, FormTriggerInput } from './interfaces';
import { NodeOperationError, jsonParse } from 'n8n-workflow';
export const prepareFormData = (
formTitle: string,
formDescription: string,
formSubmittedText: string | undefined,
redirectUrl: string | undefined,
formFields: FormField[],
testRun: boolean,
instanceId?: string,
useResponseData?: boolean,
import type { FormField, FormTriggerData, FormTriggerInput } from './interfaces';
import { FORM_TRIGGER_AUTHENTICATION_PROPERTY } from './interfaces';
import { WebhookAuthorizationError } from '../Webhook/error';
import { validateWebhookAuthentication } from '../Webhook/utils';
import { DateTime } from 'luxon';
import isbot from 'isbot';
export function prepareFormData({
formTitle,
formDescription,
formSubmittedText,
redirectUrl,
formFields,
testRun,
query,
instanceId,
useResponseData,
appendAttribution = true,
) => {
customAttribution,
}: {
formTitle: string;
formDescription: string;
formSubmittedText: string | undefined;
redirectUrl: string | undefined;
formFields: FormField[];
testRun: boolean;
query: IDataObject;
instanceId?: string;
useResponseData?: boolean;
appendAttribution?: boolean;
customAttribution?: 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}`;
@@ -35,6 +58,7 @@ export const prepareFormData = (
formFields: [],
useResponseData,
appendAttribution,
customAttribution,
};
if (redirectUrl) {
@@ -49,13 +73,15 @@ export const prepareFormData = (
}
for (const [index, field] of formFields.entries()) {
const { fieldType, requiredField, multiselect } = field;
const { fieldType, requiredField, multiselect, placeholder } = field;
const input: IDataObject = {
id: `field-${index}`,
errorId: `error-field-${index}`,
label: field.fieldLabel,
inputRequired: requiredField ? 'form-required' : '',
defaultValue: query[field.fieldLabel] ?? '',
placeholder,
};
if (multiselect) {
@@ -65,6 +91,10 @@ export const prepareFormData = (
id: `option${i}`,
label: e.option,
})) ?? [];
} else if (fieldType === 'file') {
input.isFileInput = true;
input.acceptFileTypes = field.acceptFileTypes;
input.multipleFiles = field.multipleFiles ? 'multiple' : '';
} else if (fieldType === 'dropdown') {
input.isSelect = true;
const fieldOptions = field.fieldOptions?.values ?? [];
@@ -73,14 +103,14 @@ export const prepareFormData = (
input.isTextarea = true;
} else {
input.isInput = true;
input.type = fieldType as 'text' | 'number' | 'date';
input.type = fieldType as 'text' | 'number' | 'date' | 'email';
}
formData.formFields.push(input as FormTriggerInput);
}
return formData;
};
}
const checkResponseModeConfiguration = (context: IWebhookFunctions) => {
const responseMode = context.getNodeParameter('responseMode', 'onReceived') as string;
@@ -114,6 +144,37 @@ const checkResponseModeConfiguration = (context: IWebhookFunctions) => {
};
export async function formWebhook(context: IWebhookFunctions) {
const nodeVersion = context.getNode().typeVersion;
const options = context.getNodeParameter('options', {}) as {
ignoreBots?: boolean;
respondWithOptions?: {
values: {
respondWith: 'text' | 'redirect';
formSubmittedText: string;
redirectUrl: string;
};
};
formSubmittedText?: string;
useWorkflowTimezone?: boolean;
appendAttribution?: boolean;
customAttribution?: string;
};
const res = context.getResponseObject();
const req = context.getRequestObject();
try {
if (options.ignoreBots && isbot(req.headers['user-agent']))
throw new WebhookAuthorizationError(403);
await validateWebhookAuthentication(context, FORM_TRIGGER_AUTHENTICATION_PROPERTY);
} catch (error) {
if (error instanceof WebhookAuthorizationError) {
res.writeHead(error.responseCode, { 'WWW-Authenticate': 'Basic realm="Webhook"' });
res.end(error.message);
return { noWebhookResponse: true };
}
throw error;
}
const mode = context.getMode() === 'manual' ? 'test' : 'production';
const formFields = context.getNodeParameter('formFields.values', []) as FormField[];
const method = context.getRequestObject().method;
@@ -123,10 +184,11 @@ export async function formWebhook(context: IWebhookFunctions) {
//Show the form on GET request
if (method === 'GET') {
const formTitle = context.getNodeParameter('formTitle', '') as string;
const formDescription = context.getNodeParameter('formDescription', '') as string;
const formDescription = (context.getNodeParameter('formDescription', '') as string)
.replace(/\\n/g, '\n')
.replace(/<br>/g, '\n');
const instanceId = context.getInstanceId();
const responseMode = context.getNodeParameter('responseMode', '') as string;
const options = context.getNodeParameter('options', {}) as IDataObject;
let formSubmittedText;
let redirectUrl;
@@ -150,19 +212,22 @@ export async function formWebhook(context: IWebhookFunctions) {
const useResponseData = responseMode === 'responseNode';
const data = prepareFormData(
const query = context.getRequestObject().query as IDataObject;
const data = prepareFormData({
formTitle,
formDescription,
formSubmittedText,
redirectUrl,
formFields,
mode === 'test',
testRun: mode === 'test',
query,
instanceId,
useResponseData,
appendAttribution,
);
customAttribution: options.customAttribution as string,
});
const res = context.getResponseObject();
res.render('form-trigger', data);
return {
noWebhookResponse: true,
@@ -170,13 +235,64 @@ export async function formWebhook(context: IWebhookFunctions) {
}
const bodyData = (context.getBodyData().data as IDataObject) ?? {};
const files = (context.getBodyData().files as IDataObject) ?? {};
const returnItem: INodeExecutionData = {
json: {},
};
if (files && Object.keys(files).length) {
returnItem.binary = {};
}
for (const key of Object.keys(files)) {
const processFiles: MultiPartFormData.File[] = [];
let multiFile = false;
const filesInput = files[key] as MultiPartFormData.File[] | MultiPartFormData.File;
if (Array.isArray(filesInput)) {
bodyData[key] = filesInput.map((file) => ({
filename: file.originalFilename,
mimetype: file.mimetype,
size: file.size,
}));
processFiles.push(...filesInput);
multiFile = true;
} else {
bodyData[key] = {
filename: filesInput.originalFilename,
mimetype: filesInput.mimetype,
size: filesInput.size,
};
processFiles.push(filesInput);
}
const entryIndex = Number(key.replace(/field-/g, ''));
const fieldLabel = isNaN(entryIndex) ? key : formFields[entryIndex].fieldLabel;
let fileCount = 0;
for (const file of processFiles) {
let binaryPropertyName = fieldLabel.replace(/\W/g, '_');
if (multiFile) {
binaryPropertyName += `_${fileCount++}`;
}
returnItem.binary![binaryPropertyName] = await context.nodeHelpers.copyBinaryFile(
file.filepath,
file.originalFilename ?? file.newFilename,
file.mimetype,
);
}
}
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 (value === null) {
returnItem.json[field.fieldLabel] = null;
continue;
}
if (field.fieldType === 'number') {
value = Number(value);
@@ -187,16 +303,31 @@ export async function formWebhook(context: IWebhookFunctions) {
if (field.multiselect && typeof value === 'string') {
value = jsonParse(value);
}
if (field.fieldType === 'date' && value && field.formatDate !== '') {
value = DateTime.fromFormat(String(value), 'yyyy-mm-dd').toFormat(field.formatDate as string);
}
if (field.fieldType === 'file' && field.multipleFiles && !Array.isArray(value)) {
value = [value];
}
returnData[field.fieldLabel] = value;
returnItem.json[field.fieldLabel] = value;
}
returnData.submittedAt = new Date().toISOString();
returnData.formMode = mode;
let { useWorkflowTimezone } = options;
if (useWorkflowTimezone === undefined && nodeVersion > 2) {
useWorkflowTimezone = true;
}
const timezone = useWorkflowTimezone ? context.getTimezone() : 'UTC';
returnItem.json.submittedAt = DateTime.now().setZone(timezone).toISO();
returnItem.json.formMode = mode;
const webhookResponse: IDataObject = { status: 200 };
return {
webhookResponse,
workflowData: [context.helpers.returnJsonArray(returnData)],
workflowData: [[returnItem]],
};
}

View File

@@ -1,9 +1,10 @@
/* eslint-disable n8n-nodes-base/node-filename-against-convention */
import {
type INodeType,
type INodeTypeBaseDescription,
type INodeTypeDescription,
type IWebhookFunctions,
import type {
INodeProperties,
INodeType,
INodeTypeBaseDescription,
INodeTypeDescription,
IWebhookFunctions,
} from 'n8n-workflow';
import { formWebhook } from '../utils';
@@ -16,13 +17,22 @@ import {
respondWithOptions,
webhookPath,
} from '../common.descriptions';
import { FORM_TRIGGER_AUTHENTICATION_PROPERTY } from '../interfaces';
const useWorkflowTimezone: INodeProperties = {
displayName: 'Use Workflow Timezone',
name: 'useWorkflowTimezone',
type: 'boolean',
default: false,
description: "Whether to use the workflow timezone set in node's settings rather than UTC",
};
const descriptionV2: INodeTypeDescription = {
displayName: 'n8n Form Trigger',
name: 'formTrigger',
icon: 'file:form.svg',
group: ['trigger'],
version: 2,
version: [2, 2.1],
description: 'Runs the flow when an n8n generated webform is submitted',
defaults: {
name: 'n8n Form Trigger',
@@ -54,7 +64,35 @@ const descriptionV2: INodeTypeDescription = {
eventTriggerDescription: 'Waiting for you to submit the form',
activationMessage: 'You can now make calls to your production Form URL.',
triggerPanel: formTriggerPanel,
credentials: [
{
// eslint-disable-next-line n8n-nodes-base/node-class-description-credentials-name-unsuffixed
name: 'httpBasicAuth',
required: true,
displayOptions: {
show: {
[FORM_TRIGGER_AUTHENTICATION_PROPERTY]: ['basicAuth'],
},
},
},
],
properties: [
{
displayName: 'Authentication',
name: FORM_TRIGGER_AUTHENTICATION_PROPERTY,
type: 'options',
options: [
{
name: 'Basic Auth',
value: 'basicAuth',
},
{
name: 'None',
value: 'none',
},
],
default: 'none',
},
webhookPath,
formTitle,
formDescription,
@@ -77,6 +115,17 @@ const descriptionV2: INodeTypeDescription = {
placeholder: 'Add option',
default: {},
options: [
{
displayName: 'Custom Attribution',
name: 'customAttribution',
type: 'string',
placeholder: 'e.g. <svg> ...</svg>',
description: "HTML code that will be shown at the bottom of the form instead n8n's logo",
default: '',
typeOptions: {
rows: 2,
},
},
{
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
displayName: 'Append n8n Attribution',
@@ -94,6 +143,31 @@ const descriptionV2: INodeTypeDescription = {
},
},
},
{
displayName: 'Ignore Bots',
name: 'ignoreBots',
type: 'boolean',
default: false,
description: 'Whether to ignore requests from bots like link previewers and web crawlers',
},
{
...useWorkflowTimezone,
default: false,
displayOptions: {
show: {
'@version': [2],
},
},
},
{
...useWorkflowTimezone,
default: true,
displayOptions: {
show: {
'@version': [{ _cnd: { gt: 2 } }],
},
},
},
],
},
],