mirror of
https://github.com/n8n-io/n8n.git
synced 2025-02-21 02:56:40 -08:00
fix(n8n Form Node): Redirection update (no-changelog) (#13104)
Co-authored-by: Dana <152518854+dana-gill@users.noreply.github.com>
This commit is contained in:
parent
60ff82f648
commit
755734d349
|
@ -5,6 +5,7 @@ pnpm-lock.yaml
|
||||||
packages/editor-ui/index.html
|
packages/editor-ui/index.html
|
||||||
packages/nodes-base/nodes/**/test
|
packages/nodes-base/nodes/**/test
|
||||||
packages/cli/templates/form-trigger.handlebars
|
packages/cli/templates/form-trigger.handlebars
|
||||||
|
packages/cli/templates/form-trigger-completion.handlebars
|
||||||
cypress/fixtures
|
cypress/fixtures
|
||||||
CHANGELOG.md
|
CHANGELOG.md
|
||||||
.github/pull_request_template.md
|
.github/pull_request_template.md
|
||||||
|
|
|
@ -174,6 +174,22 @@ export class ActiveExecutions {
|
||||||
this.logger.debug('Execution finalized', { executionId });
|
this.logger.debug('Execution finalized', { executionId });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Resolve the response promise in an execution. */
|
||||||
|
resolveExecutionResponsePromise(executionId: string) {
|
||||||
|
// TODO: This should probably be refactored.
|
||||||
|
// The reason for adding this method is that the Form node works in 'responseNode' mode
|
||||||
|
// and expects the next Form to 'sendResponse' to redirect to the current Form node.
|
||||||
|
// Resolving responsePromise here is needed to complete the redirection chain; otherwise, a manual reload will be required.
|
||||||
|
|
||||||
|
if (!this.has(executionId)) return;
|
||||||
|
const execution = this.getExecutionOrFail(executionId);
|
||||||
|
|
||||||
|
if (execution.status !== 'waiting' && execution?.responsePromise) {
|
||||||
|
execution.responsePromise.resolve({});
|
||||||
|
this.logger.debug('Execution response promise cleaned', { executionId });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a promise which will resolve with the data of the execution with the given id
|
* Returns a promise which will resolve with the data of the execution with the given id
|
||||||
*/
|
*/
|
||||||
|
|
97
packages/cli/src/webhooks/__tests__/webhook-helpers.test.ts
Normal file
97
packages/cli/src/webhooks/__tests__/webhook-helpers.test.ts
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
import { mock, type MockProxy } from 'jest-mock-extended';
|
||||||
|
import type { Workflow, INode, IDataObject } from 'n8n-workflow';
|
||||||
|
import { FORM_NODE_TYPE, WAIT_NODE_TYPE } from 'n8n-workflow';
|
||||||
|
|
||||||
|
import { autoDetectResponseMode, handleFormRedirectionCase } from '../webhook-helpers';
|
||||||
|
import type { IWebhookResponseCallbackData } from '../webhook.types';
|
||||||
|
|
||||||
|
describe('autoDetectResponseMode', () => {
|
||||||
|
let workflow: MockProxy<Workflow>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
workflow = mock<Workflow>();
|
||||||
|
workflow.nodes = {};
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return undefined if start node is WAIT_NODE_TYPE with resume not equal to form', () => {
|
||||||
|
const workflowStartNode = mock<INode>({
|
||||||
|
type: WAIT_NODE_TYPE,
|
||||||
|
parameters: { resume: 'webhook' },
|
||||||
|
});
|
||||||
|
const result = autoDetectResponseMode(workflowStartNode, workflow, 'POST');
|
||||||
|
expect(result).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return responseNode when start node is FORM_NODE_TYPE and method is POST', () => {
|
||||||
|
const workflowStartNode = mock<INode>({
|
||||||
|
type: FORM_NODE_TYPE,
|
||||||
|
name: 'startNode',
|
||||||
|
parameters: {},
|
||||||
|
});
|
||||||
|
workflow.getChildNodes.mockReturnValue(['childNode']);
|
||||||
|
workflow.nodes.childNode = mock<INode>({
|
||||||
|
type: WAIT_NODE_TYPE,
|
||||||
|
parameters: { resume: 'form' },
|
||||||
|
disabled: false,
|
||||||
|
});
|
||||||
|
const result = autoDetectResponseMode(workflowStartNode, workflow, 'POST');
|
||||||
|
expect(result).toBe('responseNode');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return undefined when start node is FORM_NODE_TYPE with no other form child nodes', () => {
|
||||||
|
const workflowStartNode = mock<INode>({
|
||||||
|
type: FORM_NODE_TYPE,
|
||||||
|
name: 'startNode',
|
||||||
|
parameters: {},
|
||||||
|
});
|
||||||
|
workflow.getChildNodes.mockReturnValue([]);
|
||||||
|
const result = autoDetectResponseMode(workflowStartNode, workflow, 'POST');
|
||||||
|
expect(result).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return undefined for non-matching node type and method', () => {
|
||||||
|
const workflowStartNode = mock<INode>({ type: 'someOtherNodeType', parameters: {} });
|
||||||
|
const result = autoDetectResponseMode(workflowStartNode, workflow, 'GET');
|
||||||
|
expect(result).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('handleFormRedirectionCase', () => {
|
||||||
|
test('should return data unchanged if start node is WAIT_NODE_TYPE with resume not equal to form', () => {
|
||||||
|
const data: IWebhookResponseCallbackData = {
|
||||||
|
responseCode: 302,
|
||||||
|
headers: { location: 'http://example.com' },
|
||||||
|
};
|
||||||
|
const workflowStartNode = mock<INode>({
|
||||||
|
type: WAIT_NODE_TYPE,
|
||||||
|
parameters: { resume: 'webhook' },
|
||||||
|
});
|
||||||
|
const result = handleFormRedirectionCase(data, workflowStartNode);
|
||||||
|
expect(result).toEqual(data);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should modify data if start node type matches and responseCode is a redirect', () => {
|
||||||
|
const data: IWebhookResponseCallbackData = {
|
||||||
|
responseCode: 302,
|
||||||
|
headers: { location: 'http://example.com' },
|
||||||
|
};
|
||||||
|
const workflowStartNode = mock<INode>({
|
||||||
|
type: FORM_NODE_TYPE,
|
||||||
|
parameters: {},
|
||||||
|
});
|
||||||
|
const result = handleFormRedirectionCase(data, workflowStartNode);
|
||||||
|
expect(result.responseCode).toBe(200);
|
||||||
|
expect(result.data).toEqual({ redirectURL: 'http://example.com' });
|
||||||
|
expect((result?.headers as IDataObject)?.location).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not modify data if location header is missing', () => {
|
||||||
|
const data: IWebhookResponseCallbackData = { responseCode: 302, headers: {} };
|
||||||
|
const workflowStartNode = mock<INode>({
|
||||||
|
type: FORM_NODE_TYPE,
|
||||||
|
parameters: {},
|
||||||
|
});
|
||||||
|
const result = handleFormRedirectionCase(data, workflowStartNode);
|
||||||
|
expect(result).toEqual(data);
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,8 +1,7 @@
|
||||||
import { Service } from '@n8n/di';
|
import { Service } from '@n8n/di';
|
||||||
import axios from 'axios';
|
|
||||||
import type express from 'express';
|
import type express from 'express';
|
||||||
import type { IRunData } from 'n8n-workflow';
|
import type { IRunData } from 'n8n-workflow';
|
||||||
import { FORM_NODE_TYPE, sleep, Workflow } from 'n8n-workflow';
|
import { FORM_NODE_TYPE, Workflow } from 'n8n-workflow';
|
||||||
|
|
||||||
import { ConflictError } from '@/errors/response-errors/conflict.error';
|
import { ConflictError } from '@/errors/response-errors/conflict.error';
|
||||||
import { NotFoundError } from '@/errors/response-errors/not-found.error';
|
import { NotFoundError } from '@/errors/response-errors/not-found.error';
|
||||||
|
@ -39,25 +38,6 @@ export class WaitingForms extends WaitingWebhooks {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async reloadForm(req: WaitingWebhookRequest, res: express.Response) {
|
|
||||||
try {
|
|
||||||
await sleep(1000);
|
|
||||||
|
|
||||||
const url = `${req.protocol}://${req.get('host')}${req.originalUrl}`;
|
|
||||||
const page = await axios({ url });
|
|
||||||
|
|
||||||
if (page) {
|
|
||||||
res.send(`
|
|
||||||
<script>
|
|
||||||
setTimeout(function() {
|
|
||||||
window.location.reload();
|
|
||||||
}, 1);
|
|
||||||
</script>
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
} catch (error) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
findCompletionPage(workflow: Workflow, runData: IRunData, lastNodeExecuted: string) {
|
findCompletionPage(workflow: Workflow, runData: IRunData, lastNodeExecuted: string) {
|
||||||
const parentNodes = workflow.getParentNodes(lastNodeExecuted);
|
const parentNodes = workflow.getParentNodes(lastNodeExecuted);
|
||||||
const lastNode = workflow.nodes[lastNodeExecuted];
|
const lastNode = workflow.nodes[lastNodeExecuted];
|
||||||
|
@ -105,11 +85,6 @@ export class WaitingForms extends WaitingWebhooks {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (execution.status === 'running') {
|
if (execution.status === 'running') {
|
||||||
if (this.includeForms && req.method === 'GET') {
|
|
||||||
await this.reloadForm(req, res);
|
|
||||||
return { noWebhookResponse: true };
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new ConflictError(`The execution "${executionId}" is running already.`);
|
throw new ConflictError(`The execution "${executionId}" is running already.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -37,7 +37,9 @@ import {
|
||||||
createDeferredPromise,
|
createDeferredPromise,
|
||||||
ExecutionCancelledError,
|
ExecutionCancelledError,
|
||||||
FORM_NODE_TYPE,
|
FORM_NODE_TYPE,
|
||||||
|
FORM_TRIGGER_NODE_TYPE,
|
||||||
NodeOperationError,
|
NodeOperationError,
|
||||||
|
WAIT_NODE_TYPE,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import assert from 'node:assert';
|
import assert from 'node:assert';
|
||||||
import { finished } from 'stream/promises';
|
import { finished } from 'stream/promises';
|
||||||
|
@ -101,6 +103,62 @@ export function getWorkflowWebhooks(
|
||||||
return returnData;
|
return returnData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function autoDetectResponseMode(
|
||||||
|
workflowStartNode: INode,
|
||||||
|
workflow: Workflow,
|
||||||
|
method: string,
|
||||||
|
) {
|
||||||
|
if (workflowStartNode.type === WAIT_NODE_TYPE && workflowStartNode.parameters.resume !== 'form') {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
[FORM_NODE_TYPE, FORM_TRIGGER_NODE_TYPE, WAIT_NODE_TYPE].includes(workflowStartNode.type) &&
|
||||||
|
method === 'POST'
|
||||||
|
) {
|
||||||
|
const connectedNodes = workflow.getChildNodes(workflowStartNode.name);
|
||||||
|
|
||||||
|
for (const nodeName of connectedNodes) {
|
||||||
|
const node = workflow.nodes[nodeName];
|
||||||
|
|
||||||
|
if (node.type === WAIT_NODE_TYPE && node.parameters.resume !== 'form') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ([FORM_NODE_TYPE, WAIT_NODE_TYPE].includes(node.type) && !node.disabled) {
|
||||||
|
return 'responseNode';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* for formTrigger and form nodes redirection has to be handled by sending redirectURL in response body
|
||||||
|
*/
|
||||||
|
export const handleFormRedirectionCase = (
|
||||||
|
data: IWebhookResponseCallbackData,
|
||||||
|
workflowStartNode: INode,
|
||||||
|
) => {
|
||||||
|
if (workflowStartNode.type === WAIT_NODE_TYPE && workflowStartNode.parameters.resume !== 'form') {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
[FORM_NODE_TYPE, FORM_TRIGGER_NODE_TYPE, WAIT_NODE_TYPE].includes(workflowStartNode.type) &&
|
||||||
|
(data?.headers as IDataObject)?.location &&
|
||||||
|
String(data?.responseCode).startsWith('3')
|
||||||
|
) {
|
||||||
|
data.responseCode = 200;
|
||||||
|
data.data = {
|
||||||
|
redirectURL: (data?.headers as IDataObject)?.location,
|
||||||
|
};
|
||||||
|
(data.headers as IDataObject).location = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
const { formDataFileSizeMax } = Container.get(GlobalConfig).endpoints;
|
const { formDataFileSizeMax } = Container.get(GlobalConfig).endpoints;
|
||||||
const parseFormData = createMultiFormDataParser(formDataFileSizeMax);
|
const parseFormData = createMultiFormDataParser(formDataFileSizeMax);
|
||||||
|
|
||||||
|
@ -154,23 +212,8 @@ export async function executeWebhook(
|
||||||
// Get the responseMode
|
// Get the responseMode
|
||||||
let responseMode;
|
let responseMode;
|
||||||
|
|
||||||
// if this is n8n FormTrigger node, check if there is a Form node in child nodes,
|
//check if response mode should be set automatically, e.g. multipage form
|
||||||
// if so, set 'responseMode' to 'formPage' to redirect to URL of that Form later
|
responseMode = autoDetectResponseMode(workflowStartNode, workflow, req.method);
|
||||||
if (nodeType.description.name === 'formTrigger') {
|
|
||||||
const connectedNodes = workflow.getChildNodes(workflowStartNode.name);
|
|
||||||
let hasNextPage = false;
|
|
||||||
for (const nodeName of connectedNodes) {
|
|
||||||
const node = workflow.nodes[nodeName];
|
|
||||||
if (node.type === FORM_NODE_TYPE && !node.disabled) {
|
|
||||||
hasNextPage = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasNextPage) {
|
|
||||||
responseMode = 'formPage';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!responseMode) {
|
if (!responseMode) {
|
||||||
responseMode = workflow.expression.getSimpleParameterValue(
|
responseMode = workflow.expression.getSimpleParameterValue(
|
||||||
|
@ -201,7 +244,7 @@ export async function executeWebhook(
|
||||||
'firstEntryJson',
|
'firstEntryJson',
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!['onReceived', 'lastNode', 'responseNode', 'formPage'].includes(responseMode)) {
|
if (!['onReceived', 'lastNode', 'responseNode'].includes(responseMode)) {
|
||||||
// If the mode is not known we error. Is probably best like that instead of using
|
// If the mode is not known we error. Is probably best like that instead of using
|
||||||
// the default that people know as early as possible (probably already testing phase)
|
// the default that people know as early as possible (probably already testing phase)
|
||||||
// that something does not resolve properly.
|
// that something does not resolve properly.
|
||||||
|
@ -497,28 +540,16 @@ export async function executeWebhook(
|
||||||
} else {
|
} else {
|
||||||
// TODO: This probably needs some more changes depending on the options on the
|
// TODO: This probably needs some more changes depending on the options on the
|
||||||
// Webhook Response node
|
// Webhook Response node
|
||||||
const headers = response.headers;
|
|
||||||
let responseCode = response.statusCode;
|
|
||||||
let data = response.body as IDataObject;
|
|
||||||
|
|
||||||
// for formTrigger node redirection has to be handled by sending redirectURL in response body
|
let data: IWebhookResponseCallbackData = {
|
||||||
if (
|
data: response.body as IDataObject,
|
||||||
nodeType.description.name === 'formTrigger' &&
|
headers: response.headers,
|
||||||
headers.location &&
|
responseCode: response.statusCode,
|
||||||
String(responseCode).startsWith('3')
|
};
|
||||||
) {
|
|
||||||
responseCode = 200;
|
|
||||||
data = {
|
|
||||||
redirectURL: headers.location,
|
|
||||||
};
|
|
||||||
headers.location = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
responseCallback(null, {
|
data = handleFormRedirectionCase(data, workflowStartNode);
|
||||||
data,
|
|
||||||
headers,
|
responseCallback(null, data);
|
||||||
responseCode,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
process.nextTick(() => res.end());
|
process.nextTick(() => res.end());
|
||||||
|
@ -552,12 +583,6 @@ export async function executeWebhook(
|
||||||
responsePromise,
|
responsePromise,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (responseMode === 'formPage' && !didSendResponse) {
|
|
||||||
res.redirect(`${additionalData.formWaitingBaseUrl}/${executionId}`);
|
|
||||||
process.nextTick(() => res.end());
|
|
||||||
didSendResponse = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
Container.get(Logger).debug(
|
Container.get(Logger).debug(
|
||||||
`Started execution of workflow "${workflow.name}" from webhook with execution ID ${executionId}`,
|
`Started execution of workflow "${workflow.name}" from webhook with execution ID ${executionId}`,
|
||||||
{ executionId },
|
{ executionId },
|
||||||
|
|
|
@ -296,6 +296,7 @@ export class WorkflowRunner {
|
||||||
fullRunData.finished = false;
|
fullRunData.finished = false;
|
||||||
}
|
}
|
||||||
fullRunData.status = this.activeExecutions.getStatus(executionId);
|
fullRunData.status = this.activeExecutions.getStatus(executionId);
|
||||||
|
this.activeExecutions.resolveExecutionResponsePromise(executionId);
|
||||||
this.activeExecutions.finalizeExecution(executionId, fullRunData);
|
this.activeExecutions.finalizeExecution(executionId, fullRunData);
|
||||||
})
|
})
|
||||||
.catch(
|
.catch(
|
||||||
|
|
|
@ -28,6 +28,8 @@
|
||||||
<body>
|
<body>
|
||||||
{{#if responseText}}
|
{{#if responseText}}
|
||||||
{{{responseText}}}
|
{{{responseText}}}
|
||||||
|
{{else if redirectUrl}}
|
||||||
|
<div>Redirecting to <a href='{{redirectUrl}}'>{{redirectUrl}}</a></div>
|
||||||
{{else}}
|
{{else}}
|
||||||
<div class='container'>
|
<div class='container'>
|
||||||
<section>
|
<section>
|
||||||
|
@ -73,9 +75,40 @@
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
<script>
|
{{#if redirectUrl}}
|
||||||
fetch('', { method: 'POST', body: {}, }).catch(() => {});
|
<a id='redirectUrl' href='{{redirectUrl}}' style='display: none;'></a>
|
||||||
</script>
|
{{/if}}
|
||||||
|
<script>
|
||||||
|
fetch('', {
|
||||||
|
method: 'POST',
|
||||||
|
body: {}
|
||||||
|
})
|
||||||
|
.then(async function (response) {
|
||||||
|
if (response.status === 200) {
|
||||||
|
const redirectUrl = document.getElementById("redirectUrl");
|
||||||
|
if (redirectUrl) {
|
||||||
|
window.location.replace(redirectUrl.href);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = await response.text();
|
||||||
|
let json;
|
||||||
|
|
||||||
|
try {
|
||||||
|
json = JSON.parse(text);
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
if (json?.redirectURL) {
|
||||||
|
const url = json.redirectURL.includes("://")
|
||||||
|
? json.redirectURL
|
||||||
|
: "https://" + json.redirectURL;
|
||||||
|
window.location.replace(url);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(function (error) {
|
||||||
|
console.error('Error:', error);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -779,14 +779,20 @@
|
||||||
if (json?.redirectURL) {
|
if (json?.redirectURL) {
|
||||||
const url = json.redirectURL.includes("://") ? json.redirectURL : "https://" + json.redirectURL;
|
const url = json.redirectURL.includes("://") ? json.redirectURL : "https://" + json.redirectURL;
|
||||||
window.location.replace(url);
|
window.location.replace(url);
|
||||||
} else if (json?.formSubmittedText) {
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (json?.formSubmittedText) {
|
||||||
form.style.display = 'none';
|
form.style.display = 'none';
|
||||||
document.querySelector('#submitted-form').style.display = 'block';
|
document.querySelector('#submitted-form').style.display = 'block';
|
||||||
document.querySelector('#submitted-content').textContent = json.formSubmittedText;
|
document.querySelector('#submitted-content').textContent = json.formSubmittedText;
|
||||||
} else {
|
return;
|
||||||
document.body.innerHTML = text;
|
}
|
||||||
|
|
||||||
|
if (text) {
|
||||||
|
document.body.innerHTML = text;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
|
@ -814,18 +820,6 @@
|
||||||
.catch(function (error) {
|
.catch(function (error) {
|
||||||
console.error('Error:', error);
|
console.error('Error:', error);
|
||||||
});
|
});
|
||||||
|
|
||||||
const isWaitingForm = window.location.href.includes('form-waiting');
|
|
||||||
if(isWaitingForm) {
|
|
||||||
const interval = setInterval(function() {
|
|
||||||
const isSubmited = document.querySelector('#submitted-form').style.display;
|
|
||||||
if(isSubmited === 'block') {
|
|
||||||
clearInterval(interval);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
window.location.reload();
|
|
||||||
}, 2000);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -243,7 +243,7 @@ export class Form extends Node {
|
||||||
{
|
{
|
||||||
name: 'default',
|
name: 'default',
|
||||||
httpMethod: 'POST',
|
httpMethod: 'POST',
|
||||||
responseMode: 'onReceived',
|
responseMode: 'responseNode',
|
||||||
path: '',
|
path: '',
|
||||||
restartWebhook: true,
|
restartWebhook: true,
|
||||||
isFullPath: true,
|
isFullPath: true,
|
||||||
|
@ -384,6 +384,13 @@ export class Form extends Node {
|
||||||
const waitTill = configureWaitTillDate(context, 'root');
|
const waitTill = configureWaitTillDate(context, 'root');
|
||||||
await context.putExecutionToWait(waitTill);
|
await context.putExecutionToWait(waitTill);
|
||||||
|
|
||||||
|
context.sendResponse({
|
||||||
|
headers: {
|
||||||
|
location: context.evaluateExpression('{{ $execution.resumeFormUrl }}', 0),
|
||||||
|
},
|
||||||
|
statusCode: 307,
|
||||||
|
});
|
||||||
|
|
||||||
return [context.getInputData()];
|
return [context.getInputData()];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,13 +18,6 @@ export const renderFormCompletion = async (
|
||||||
const options = context.getNodeParameter('options', {}) as { formTitle: string };
|
const options = context.getNodeParameter('options', {}) as { formTitle: string };
|
||||||
const responseText = context.getNodeParameter('responseText', '') as string;
|
const responseText = context.getNodeParameter('responseText', '') as string;
|
||||||
|
|
||||||
if (redirectUrl) {
|
|
||||||
res.send(
|
|
||||||
`<html><head><meta http-equiv="refresh" content="0; url=${redirectUrl}"></head></html>`,
|
|
||||||
);
|
|
||||||
return { noWebhookResponse: true };
|
|
||||||
}
|
|
||||||
|
|
||||||
let title = options.formTitle;
|
let title = options.formTitle;
|
||||||
if (!title) {
|
if (!title) {
|
||||||
title = context.evaluateExpression(`{{ $('${trigger?.name}').params.formTitle }}`) as string;
|
title = context.evaluateExpression(`{{ $('${trigger?.name}').params.formTitle }}`) as string;
|
||||||
|
@ -39,6 +32,7 @@ export const renderFormCompletion = async (
|
||||||
formTitle: title,
|
formTitle: title,
|
||||||
appendAttribution,
|
appendAttribution,
|
||||||
responseText: sanitizeHtml(responseText),
|
responseText: sanitizeHtml(responseText),
|
||||||
|
redirectUrl,
|
||||||
});
|
});
|
||||||
|
|
||||||
return { noWebhookResponse: true };
|
return { noWebhookResponse: true };
|
||||||
|
|
|
@ -2,8 +2,6 @@ import { type Response } from 'express';
|
||||||
import {
|
import {
|
||||||
type NodeTypeAndVersion,
|
type NodeTypeAndVersion,
|
||||||
type IWebhookFunctions,
|
type IWebhookFunctions,
|
||||||
FORM_NODE_TYPE,
|
|
||||||
WAIT_NODE_TYPE,
|
|
||||||
type FormFieldsParameter,
|
type FormFieldsParameter,
|
||||||
type IWebhookResponseData,
|
type IWebhookResponseData,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
@ -43,20 +41,6 @@ export const renderFormNode = async (
|
||||||
) as string) || 'Submit';
|
) as string) || 'Submit';
|
||||||
}
|
}
|
||||||
|
|
||||||
const responseMode = 'onReceived';
|
|
||||||
|
|
||||||
let redirectUrl;
|
|
||||||
|
|
||||||
const connectedNodes = context.getChildNodes(context.getNode().name);
|
|
||||||
|
|
||||||
const hasNextPage = connectedNodes.some(
|
|
||||||
(node) => !node.disabled && (node.type === FORM_NODE_TYPE || node.type === WAIT_NODE_TYPE),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (hasNextPage) {
|
|
||||||
redirectUrl = context.evaluateExpression('{{ $execution.resumeFormUrl }}') as string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const appendAttribution = context.evaluateExpression(
|
const appendAttribution = context.evaluateExpression(
|
||||||
`{{ $('${trigger?.name}').params.options?.appendAttribution === false ? false : true }}`,
|
`{{ $('${trigger?.name}').params.options?.appendAttribution === false ? false : true }}`,
|
||||||
) as boolean;
|
) as boolean;
|
||||||
|
@ -67,9 +51,9 @@ export const renderFormNode = async (
|
||||||
formTitle: title,
|
formTitle: title,
|
||||||
formDescription: description,
|
formDescription: description,
|
||||||
formFields: fields,
|
formFields: fields,
|
||||||
responseMode,
|
responseMode: 'responseNode',
|
||||||
mode,
|
mode,
|
||||||
redirectUrl,
|
redirectUrl: undefined,
|
||||||
appendAttribution,
|
appendAttribution,
|
||||||
buttonLabel,
|
buttonLabel,
|
||||||
});
|
});
|
||||||
|
|
|
@ -166,7 +166,7 @@ describe('Form Node', () => {
|
||||||
formTitle: 'Form Title',
|
formTitle: 'Form Title',
|
||||||
n8nWebsiteLink: 'https://n8n.io/?utm_source=n8n-internal&utm_medium=form-trigger',
|
n8nWebsiteLink: 'https://n8n.io/?utm_source=n8n-internal&utm_medium=form-trigger',
|
||||||
testRun: true,
|
testRun: true,
|
||||||
useResponseData: false,
|
useResponseData: true,
|
||||||
validForm: true,
|
validForm: true,
|
||||||
formSubmittedHeader: undefined,
|
formSubmittedHeader: undefined,
|
||||||
});
|
});
|
||||||
|
@ -230,6 +230,7 @@ describe('Form Node', () => {
|
||||||
appendAttribution: 'test',
|
appendAttribution: 'test',
|
||||||
formTitle: 'test',
|
formTitle: 'test',
|
||||||
message: 'Test Message',
|
message: 'Test Message',
|
||||||
|
redirectUrl: '',
|
||||||
title: 'Test Title',
|
title: 'Test Title',
|
||||||
responseText: '',
|
responseText: '',
|
||||||
},
|
},
|
||||||
|
@ -242,6 +243,7 @@ describe('Form Node', () => {
|
||||||
appendAttribution: 'test',
|
appendAttribution: 'test',
|
||||||
formTitle: 'test',
|
formTitle: 'test',
|
||||||
message: 'Test Message',
|
message: 'Test Message',
|
||||||
|
redirectUrl: '',
|
||||||
title: 'Test Title',
|
title: 'Test Title',
|
||||||
responseText: '<div>hey</div>',
|
responseText: '<div>hey</div>',
|
||||||
},
|
},
|
||||||
|
@ -254,6 +256,7 @@ describe('Form Node', () => {
|
||||||
appendAttribution: 'test',
|
appendAttribution: 'test',
|
||||||
formTitle: 'test',
|
formTitle: 'test',
|
||||||
message: 'Test Message',
|
message: 'Test Message',
|
||||||
|
redirectUrl: '',
|
||||||
title: 'Test Title',
|
title: 'Test Title',
|
||||||
responseText: 'my text over here',
|
responseText: 'my text over here',
|
||||||
},
|
},
|
||||||
|
@ -340,9 +343,14 @@ describe('Form Node', () => {
|
||||||
const result = await form.webhook(mockWebhookFunctions);
|
const result = await form.webhook(mockWebhookFunctions);
|
||||||
|
|
||||||
expect(result).toEqual({ noWebhookResponse: true });
|
expect(result).toEqual({ noWebhookResponse: true });
|
||||||
expect(mockResponseObject.send).toHaveBeenCalledWith(
|
expect(mockResponseObject.render).toHaveBeenCalledWith('form-trigger-completion', {
|
||||||
'<html><head><meta http-equiv="refresh" content="0; url=https://n8n.io"></head></html>',
|
appendAttribution: 'test',
|
||||||
);
|
formTitle: 'test',
|
||||||
|
message: 'Test Message',
|
||||||
|
redirectUrl: 'https://n8n.io',
|
||||||
|
responseText: '',
|
||||||
|
title: 'Test Title',
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -476,7 +476,7 @@ export async function formWebhook(
|
||||||
if (method === 'GET') {
|
if (method === 'GET') {
|
||||||
const formTitle = context.getNodeParameter('formTitle', '') as string;
|
const formTitle = context.getNodeParameter('formTitle', '') as string;
|
||||||
const formDescription = sanitizeHtml(context.getNodeParameter('formDescription', '') as string);
|
const formDescription = sanitizeHtml(context.getNodeParameter('formDescription', '') as string);
|
||||||
const responseMode = context.getNodeParameter('responseMode', '') as string;
|
let responseMode = context.getNodeParameter('responseMode', '') as string;
|
||||||
|
|
||||||
let formSubmittedText;
|
let formSubmittedText;
|
||||||
let redirectUrl;
|
let redirectUrl;
|
||||||
|
@ -504,15 +504,14 @@ export async function formWebhook(
|
||||||
buttonLabel = options.buttonLabel;
|
buttonLabel = options.buttonLabel;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!redirectUrl && node.type !== FORM_TRIGGER_NODE_TYPE) {
|
const connectedNodes = context.getChildNodes(context.getNode().name, {
|
||||||
const connectedNodes = context.getChildNodes(context.getNode().name, {
|
includeNodeParameters: true,
|
||||||
includeNodeParameters: true,
|
});
|
||||||
});
|
const hasNextPage = isFormConnected(connectedNodes);
|
||||||
const hasNextPage = isFormConnected(connectedNodes);
|
|
||||||
|
|
||||||
if (hasNextPage) {
|
if (hasNextPage) {
|
||||||
redirectUrl = context.evaluateExpression('{{ $execution.resumeFormUrl }}') as string;
|
redirectUrl = undefined;
|
||||||
}
|
responseMode = 'responseNode';
|
||||||
}
|
}
|
||||||
|
|
||||||
renderForm({
|
renderForm({
|
||||||
|
|
|
@ -7,7 +7,12 @@ import type {
|
||||||
IDisplayOptions,
|
IDisplayOptions,
|
||||||
IWebhookFunctions,
|
IWebhookFunctions,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import { NodeOperationError, NodeConnectionType, WAIT_INDEFINITELY } from 'n8n-workflow';
|
import {
|
||||||
|
NodeOperationError,
|
||||||
|
NodeConnectionType,
|
||||||
|
WAIT_INDEFINITELY,
|
||||||
|
FORM_TRIGGER_NODE_TYPE,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
import { updateDisplayOptions } from '../../utils/utilities';
|
import { updateDisplayOptions } from '../../utils/utilities';
|
||||||
import {
|
import {
|
||||||
|
@ -459,7 +464,25 @@ export class Wait extends Webhook {
|
||||||
const resume = context.getNodeParameter('resume', 0) as string;
|
const resume = context.getNodeParameter('resume', 0) as string;
|
||||||
|
|
||||||
if (['webhook', 'form'].includes(resume)) {
|
if (['webhook', 'form'].includes(resume)) {
|
||||||
return await this.configureAndPutToWait(context);
|
let hasFormTrigger = false;
|
||||||
|
|
||||||
|
if (resume === 'form') {
|
||||||
|
const parentNodes = context.getParentNodes(context.getNode().name);
|
||||||
|
hasFormTrigger = parentNodes.some((node) => node.type === FORM_TRIGGER_NODE_TYPE);
|
||||||
|
}
|
||||||
|
|
||||||
|
const returnData = await this.configureAndPutToWait(context);
|
||||||
|
|
||||||
|
if (resume === 'form' && hasFormTrigger) {
|
||||||
|
context.sendResponse({
|
||||||
|
headers: {
|
||||||
|
location: context.evaluateExpression('{{ $execution.resumeFormUrl }}', 0),
|
||||||
|
},
|
||||||
|
statusCode: 307,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return returnData;
|
||||||
}
|
}
|
||||||
|
|
||||||
let waitTill: Date;
|
let waitTill: Date;
|
||||||
|
|
|
@ -2031,7 +2031,7 @@ export interface IWebhookResponseData {
|
||||||
}
|
}
|
||||||
|
|
||||||
export type WebhookResponseData = 'allEntries' | 'firstEntryJson' | 'firstEntryBinary' | 'noData';
|
export type WebhookResponseData = 'allEntries' | 'firstEntryJson' | 'firstEntryBinary' | 'noData';
|
||||||
export type WebhookResponseMode = 'onReceived' | 'lastNode' | 'responseNode' | 'formPage';
|
export type WebhookResponseMode = 'onReceived' | 'lastNode' | 'responseNode';
|
||||||
|
|
||||||
export interface INodeTypes {
|
export interface INodeTypes {
|
||||||
getByName(nodeType: string): INodeType | IVersionedNodeType;
|
getByName(nodeType: string): INodeType | IVersionedNodeType;
|
||||||
|
|
Loading…
Reference in a new issue