mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 10:02:05 +00:00
feat(n8n Form Page Node): New node (#10390)
Co-authored-by: Giulio Andreini <g.andreini@gmail.com> Co-authored-by: Shireen Missi <94372015+ShireenMissi@users.noreply.github.com>
This commit is contained in:
@@ -3,15 +3,27 @@ import type {
|
||||
MultiPartFormData,
|
||||
IDataObject,
|
||||
IWebhookFunctions,
|
||||
FormFieldsParameter,
|
||||
NodeTypeAndVersion,
|
||||
} from 'n8n-workflow';
|
||||
import { NodeOperationError, jsonParse } from 'n8n-workflow';
|
||||
import {
|
||||
FORM_NODE_TYPE,
|
||||
FORM_TRIGGER_NODE_TYPE,
|
||||
NodeOperationError,
|
||||
WAIT_NODE_TYPE,
|
||||
jsonParse,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import type { 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';
|
||||
import { WebhookAuthorizationError } from '../Webhook/error';
|
||||
import { validateWebhookAuthentication } from '../Webhook/utils';
|
||||
import type { FormField, FormTriggerData, FormTriggerInput } from './interfaces';
|
||||
import { FORM_TRIGGER_AUTHENTICATION_PROPERTY } from './interfaces';
|
||||
import type { Response } from 'express';
|
||||
import { getResolvables } from '../../utils/utilities';
|
||||
|
||||
export function prepareFormData({
|
||||
formTitle,
|
||||
@@ -24,17 +36,19 @@ export function prepareFormData({
|
||||
instanceId,
|
||||
useResponseData,
|
||||
appendAttribution = true,
|
||||
buttonLabel,
|
||||
}: {
|
||||
formTitle: string;
|
||||
formDescription: string;
|
||||
formSubmittedText: string | undefined;
|
||||
redirectUrl: string | undefined;
|
||||
formFields: FormField[];
|
||||
formFields: FormFieldsParameter;
|
||||
testRun: boolean;
|
||||
query: IDataObject;
|
||||
instanceId?: string;
|
||||
useResponseData?: boolean;
|
||||
appendAttribution?: boolean;
|
||||
buttonLabel?: string;
|
||||
}) {
|
||||
const validForm = formFields.length > 0;
|
||||
const utm_campaign = instanceId ? `&utm_campaign=${instanceId}` : '';
|
||||
@@ -54,6 +68,7 @@ export function prepareFormData({
|
||||
formFields: [],
|
||||
useResponseData,
|
||||
appendAttribution,
|
||||
buttonLabel,
|
||||
};
|
||||
|
||||
if (redirectUrl) {
|
||||
@@ -138,101 +153,12 @@ const checkResponseModeConfiguration = (context: IWebhookFunctions) => {
|
||||
}
|
||||
};
|
||||
|
||||
export async function formWebhook(
|
||||
export async function prepareFormReturnItem(
|
||||
context: IWebhookFunctions,
|
||||
authProperty = FORM_TRIGGER_AUTHENTICATION_PROPERTY,
|
||||
formFields: FormFieldsParameter,
|
||||
mode: 'test' | 'production',
|
||||
useWorkflowTimezone: boolean = false,
|
||||
) {
|
||||
const node = context.getNode();
|
||||
const options = context.getNodeParameter('options', {}) as {
|
||||
ignoreBots?: boolean;
|
||||
respondWithOptions?: {
|
||||
values: {
|
||||
respondWith: 'text' | 'redirect';
|
||||
formSubmittedText: string;
|
||||
redirectUrl: string;
|
||||
};
|
||||
};
|
||||
formSubmittedText?: string;
|
||||
useWorkflowTimezone?: boolean;
|
||||
appendAttribution?: boolean;
|
||||
};
|
||||
const res = context.getResponseObject();
|
||||
const req = context.getRequestObject();
|
||||
|
||||
try {
|
||||
if (options.ignoreBots && isbot(req.headers['user-agent'])) {
|
||||
throw new WebhookAuthorizationError(403);
|
||||
}
|
||||
if (node.typeVersion > 1) {
|
||||
await validateWebhookAuthentication(context, authProperty);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof WebhookAuthorizationError) {
|
||||
res.setHeader('WWW-Authenticate', 'Basic realm="Enter credentials"');
|
||||
res.status(401).send();
|
||||
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;
|
||||
|
||||
checkResponseModeConfiguration(context);
|
||||
|
||||
//Show the form on GET request
|
||||
if (method === 'GET') {
|
||||
const formTitle = context.getNodeParameter('formTitle', '') 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;
|
||||
|
||||
let formSubmittedText;
|
||||
let redirectUrl;
|
||||
let appendAttribution = true;
|
||||
|
||||
if (options.respondWithOptions) {
|
||||
const values = (options.respondWithOptions as IDataObject).values as IDataObject;
|
||||
if (values.respondWith === 'text') {
|
||||
formSubmittedText = values.formSubmittedText as string;
|
||||
}
|
||||
if (values.respondWith === 'redirect') {
|
||||
redirectUrl = values.redirectUrl as string;
|
||||
}
|
||||
} else {
|
||||
formSubmittedText = options.formSubmittedText as string;
|
||||
}
|
||||
|
||||
if (options.appendAttribution === false) {
|
||||
appendAttribution = false;
|
||||
}
|
||||
|
||||
const useResponseData = responseMode === 'responseNode';
|
||||
|
||||
const query = context.getRequestObject().query as IDataObject;
|
||||
|
||||
const data = prepareFormData({
|
||||
formTitle,
|
||||
formDescription,
|
||||
formSubmittedText,
|
||||
redirectUrl,
|
||||
formFields,
|
||||
testRun: mode === 'test',
|
||||
query,
|
||||
instanceId,
|
||||
useResponseData,
|
||||
appendAttribution,
|
||||
});
|
||||
|
||||
res.render('form-trigger', data);
|
||||
return {
|
||||
noWebhookResponse: true,
|
||||
};
|
||||
}
|
||||
|
||||
const bodyData = (context.getBodyData().data as IDataObject) ?? {};
|
||||
const files = (context.getBodyData().files as IDataObject) ?? {};
|
||||
|
||||
@@ -312,21 +238,233 @@ export async function formWebhook(
|
||||
returnItem.json[field.fieldLabel] = value;
|
||||
}
|
||||
|
||||
const timezone = useWorkflowTimezone ? context.getTimezone() : 'UTC';
|
||||
returnItem.json.submittedAt = DateTime.now().setZone(timezone).toISO();
|
||||
|
||||
returnItem.json.formMode = mode;
|
||||
|
||||
const workflowStaticData = context.getWorkflowStaticData('node');
|
||||
if (
|
||||
Object.keys(workflowStaticData || {}).length &&
|
||||
context.getNode().type === FORM_TRIGGER_NODE_TYPE
|
||||
) {
|
||||
returnItem.json.formQueryParameters = workflowStaticData;
|
||||
}
|
||||
|
||||
return returnItem;
|
||||
}
|
||||
|
||||
export function renderForm({
|
||||
context,
|
||||
res,
|
||||
formTitle,
|
||||
formDescription,
|
||||
formFields,
|
||||
responseMode,
|
||||
mode,
|
||||
formSubmittedText,
|
||||
redirectUrl,
|
||||
appendAttribution,
|
||||
buttonLabel,
|
||||
}: {
|
||||
context: IWebhookFunctions;
|
||||
res: Response;
|
||||
formTitle: string;
|
||||
formDescription: string;
|
||||
formFields: FormFieldsParameter;
|
||||
responseMode: string;
|
||||
mode: 'test' | 'production';
|
||||
formSubmittedText?: string;
|
||||
redirectUrl?: string;
|
||||
appendAttribution?: boolean;
|
||||
buttonLabel?: string;
|
||||
}) {
|
||||
formDescription = (formDescription || '').replace(/\\n/g, '\n').replace(/<br>/g, '\n');
|
||||
const instanceId = context.getInstanceId();
|
||||
|
||||
const useResponseData = responseMode === 'responseNode';
|
||||
|
||||
let query: IDataObject = {};
|
||||
|
||||
if (context.getNode().type === FORM_TRIGGER_NODE_TYPE) {
|
||||
query = context.getRequestObject().query as IDataObject;
|
||||
const workflowStaticData = context.getWorkflowStaticData('node');
|
||||
for (const key of Object.keys(query)) {
|
||||
workflowStaticData[key] = query[key];
|
||||
}
|
||||
} else if (context.getNode().type === FORM_NODE_TYPE) {
|
||||
const parentNodes = context.getParentNodes(context.getNode().name);
|
||||
const trigger = parentNodes.find(
|
||||
(node) => node.type === FORM_TRIGGER_NODE_TYPE,
|
||||
) as NodeTypeAndVersion;
|
||||
try {
|
||||
const triggerQueryParameters = context.evaluateExpression(
|
||||
`{{ $('${trigger?.name}').first().json.formQueryParameters }}`,
|
||||
) as IDataObject;
|
||||
|
||||
if (triggerQueryParameters) {
|
||||
query = triggerQueryParameters;
|
||||
}
|
||||
} catch (error) {}
|
||||
}
|
||||
|
||||
const data = prepareFormData({
|
||||
formTitle,
|
||||
formDescription,
|
||||
formSubmittedText,
|
||||
redirectUrl,
|
||||
formFields,
|
||||
testRun: mode === 'test',
|
||||
query,
|
||||
instanceId,
|
||||
useResponseData,
|
||||
appendAttribution,
|
||||
buttonLabel,
|
||||
});
|
||||
|
||||
res.render('form-trigger', data);
|
||||
}
|
||||
|
||||
export async function formWebhook(
|
||||
context: IWebhookFunctions,
|
||||
authProperty = FORM_TRIGGER_AUTHENTICATION_PROPERTY,
|
||||
) {
|
||||
const node = context.getNode();
|
||||
const options = context.getNodeParameter('options', {}) as {
|
||||
ignoreBots?: boolean;
|
||||
respondWithOptions?: {
|
||||
values: {
|
||||
respondWith: 'text' | 'redirect';
|
||||
formSubmittedText: string;
|
||||
redirectUrl: string;
|
||||
};
|
||||
};
|
||||
formSubmittedText?: string;
|
||||
useWorkflowTimezone?: boolean;
|
||||
appendAttribution?: boolean;
|
||||
buttonLabel?: string;
|
||||
};
|
||||
const res = context.getResponseObject();
|
||||
const req = context.getRequestObject();
|
||||
|
||||
try {
|
||||
if (options.ignoreBots && isbot(req.headers['user-agent'])) {
|
||||
throw new WebhookAuthorizationError(403);
|
||||
}
|
||||
if (node.typeVersion > 1) {
|
||||
await validateWebhookAuthentication(context, authProperty);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof WebhookAuthorizationError) {
|
||||
res.setHeader('WWW-Authenticate', 'Basic realm="Enter credentials"');
|
||||
res.status(401).send();
|
||||
return { noWebhookResponse: true };
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
const mode = context.getMode() === 'manual' ? 'test' : 'production';
|
||||
const formFields = context.getNodeParameter('formFields.values', []) as FormFieldsParameter;
|
||||
const method = context.getRequestObject().method;
|
||||
|
||||
checkResponseModeConfiguration(context);
|
||||
|
||||
//Show the form on GET request
|
||||
if (method === 'GET') {
|
||||
const formTitle = context.getNodeParameter('formTitle', '') as string;
|
||||
const formDescription = context.getNodeParameter('formDescription', '') as string;
|
||||
const responseMode = context.getNodeParameter('responseMode', '') as string;
|
||||
|
||||
let formSubmittedText;
|
||||
let redirectUrl;
|
||||
let appendAttribution = true;
|
||||
|
||||
if (options.respondWithOptions) {
|
||||
const values = (options.respondWithOptions as IDataObject).values as IDataObject;
|
||||
if (values.respondWith === 'text') {
|
||||
formSubmittedText = values.formSubmittedText as string;
|
||||
}
|
||||
if (values.respondWith === 'redirect') {
|
||||
redirectUrl = values.redirectUrl as string;
|
||||
}
|
||||
} else {
|
||||
formSubmittedText = options.formSubmittedText as string;
|
||||
}
|
||||
|
||||
if (options.appendAttribution === false) {
|
||||
appendAttribution = false;
|
||||
}
|
||||
|
||||
let buttonLabel = 'Submit';
|
||||
|
||||
if (options.buttonLabel) {
|
||||
buttonLabel = options.buttonLabel;
|
||||
}
|
||||
|
||||
if (!redirectUrl && node.type !== FORM_TRIGGER_NODE_TYPE) {
|
||||
const connectedNodes = context.getChildNodes(context.getNode().name);
|
||||
const hasNextPage = connectedNodes.some(
|
||||
(n) => n.type === FORM_NODE_TYPE || n.type === WAIT_NODE_TYPE,
|
||||
);
|
||||
|
||||
if (hasNextPage) {
|
||||
redirectUrl = context.evaluateExpression('{{ $execution.resumeFormUrl }}') as string;
|
||||
}
|
||||
}
|
||||
|
||||
renderForm({
|
||||
context,
|
||||
res,
|
||||
formTitle,
|
||||
formDescription,
|
||||
formFields,
|
||||
responseMode,
|
||||
mode,
|
||||
formSubmittedText,
|
||||
redirectUrl,
|
||||
appendAttribution,
|
||||
buttonLabel,
|
||||
});
|
||||
|
||||
return {
|
||||
noWebhookResponse: true,
|
||||
};
|
||||
}
|
||||
|
||||
let { useWorkflowTimezone } = options;
|
||||
|
||||
if (useWorkflowTimezone === undefined && node.typeVersion > 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 };
|
||||
const returnItem = await prepareFormReturnItem(context, formFields, mode, useWorkflowTimezone);
|
||||
|
||||
return {
|
||||
webhookResponse,
|
||||
webhookResponse: { status: 200 },
|
||||
workflowData: [[returnItem]],
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveRawData(context: IWebhookFunctions, rawData: string) {
|
||||
const resolvables = getResolvables(rawData);
|
||||
let returnData: string = rawData;
|
||||
|
||||
if (returnData.startsWith('=')) {
|
||||
returnData = returnData.replace(/^=+/, '');
|
||||
} else {
|
||||
return returnData;
|
||||
}
|
||||
|
||||
if (resolvables.length) {
|
||||
for (const resolvable of resolvables) {
|
||||
const resolvedValue = context.evaluateExpression(`${resolvable}`);
|
||||
|
||||
if (typeof resolvedValue === 'object' && resolvedValue !== null) {
|
||||
returnData = returnData.replace(resolvable, JSON.stringify(resolvedValue));
|
||||
} else {
|
||||
returnData = returnData.replace(resolvable, resolvedValue as string);
|
||||
}
|
||||
}
|
||||
}
|
||||
return returnData;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user