mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-24 20:24:05 -08: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:
parent
869b8f14ca
commit
3ddc176dfa
|
@ -4,5 +4,6 @@ package.json
|
|||
pnpm-lock.yaml
|
||||
packages/editor-ui/index.html
|
||||
packages/nodes-base/nodes/**/test
|
||||
packages/cli/templates/form-trigger.handlebars
|
||||
cypress/fixtures
|
||||
CHANGELOG.md
|
||||
|
|
88
cypress/e2e/16-form-trigger-node.cy.ts
Normal file
88
cypress/e2e/16-form-trigger-node.cy.ts
Normal file
|
@ -0,0 +1,88 @@
|
|||
import { WorkflowPage, NDV } from '../pages';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { getPopper, getVisiblePopper, getVisibleSelect } from '../utils';
|
||||
import { META_KEY } from '../constants';
|
||||
|
||||
const workflowPage = new WorkflowPage();
|
||||
const ndv = new NDV();
|
||||
|
||||
describe('n8n Form Trigger', () => {
|
||||
beforeEach(() => {
|
||||
workflowPage.actions.visit();
|
||||
});
|
||||
|
||||
it("add node by clicking on 'On form submission'", () => {
|
||||
workflowPage.getters.canvasPlusButton().click();
|
||||
cy.get('#node-view-root > div:nth-child(2) > div > div > aside ')
|
||||
.find('span')
|
||||
.contains('On form submission')
|
||||
.click();
|
||||
ndv.getters.parameterInput('formTitle').type('Test Form');
|
||||
ndv.getters.parameterInput('formDescription').type('Test Form Description');
|
||||
ndv.getters.parameterInput('fieldLabel').type('Test Field 1');
|
||||
ndv.getters.backToCanvas().click();
|
||||
workflowPage.getters.nodeIssuesByName('n8n Form Trigger').should('not.exist');
|
||||
});
|
||||
|
||||
it('should fill up form fields', () => {
|
||||
workflowPage.actions.addInitialNodeToCanvas('n8n Form Trigger');
|
||||
workflowPage.getters.canvasNodes().first().dblclick();
|
||||
ndv.getters.parameterInput('formTitle').type('Test Form');
|
||||
ndv.getters.parameterInput('formDescription').type('Test Form Description');
|
||||
//fill up first field of type number
|
||||
ndv.getters.parameterInput('fieldLabel').type('Test Field 1');
|
||||
ndv.getters.parameterInput('fieldType').click();
|
||||
getVisibleSelect().contains('Number').click();
|
||||
cy.get(
|
||||
'[data-test-id="parameter-input-requiredField"] > .parameter-input > .el-switch > .el-switch__core',
|
||||
).click();
|
||||
//fill up second field of type text
|
||||
cy.get('.fixed-collection-parameter > :nth-child(2) > .button > span').click();
|
||||
cy.get('.border-top-dashed > .parameter-input-list-wrapper > :nth-child(1) > .parameter-item')
|
||||
.find('input[placeholder*="e.g. What is your name?"]')
|
||||
.type('Test Field 2');
|
||||
//fill up second field of type date
|
||||
cy.get('.fixed-collection-parameter > :nth-child(2) > .button > span').click();
|
||||
cy.get(
|
||||
':nth-child(3) > .border-top-dashed > .parameter-input-list-wrapper > :nth-child(1) > .parameter-item',
|
||||
)
|
||||
.find('input[placeholder*="e.g. What is your name?"]')
|
||||
.type('Test Field 3');
|
||||
cy.get(
|
||||
':nth-child(3) > .border-top-dashed > .parameter-input-list-wrapper > :nth-child(2) > .parameter-item',
|
||||
).click();
|
||||
getVisibleSelect().contains('Date').click();
|
||||
// fill up second field of type dropdown
|
||||
cy.get('.fixed-collection-parameter > :nth-child(2) > .button').click();
|
||||
cy.get(
|
||||
':nth-child(4) > .border-top-dashed > .parameter-input-list-wrapper > :nth-child(1) > .parameter-item',
|
||||
)
|
||||
.find('input[placeholder*="e.g. What is your name?"]')
|
||||
.type('Test Field 4');
|
||||
cy.get(
|
||||
':nth-child(4) > .border-top-dashed > .parameter-input-list-wrapper > :nth-child(2) > .parameter-item',
|
||||
).click();
|
||||
getVisibleSelect().contains('Dropdown').click();
|
||||
cy.get(
|
||||
'.border-top-dashed > :nth-child(2) > :nth-child(3) > .multi-parameter > .fixed-collection-parameter > :nth-child(2) > .button',
|
||||
).click();
|
||||
cy.get(
|
||||
':nth-child(4) > :nth-child(1) > :nth-child(2) > :nth-child(3) > .multi-parameter > .fixed-collection-parameter > .fixed-collection-parameter-property > :nth-child(1) > :nth-child(1)',
|
||||
)
|
||||
.find('input')
|
||||
.type('Option 1');
|
||||
cy.get(
|
||||
':nth-child(4) > :nth-child(1) > :nth-child(2) > :nth-child(3) > .multi-parameter > .fixed-collection-parameter > .fixed-collection-parameter-property > :nth-child(1) > :nth-child(2)',
|
||||
)
|
||||
.find('input')
|
||||
.type('Option 2');
|
||||
//add optionall submitted message
|
||||
cy.get('.param-options > .button').click();
|
||||
cy.get('.indent > .parameter-item')
|
||||
.find('input')
|
||||
.clear()
|
||||
.type('Your test form was successfully submitted');
|
||||
ndv.getters.backToCanvas().click();
|
||||
workflowPage.getters.nodeIssuesByName('n8n Form Trigger').should('not.exist');
|
||||
});
|
||||
});
|
|
@ -38,6 +38,7 @@ import {
|
|||
BINARY_ENCODING,
|
||||
createDeferredPromise,
|
||||
ErrorReporterProxy as ErrorReporter,
|
||||
FORM_TRIGGER_PATH_IDENTIFIER,
|
||||
LoggerProxy as Logger,
|
||||
NodeHelpers,
|
||||
} from 'n8n-workflow';
|
||||
|
@ -109,7 +110,16 @@ export const webhookRequestHandler =
|
|||
try {
|
||||
response = await webhookManager.executeWebhook(req, res);
|
||||
} catch (error) {
|
||||
return ResponseHelper.sendErrorResponse(res, error as Error);
|
||||
if (
|
||||
error.errorCode === 404 &&
|
||||
(error.message as string).includes(FORM_TRIGGER_PATH_IDENTIFIER)
|
||||
) {
|
||||
const isTestWebhook = req.originalUrl.includes('webhook-test');
|
||||
res.status(404);
|
||||
return res.render('form-trigger-404', { isTestWebhook });
|
||||
} else {
|
||||
return ResponseHelper.sendErrorResponse(res, error as Error);
|
||||
}
|
||||
}
|
||||
|
||||
// Don't respond, if already responded
|
||||
|
|
86
packages/cli/templates/form-trigger-404.handlebars
Normal file
86
packages/cli/templates/form-trigger-404.handlebars
Normal file
|
@ -0,0 +1,86 @@
|
|||
<html lang='en'>
|
||||
|
||||
<head>
|
||||
<meta charset='UTF-8' />
|
||||
<meta name='viewport' content='width=device-width, initial-scale=1.0' />
|
||||
<link rel='icon' type='image/png' href='https://n8n.io/favicon.ico' />
|
||||
<link
|
||||
href='http://fonts.googleapis.com/css?family=Open+Sans'
|
||||
rel='stylesheet'
|
||||
type='text/css'
|
||||
/>
|
||||
{{#if isTestWebhook}}
|
||||
<title>Form Trigger isn't listening yet</title>
|
||||
{{else}}
|
||||
<title>Problem loading form</title>
|
||||
{{/if}}
|
||||
<style>
|
||||
*, ::after, ::before { box-sizing: border-box; margin: 0; padding: 0; } body { font-family:
|
||||
Open Sans, sans-serif; font-weight: 400; font-size: 12px; display: flex; flex-direction:
|
||||
column; justify-content: start; background-color: #FBFCFE; } .container { margin: auto;
|
||||
text-align: center; padding-top: 24px; width: 448px; } .card { padding: 24px;
|
||||
background-color: white; border: 1px solid #DBDFE7; border-radius: 8px; box-shadow: 0px 4px
|
||||
16px 0px #634DFF0F; margin-bottom: 16px; } .n8n-link a { color: #7E8186; font-weight: 600;
|
||||
font-size: 12px; text-decoration: none; } .n8n-link svg { display: inline-block;
|
||||
vertical-align: middle; } .header h1 { color: #525356; font-size: 20px; font-weight: 400;
|
||||
padding-bottom: 8px; } .header p { color: #7E8186; font-size: 14px; font-weight: 400; }
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class='container'>
|
||||
<section>
|
||||
<div class='card'>
|
||||
{{#if isTestWebhook}}
|
||||
<div class='header'>
|
||||
<h1>Form Trigger isn't listening yet</h1>
|
||||
<p>Click the <strong>"Test Step"</strong> button in your form trigger</p>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class='header'>
|
||||
<h1>Problem loading form</h1>
|
||||
<p>This usually occurs if the n8n workflow serving this form is deactivated or no
|
||||
longer exist</p>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class='n8n-link'>
|
||||
<a
|
||||
href='https://n8n.io/?utm_source=n8n-internal&utm_medium=form-trigger&'
|
||||
target='_blank'
|
||||
>
|
||||
Form automated with
|
||||
<svg
|
||||
width='73'
|
||||
height='20'
|
||||
viewBox='0 0 73 20'
|
||||
fill='none'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
>
|
||||
<path
|
||||
fill-rule='evenodd'
|
||||
clip-rule='evenodd'
|
||||
d='M40.2373 4C40.2373 6.20915 38.4464 8 36.2373 8C34.3735 8 32.8074 6.72525 32.3633 5H26.7787C25.801 5 24.9666 5.70685 24.8059 6.6712L24.6415 7.6576C24.4854 8.59415 24.0116 9.40925 23.3417 10C24.0116 10.5907 24.4854 11.4058 24.6415 12.3424L24.8059 13.3288C24.9666 14.2931 25.801 15 26.7787 15H28.3633C28.8074 13.2747 30.3735 12 32.2373 12C34.4464 12 36.2373 13.7908 36.2373 16C36.2373 18.2092 34.4464 20 32.2373 20C30.3735 20 28.8074 18.7253 28.3633 17H26.7787C24.8233 17 23.1546 15.5864 22.8331 13.6576L22.6687 12.6712C22.508 11.7069 21.6736 11 20.6959 11H19.0645C18.5652 12.64 17.0406 13.8334 15.2373 13.8334C13.434 13.8334 11.9094 12.64 11.4101 11H9.06449C8.56519 12.64 7.04059 13.8334 5.2373 13.8334C3.02817 13.8334 1.2373 12.0424 1.2373 9.83335C1.2373 7.6242 3.02817 5.83335 5.2373 5.83335C7.16069 5.83335 8.76699 7.19085 9.15039 9H11.3242C11.7076 7.19085 13.3139 5.83335 15.2373 5.83335C17.1607 5.83335 18.767 7.19085 19.1504 9H20.6959C21.6736 9 22.508 8.29315 22.6687 7.3288L22.8331 6.3424C23.1546 4.41365 24.8233 3 26.7787 3H32.3633C32.8074 1.27478 34.3735 0 36.2373 0C38.4464 0 40.2373 1.79086 40.2373 4ZM38.2373 4C38.2373 5.10455 37.3419 6 36.2373 6C35.1327 6 34.2373 5.10455 34.2373 4C34.2373 2.89543 35.1327 2 36.2373 2C37.3419 2 38.2373 2.89543 38.2373 4ZM5.2373 11.8334C6.34189 11.8334 7.23729 10.9379 7.23729 9.83335C7.23729 8.72875 6.34189 7.83335 5.2373 7.83335C4.13273 7.83335 3.2373 8.72875 3.2373 9.83335C3.2373 10.9379 4.13273 11.8334 5.2373 11.8334ZM15.2373 11.8334C16.3419 11.8334 17.2373 10.9379 17.2373 9.83335C17.2373 8.72875 16.3419 7.83335 15.2373 7.83335C14.1327 7.83335 13.2373 8.72875 13.2373 9.83335C13.2373 10.9379 14.1327 11.8334 15.2373 11.8334ZM32.2373 18C33.3419 18 34.2373 17.1045 34.2373 16C34.2373 14.8954 33.3419 14 32.2373 14C31.1327 14 30.2373 14.8954 30.2373 16C30.2373 17.1045 31.1327 18 32.2373 18Z'
|
||||
fill='#EA4B71'
|
||||
></path>
|
||||
<path
|
||||
d='M44.2393 15.0007H46.3277V10.5791C46.3277 9.12704 47.2088 8.49074 48.204 8.49074C49.183 8.49074 49.9498 9.14334 49.9498 10.4812V15.0007H52.038V10.057C52.038 7.91969 50.798 6.67969 48.8567 6.67969C47.633 6.67969 46.9477 7.16914 46.4582 7.80544H46.3277L46.1482 6.84284H44.2393V15.0007Z'
|
||||
fill='#101330'
|
||||
></path>
|
||||
<path
|
||||
d='M60.0318 9.50205V9.40415C60.7498 9.0452 61.4678 8.4252 61.4678 7.20155C61.4678 5.43945 60.0153 4.37891 58.0088 4.37891C55.9528 4.37891 54.4843 5.5047 54.4843 7.23415C54.4843 8.4089 55.1698 9.0452 55.9203 9.40415V9.50205C55.0883 9.79575 54.0928 10.6768 54.0928 12.1452C54.0928 13.9237 55.5613 15.1637 57.9923 15.1637C60.4233 15.1637 61.8428 13.9237 61.8428 12.1452C61.8428 10.6768 60.8638 9.81205 60.0318 9.50205ZM57.9923 5.87995C58.8083 5.87995 59.4118 6.40205 59.4118 7.2831C59.4118 8.16415 58.7918 8.6863 57.9923 8.6863C57.1928 8.6863 56.5238 8.16415 56.5238 7.2831C56.5238 6.38575 57.1603 5.87995 57.9923 5.87995ZM57.9923 13.5974C57.0458 13.5974 56.2793 12.9937 56.2793 11.9658C56.2793 11.0358 56.9153 10.3342 57.9758 10.3342C59.0203 10.3342 59.6568 11.0195 59.6568 11.9984C59.6568 12.9937 58.9223 13.5974 57.9923 13.5974Z'
|
||||
fill='#101330'
|
||||
></path>
|
||||
<path
|
||||
d='M63.9639 15.0007H66.0524V10.5791C66.0524 9.12704 66.9334 8.49074 67.9289 8.49074C68.9079 8.49074 69.6744 9.14334 69.6744 10.4812V15.0007H71.7629V10.057C71.7629 7.91969 70.5229 6.67969 68.5814 6.67969C67.3579 6.67969 66.6724 7.16914 66.1829 7.80544H66.0524L65.8729 6.84284H63.9639V15.0007Z'
|
||||
fill='#101330'
|
||||
></path>
|
||||
</svg>
|
||||
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
500
packages/cli/templates/form-trigger.handlebars
Normal file
500
packages/cli/templates/form-trigger.handlebars
Normal file
|
@ -0,0 +1,500 @@
|
|||
<html lang='en'>
|
||||
<head>
|
||||
<meta charset='UTF-8' />
|
||||
<meta name='viewport' content='width=device-width, initial-scale=1.0' />
|
||||
<link rel='icon' type='image/png' href='https://n8n.io/favicon.ico' />
|
||||
<link
|
||||
href='http://fonts.googleapis.com/css?family=Open+Sans'
|
||||
rel='stylesheet'
|
||||
type='text/css'
|
||||
/>
|
||||
<title>{{formTitle}}</title>
|
||||
<style>
|
||||
*,
|
||||
::after,
|
||||
::before {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family:
|
||||
Open Sans,
|
||||
sans-serif;
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: start;
|
||||
background-color: #fbfcfe;
|
||||
}
|
||||
|
||||
.container {
|
||||
margin: auto;
|
||||
text-align: center;
|
||||
padding-top: 24px;
|
||||
width: 448px;
|
||||
}
|
||||
|
||||
.test-notice {
|
||||
padding: 12px 24px;
|
||||
color: #e6a23d;
|
||||
background-color: #fefaf6;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #f6dcb7;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 16px;
|
||||
text-align: left;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 24px;
|
||||
background-color: white;
|
||||
border: 1px solid #dbdfe7;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0px 4px 16px 0px #634dff0f;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.n8n-link a {
|
||||
color: #7e8186;
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.n8n-link svg {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.form-header h1 {
|
||||
color: #525356;
|
||||
font-size: 20px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.form-header p {
|
||||
padding-top: 8px;
|
||||
color: #7e8186;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.inputs-wrapper {
|
||||
padding-top: 24px;
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
|
||||
form label {
|
||||
display: block;
|
||||
text-align: left;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #555555;
|
||||
padding-bottom: 6px;
|
||||
}
|
||||
|
||||
form .form-input {
|
||||
border: 1px solid #dbdfe7;
|
||||
border-radius: 6px;
|
||||
width: 100%;
|
||||
font-size: 14px;
|
||||
color: #909399;
|
||||
font-weight: 400;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
form input:focus {
|
||||
outline: none;
|
||||
border-color: rgb(90, 76, 194);
|
||||
}
|
||||
|
||||
.select-input {
|
||||
border: 1px solid #dbdfe7;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.select-input:focus-within {
|
||||
border: 1px solid rgb(90, 76, 194);
|
||||
}
|
||||
|
||||
form select {
|
||||
outline: transparent;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
width: 100%;
|
||||
font-size: 14px;
|
||||
color: #909399;
|
||||
font-weight: 400;
|
||||
background-color: white;
|
||||
padding: 12px;
|
||||
border-right: 12px solid transparent;
|
||||
}
|
||||
|
||||
input[type='date'] {
|
||||
font-family:
|
||||
Open Sans,
|
||||
sans-serif;
|
||||
}
|
||||
|
||||
#submit-btn {
|
||||
width: 100%;
|
||||
height: 48px;
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
border: 0;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
font-family:
|
||||
Open Sans,
|
||||
sans-serif;
|
||||
background-color: #ff6d5a;
|
||||
color: #ffffff;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#submit-btn span {
|
||||
padding-right: 6px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
#submit-btn span svg {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
fill: #ffffff;
|
||||
animation: lds-ring 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
|
||||
}
|
||||
|
||||
@keyframes lds-ring {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
#submit-btn:hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.error-hidden {
|
||||
display: block;
|
||||
position: relative;
|
||||
color: #ea1f30;
|
||||
text-align: left;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
visibility: hidden;
|
||||
padding-top: 6px;
|
||||
padding-bottom: 9px;
|
||||
}
|
||||
|
||||
.error-show {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
/* multiselect ----------------------------------- */
|
||||
.multiselect {
|
||||
padding-left: 6px;
|
||||
padding-right: 6px;
|
||||
}
|
||||
|
||||
.multiselect-option {
|
||||
padding-top: 6px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.multiselect-option label {
|
||||
padding-left: 12px;
|
||||
color: #7e8186;
|
||||
font-weight: 400;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.multiselect-checkbox {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
input[type='checkbox'] {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.form-required {
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class='container'>
|
||||
<section>
|
||||
{{#if testRun}}
|
||||
<div class='test-notice'>
|
||||
<p>This is test version of your form. Use it only for testing your Form Trigger.</p>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if validForm}}
|
||||
<form class='card' action='#' method='POST' name='n8n-form' id='n8n-form' novalidate>
|
||||
<div class='form-header'>
|
||||
<h1>{{formTitle}}</h1>
|
||||
<p>{{formDescription}} </p>
|
||||
</div>
|
||||
|
||||
<div class='inputs-wrapper'>
|
||||
{{#each formFields}}
|
||||
{{#if isMultiSelect}}
|
||||
<div>
|
||||
<label class='form-label'>{{label}}</label>
|
||||
<div class='multiselect {{inputRequired}}' id='{{id}}'>
|
||||
{{#each multiSelectOptions}}
|
||||
<div class='multiselect-option'>
|
||||
<input type='checkbox' class='multiselect-checkbox' id='{{id}}' />
|
||||
<label for='{{id}}'>{{label}}</label>
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
<p class='{{errorId}} error-hidden'>
|
||||
This field is required
|
||||
</p>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if isSelect}}
|
||||
<div class='form-group'>
|
||||
<label class='form-label' for='{{id}}'>{{label}}</label>
|
||||
<div class='select-input'>
|
||||
<select id='{{id}}' name='{{id}}' class='{{inputRequired}}'>
|
||||
<option value='' disabled selected>Select an option ...</option>
|
||||
{{#each selectOptions}}
|
||||
<option value='{{this}}'>{{this}}</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
</div>
|
||||
<p class='{{errorId}} error-hidden'>
|
||||
This field is required
|
||||
</p>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if isInput}}
|
||||
<div class='form-group'>
|
||||
<label class='form-label' for='{{id}}'>{{label}}</label>
|
||||
<input
|
||||
class='form-input {{inputRequired}}'
|
||||
type='{{type}}'
|
||||
id='{{id}}'
|
||||
name='{{id}}'
|
||||
/>
|
||||
<p class='{{errorId}} error-hidden'>
|
||||
This field is required
|
||||
</p>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
</div>
|
||||
|
||||
<button id='submit-btn' type='submit'>
|
||||
<span><svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
height='18px'
|
||||
viewBox='0 0 512 512'
|
||||
><!--! Font Awesome Free 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->
|
||||
<path
|
||||
d='M304 48a48 48 0 1 0 -96 0 48 48 0 1 0 96 0zm0 416a48 48 0 1 0 -96 0 48 48 0 1 0 96 0zM48 304a48 48 0 1 0 0-96 48 48 0 1 0 0 96zm464-48a48 48 0 1 0 -96 0 48 48 0 1 0 96 0zM142.9 437A48 48 0 1 0 75 369.1 48 48 0 1 0 142.9 437zm0-294.2A48 48 0 1 0 75 75a48 48 0 1 0 67.9 67.9zM369.1 437A48 48 0 1 0 437 369.1 48 48 0 1 0 369.1 437z'
|
||||
/>
|
||||
</svg></span>
|
||||
Submit form
|
||||
</button>
|
||||
</form>
|
||||
{{else}}
|
||||
<div class='card'>
|
||||
<div class='form-header'>
|
||||
{{#if testRun}}
|
||||
<h1>Please add at least one field to your form</h1>
|
||||
{{else}}
|
||||
<h1>Problem loading form</h1>
|
||||
<p>
|
||||
This usually occurs if the n8n workflow serving this form is deactivated or no
|
||||
longer exist
|
||||
</p>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<div class='card' id='submitted-form' style='display: none;'>
|
||||
<div class='form-header'>
|
||||
<h1 id='submitted-header'>Form Submited</h1>
|
||||
{{#if formSubmittedText}}
|
||||
<p id='submitted-content'>
|
||||
{{formSubmittedText}}
|
||||
</p>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class='n8n-link'>
|
||||
<a href={{n8nWebsiteLink}} target='_blank'>
|
||||
Form automated with
|
||||
<svg
|
||||
width='73'
|
||||
height='20'
|
||||
viewBox='0 0 73 20'
|
||||
fill='none'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
>
|
||||
<path
|
||||
fill-rule='evenodd'
|
||||
clip-rule='evenodd'
|
||||
d='M40.2373 4C40.2373 6.20915 38.4464 8 36.2373 8C34.3735 8 32.8074 6.72525 32.3633 5H26.7787C25.801 5 24.9666 5.70685 24.8059 6.6712L24.6415 7.6576C24.4854 8.59415 24.0116 9.40925 23.3417 10C24.0116 10.5907 24.4854 11.4058 24.6415 12.3424L24.8059 13.3288C24.9666 14.2931 25.801 15 26.7787 15H28.3633C28.8074 13.2747 30.3735 12 32.2373 12C34.4464 12 36.2373 13.7908 36.2373 16C36.2373 18.2092 34.4464 20 32.2373 20C30.3735 20 28.8074 18.7253 28.3633 17H26.7787C24.8233 17 23.1546 15.5864 22.8331 13.6576L22.6687 12.6712C22.508 11.7069 21.6736 11 20.6959 11H19.0645C18.5652 12.64 17.0406 13.8334 15.2373 13.8334C13.434 13.8334 11.9094 12.64 11.4101 11H9.06449C8.56519 12.64 7.04059 13.8334 5.2373 13.8334C3.02817 13.8334 1.2373 12.0424 1.2373 9.83335C1.2373 7.6242 3.02817 5.83335 5.2373 5.83335C7.16069 5.83335 8.76699 7.19085 9.15039 9H11.3242C11.7076 7.19085 13.3139 5.83335 15.2373 5.83335C17.1607 5.83335 18.767 7.19085 19.1504 9H20.6959C21.6736 9 22.508 8.29315 22.6687 7.3288L22.8331 6.3424C23.1546 4.41365 24.8233 3 26.7787 3H32.3633C32.8074 1.27478 34.3735 0 36.2373 0C38.4464 0 40.2373 1.79086 40.2373 4ZM38.2373 4C38.2373 5.10455 37.3419 6 36.2373 6C35.1327 6 34.2373 5.10455 34.2373 4C34.2373 2.89543 35.1327 2 36.2373 2C37.3419 2 38.2373 2.89543 38.2373 4ZM5.2373 11.8334C6.34189 11.8334 7.23729 10.9379 7.23729 9.83335C7.23729 8.72875 6.34189 7.83335 5.2373 7.83335C4.13273 7.83335 3.2373 8.72875 3.2373 9.83335C3.2373 10.9379 4.13273 11.8334 5.2373 11.8334ZM15.2373 11.8334C16.3419 11.8334 17.2373 10.9379 17.2373 9.83335C17.2373 8.72875 16.3419 7.83335 15.2373 7.83335C14.1327 7.83335 13.2373 8.72875 13.2373 9.83335C13.2373 10.9379 14.1327 11.8334 15.2373 11.8334ZM32.2373 18C33.3419 18 34.2373 17.1045 34.2373 16C34.2373 14.8954 33.3419 14 32.2373 14C31.1327 14 30.2373 14.8954 30.2373 16C30.2373 17.1045 31.1327 18 32.2373 18Z'
|
||||
fill='#EA4B71'
|
||||
/>
|
||||
<path
|
||||
d='M44.2393 15.0007H46.3277V10.5791C46.3277 9.12704 47.2088 8.49074 48.204 8.49074C49.183 8.49074 49.9498 9.14334 49.9498 10.4812V15.0007H52.038V10.057C52.038 7.91969 50.798 6.67969 48.8567 6.67969C47.633 6.67969 46.9477 7.16914 46.4582 7.80544H46.3277L46.1482 6.84284H44.2393V15.0007Z'
|
||||
fill='#101330'
|
||||
/>
|
||||
<path
|
||||
d='M60.0318 9.50205V9.40415C60.7498 9.0452 61.4678 8.4252 61.4678 7.20155C61.4678 5.43945 60.0153 4.37891 58.0088 4.37891C55.9528 4.37891 54.4843 5.5047 54.4843 7.23415C54.4843 8.4089 55.1698 9.0452 55.9203 9.40415V9.50205C55.0883 9.79575 54.0928 10.6768 54.0928 12.1452C54.0928 13.9237 55.5613 15.1637 57.9923 15.1637C60.4233 15.1637 61.8428 13.9237 61.8428 12.1452C61.8428 10.6768 60.8638 9.81205 60.0318 9.50205ZM57.9923 5.87995C58.8083 5.87995 59.4118 6.40205 59.4118 7.2831C59.4118 8.16415 58.7918 8.6863 57.9923 8.6863C57.1928 8.6863 56.5238 8.16415 56.5238 7.2831C56.5238 6.38575 57.1603 5.87995 57.9923 5.87995ZM57.9923 13.5974C57.0458 13.5974 56.2793 12.9937 56.2793 11.9658C56.2793 11.0358 56.9153 10.3342 57.9758 10.3342C59.0203 10.3342 59.6568 11.0195 59.6568 11.9984C59.6568 12.9937 58.9223 13.5974 57.9923 13.5974Z'
|
||||
fill='#101330'
|
||||
/>
|
||||
<path
|
||||
d='M63.9639 15.0007H66.0524V10.5791C66.0524 9.12704 66.9334 8.49074 67.9289 8.49074C68.9079 8.49074 69.6744 9.14334 69.6744 10.4812V15.0007H71.7629V10.057C71.7629 7.91969 70.5229 6.67969 68.5814 6.67969C67.3579 6.67969 66.6724 7.16914 66.1829 7.80544H66.0524L65.8729 6.84284H63.9639V15.0007Z'
|
||||
fill='#101330'
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<script>
|
||||
function validateInput(input, errorElement) {
|
||||
if (input.type === 'number' && input.value !== '') {
|
||||
const value = input.value.trim();
|
||||
|
||||
if (value === '' || isNaN(value)) {
|
||||
errorElement.textContent = 'Enter only numbers in this field';
|
||||
errorElement.classList.add('error-show');
|
||||
return false;
|
||||
} else {
|
||||
errorElement.classList.remove('error-show');
|
||||
return true;
|
||||
}
|
||||
} else if (input.value === '') {
|
||||
errorElement.classList.add('error-show');
|
||||
return false;
|
||||
} else {
|
||||
errorElement.classList.remove('error-show');
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
function getSelectedValues(input) {
|
||||
const selectedValues = [];
|
||||
const checkboxes = input.querySelectorAll('.multiselect-checkbox');
|
||||
|
||||
checkboxes.forEach((checkbox, index) => {
|
||||
if (checkbox.checked) {
|
||||
const label = input.querySelectorAll('label')[index];
|
||||
selectedValues.push(label.textContent.trim());
|
||||
}
|
||||
});
|
||||
|
||||
return selectedValues;
|
||||
}
|
||||
|
||||
function validateMultiselect(input, errorElement) {
|
||||
const selectedValues = getSelectedValues(input);
|
||||
|
||||
if (!selectedValues.length) {
|
||||
errorElement.classList.add('error-show');
|
||||
return false;
|
||||
} else {
|
||||
errorElement.classList.remove('error-show');
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
const form = document.querySelector('#n8n-form');
|
||||
|
||||
const requiredInputs = document.querySelectorAll('.form-required');
|
||||
|
||||
requiredInputs.forEach((input) => {
|
||||
const errorSelector = `.error-${input.id}`;
|
||||
const error = document.querySelector(errorSelector);
|
||||
|
||||
if (input.classList.contains('multiselect')) {
|
||||
input.addEventListener('click', () => {
|
||||
validateMultiselect(input, error);
|
||||
});
|
||||
} else {
|
||||
input.addEventListener('blur', () => {
|
||||
validateInput(input, error);
|
||||
});
|
||||
input.addEventListener('input', () => {
|
||||
error.classList.remove('error-show');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
form.addEventListener('submit', (e) => {
|
||||
const valid = [];
|
||||
e.preventDefault();
|
||||
|
||||
requiredInputs.forEach((input) => {
|
||||
const errorSelector = `.error-${input.id}`;
|
||||
const error = document.querySelector(errorSelector);
|
||||
|
||||
if (input.classList.contains('multiselect')) {
|
||||
valid.push(validateMultiselect(input, error));
|
||||
} else {
|
||||
valid.push(validateInput(input, error));
|
||||
}
|
||||
});
|
||||
|
||||
if (valid.every((v) => v)) {
|
||||
var formData = new FormData(form);
|
||||
|
||||
document.querySelectorAll('.multiselect').forEach((multiselect) => {
|
||||
const selectedValues = getSelectedValues(multiselect);
|
||||
formData.append(multiselect.id, JSON.stringify(selectedValues));
|
||||
});
|
||||
|
||||
document.querySelector('#submit-btn').disabled = true;
|
||||
document.querySelector('#submit-btn').style.cursor = 'not-allowed';
|
||||
document.querySelector('#submit-btn span').style.display = 'inline-block';
|
||||
fetch('#', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
})
|
||||
.then(async function (response) {
|
||||
const data = await response.json();
|
||||
data.status = response.status;
|
||||
return data;
|
||||
})
|
||||
.then(function (data) {
|
||||
if (data.status === 200) {
|
||||
form.style.display = 'none';
|
||||
document.querySelector('#submitted-form').style.display = 'block';
|
||||
} else {
|
||||
form.style.display = 'none';
|
||||
document.querySelector('#submitted-form').style.display = 'block';
|
||||
document.querySelector('#submitted-header').textContent = 'Problem submitting response';
|
||||
document.querySelector('#submitted-content').textContent =
|
||||
'An error occurred in the workflow handling this form';
|
||||
}
|
||||
})
|
||||
.catch(function (error) {
|
||||
console.error('Error:', error);
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
40
packages/editor-ui/public/static/form-grey.svg
Normal file
40
packages/editor-ui/public/static/form-grey.svg
Normal file
|
@ -0,0 +1,40 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="50"
|
||||
height="50"
|
||||
viewBox="0 0 50 50"
|
||||
fill="none"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
sodipodi:docname="form-trigger-icon.svg"
|
||||
inkscape:version="1.3 (0e150ed6c4, 2023-07-21)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs1" />
|
||||
<sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="16.44"
|
||||
inkscape:cx="25"
|
||||
inkscape:cy="25"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1023"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="29"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg1" />
|
||||
<path
|
||||
d="M47.2666 22.5544C47.2646 22.8648 47.1413 23.162 46.9229 23.3826L40.1183 30.1792C39.9013 30.4029 39.6017 30.5272 39.2901 30.523C38.9789 30.5254 38.6801 30.4012 38.462 30.1792C38.2444 29.9585 38.1225 29.661 38.1225 29.3511C38.1225 29.0412 38.2444 28.7437 38.462 28.523L44.4387 22.5542L43.1262 21.2417L41.7434 22.6558L28.595 35.7808L23.9779 36.5621L24.7592 31.945L25.0951 31.609L27.6576 34.1637C27.8725 34.3906 28.1734 34.5154 28.4857 34.5075C28.7974 34.5125 29.0974 34.3882 29.3139 34.1637C29.5315 33.943 29.6534 33.6455 29.6534 33.3356C29.6534 33.0257 29.5315 32.7282 29.3139 32.5075L26.7514 29.945L35.0014 21.6716L39.2905 17.3825L43.0484 21.0934L44.6655 19.4762L46.9234 21.734C47.1413 21.9517 47.2648 22.2464 47.2671 22.5544L47.2666 22.5544ZM29.9854 37.7028L33.0166 34.6715V42.9682C33.0166 43.3826 32.8521 43.78 32.5589 44.073C32.266 44.3662 31.8685 44.5307 31.4541 44.5307H4.29785C3.88347 44.5307 3.48602 44.3661 3.19302 44.073C2.89989 43.7801 2.73535 43.3826 2.73535 42.9682V14.8432C2.73486 14.4382 2.89177 14.0489 3.17285 13.7573L9.47744 7.22602V12.5698H6.25085C5.83223 12.5698 5.44535 12.7931 5.23606 13.1557C5.02659 13.5183 5.02659 13.9649 5.23606 14.3276C5.44537 14.6902 5.83227 14.9135 6.25085 14.9135H10.6804C10.9913 14.9135 11.2893 14.7899 11.5091 14.5702C11.7288 14.3505 11.8523 14.0525 11.8523 13.7416V5.46826H31.454C31.8684 5.46826 32.2658 5.63281 32.5588 5.92593C32.852 6.21889 33.0165 6.61635 33.0165 7.03076V20.312L22.8602 30.4683C22.691 30.6424 22.5769 30.8625 22.5321 31.1011L21.4305 37.7103C21.3705 38.0843 21.4924 38.4643 21.7587 38.7337C21.9782 38.9536 22.2761 39.0771 22.5868 39.0774H22.7821L29.3838 37.9758C29.6064 37.9448 29.8154 37.8497 29.9853 37.7024L29.9854 37.7028ZM8.98535 21.7028C8.98747 22.013 9.11149 22.3099 9.33089 22.5291C9.55013 22.7485 9.84702 22.8725 10.1572 22.8746H25.4231C25.8417 22.8746 26.2286 22.6513 26.4379 22.2887C26.6473 21.9261 26.6473 21.4795 26.4379 21.1169C26.2285 20.7542 25.8416 20.5309 25.4231 20.5309H10.1572C9.84636 20.5309 9.54835 20.6545 9.3286 20.8742C9.10888 21.0939 8.98534 21.392 8.98534 21.7028L8.98535 21.7028ZM19.6962 36.4686C19.6962 36.1577 19.5727 35.8597 19.3529 35.64C19.1332 35.4203 18.8352 35.2967 18.5243 35.2967H10.1572C9.73859 35.2967 9.35171 35.52 9.14242 35.8826C8.93295 36.2453 8.93295 36.6919 9.14242 37.0545C9.35173 37.4171 9.73863 37.6404 10.1572 37.6404H18.5243C18.8352 37.6404 19.1332 37.5169 19.3529 37.2972C19.5726 37.0774 19.6962 36.7794 19.6962 36.4685V36.4686ZM20.5556 29.0778C20.5556 28.7669 20.432 28.4689 20.2123 28.2491C19.9926 28.0294 19.6945 27.9059 19.3837 27.9059H10.157C9.73838 27.9059 9.35151 28.1292 9.14222 28.4918C8.93274 28.8544 8.93274 29.3011 9.14222 29.6637C9.35152 30.0263 9.73842 30.2496 10.157 30.2496H19.3837C19.695 30.2517 19.9943 30.1288 20.2145 29.9086C20.4348 29.6884 20.5576 29.3891 20.5555 29.0777L20.5556 29.0778ZM45.751 18.6169C46.042 18.3241 46.2054 17.9281 46.2054 17.5154C46.2054 17.1026 46.042 16.7066 45.751 16.4138L44.1104 14.7731C43.8176 14.4821 43.4216 14.3187 43.0088 14.3187C42.596 14.3187 42.2 14.4821 41.9072 14.7731L40.9775 15.7028L44.8291 19.5309L45.751 18.6169Z"
|
||||
fill="#00B7BC"
|
||||
id="path1"
|
||||
style="fill:#7d7d87;fill-opacity:1" />
|
||||
</svg>
|
After Width: | Height: | Size: 4.1 KiB |
|
@ -24,7 +24,9 @@
|
|||
:key="property.name + index"
|
||||
class="parameter-item"
|
||||
>
|
||||
<div class="parameter-item-wrapper">
|
||||
<div
|
||||
:class="index ? 'border-top-dashed parameter-item-wrapper ' : 'parameter-item-wrapper'"
|
||||
>
|
||||
<div class="delete-option" v-if="!isReadOnly">
|
||||
<font-awesome-icon
|
||||
icon="trash"
|
||||
|
@ -375,8 +377,6 @@ export default defineComponent({
|
|||
|
||||
+ .parameter-item {
|
||||
.parameter-item-wrapper {
|
||||
border-top: 1px dashed #999;
|
||||
|
||||
.delete-option {
|
||||
top: 14px;
|
||||
}
|
||||
|
@ -384,6 +384,10 @@ export default defineComponent({
|
|||
}
|
||||
}
|
||||
|
||||
.border-top-dashed {
|
||||
border-top: 1px dashed #999;
|
||||
}
|
||||
|
||||
.no-items-exist {
|
||||
margin: var(--spacing-xs) 0;
|
||||
}
|
||||
|
|
|
@ -175,6 +175,7 @@ import {
|
|||
LOCAL_STORAGE_PIN_DATA_DISCOVERY_CANVAS_FLAG,
|
||||
MANUAL_TRIGGER_NODE_TYPE,
|
||||
NODE_INSERT_SPACER_BETWEEN_INPUT_GROUPS,
|
||||
NOT_DUPLICATABE_NODE_TYPES,
|
||||
WAIT_TIME_UNLIMITED,
|
||||
} from '@/constants';
|
||||
import { externalHooks } from '@/mixins/externalHooks';
|
||||
|
@ -221,6 +222,7 @@ export default defineComponent({
|
|||
},
|
||||
isDuplicatable(): boolean {
|
||||
if (!this.nodeType) return true;
|
||||
if (NOT_DUPLICATABE_NODE_TYPES.includes(this.nodeType.name)) return false;
|
||||
return (
|
||||
this.nodeType.maxNodes === undefined || this.sameTypeNodes.length < this.nodeType.maxNodes
|
||||
);
|
||||
|
|
|
@ -76,7 +76,7 @@ describe('NodesListPanel', () => {
|
|||
await fireEvent.click(container.querySelector('.backButton')!);
|
||||
await nextTick();
|
||||
|
||||
expect(screen.queryAllByTestId('item-iterator-item')).toHaveLength(6);
|
||||
expect(screen.queryAllByTestId('item-iterator-item')).toHaveLength(7);
|
||||
});
|
||||
|
||||
it('should render regular nodes', async () => {
|
||||
|
|
|
@ -3,6 +3,7 @@ import {
|
|||
WEBHOOK_NODE_TYPE,
|
||||
OTHER_TRIGGER_NODES_SUBCATEGORY,
|
||||
EXECUTE_WORKFLOW_TRIGGER_NODE_TYPE,
|
||||
FORM_TRIGGER_NODE_TYPE,
|
||||
MANUAL_TRIGGER_NODE_TYPE,
|
||||
SCHEDULE_TRIGGER_NODE_TYPE,
|
||||
REGULAR_NODE_CREATOR_VIEW,
|
||||
|
@ -264,6 +265,22 @@ export function TriggerView(nodes: SimplifiedNodeType[]) {
|
|||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
key: FORM_TRIGGER_NODE_TYPE,
|
||||
type: 'node',
|
||||
category: [CORE_NODES_CATEGORY],
|
||||
properties: {
|
||||
group: [],
|
||||
name: FORM_TRIGGER_NODE_TYPE,
|
||||
displayName: i18n.baseText('nodeCreator.triggerHelperPanel.formTriggerDisplayName'),
|
||||
description: i18n.baseText('nodeCreator.triggerHelperPanel.formTriggerDescription'),
|
||||
iconData: {
|
||||
type: 'file',
|
||||
icon: 'form',
|
||||
fileBuffer: '/static/form-grey.svg',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
key: MANUAL_TRIGGER_NODE_TYPE,
|
||||
type: 'node',
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
:label="buttonLabel"
|
||||
:type="type"
|
||||
:size="size"
|
||||
:icon="isFormTriggerNode && 'flask'"
|
||||
:transparentBackground="transparent"
|
||||
@click="onClick"
|
||||
/>
|
||||
|
@ -23,7 +24,12 @@
|
|||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import { mapStores } from 'pinia';
|
||||
import { WEBHOOK_NODE_TYPE, MANUAL_TRIGGER_NODE_TYPE, MODAL_CONFIRM } from '@/constants';
|
||||
import {
|
||||
WEBHOOK_NODE_TYPE,
|
||||
MANUAL_TRIGGER_NODE_TYPE,
|
||||
MODAL_CONFIRM,
|
||||
FORM_TRIGGER_NODE_TYPE,
|
||||
} from '@/constants';
|
||||
import type { INodeUi } from '@/Interface';
|
||||
import type { INodeTypeDescription } from 'n8n-workflow';
|
||||
import { workflowRun } from '@/mixins/workflowRun';
|
||||
|
@ -97,6 +103,9 @@ export default defineComponent({
|
|||
isManualTriggerNode(): boolean {
|
||||
return Boolean(this.nodeType && this.nodeType.name === MANUAL_TRIGGER_NODE_TYPE);
|
||||
},
|
||||
isFormTriggerNode(): boolean {
|
||||
return Boolean(this.nodeType && this.nodeType.name === FORM_TRIGGER_NODE_TYPE);
|
||||
},
|
||||
isPollingTypeNode(): boolean {
|
||||
return !!this.nodeType?.polling;
|
||||
},
|
||||
|
@ -168,11 +177,20 @@ export default defineComponent({
|
|||
return this.$locale.baseText('ndv.execute.listenForTestEvent');
|
||||
}
|
||||
|
||||
if (this.isFormTriggerNode) {
|
||||
return this.$locale.baseText('ndv.execute.testStep');
|
||||
}
|
||||
|
||||
if (this.isPollingTypeNode || this.nodeType?.mockManualExecution) {
|
||||
return this.$locale.baseText('ndv.execute.fetchEvent');
|
||||
}
|
||||
|
||||
if (this.isTriggerNode && !this.isScheduleTrigger && !this.isManualTriggerNode) {
|
||||
if (
|
||||
this.isTriggerNode &&
|
||||
!this.isScheduleTrigger &&
|
||||
!this.isManualTriggerNode &&
|
||||
!this.isFormTriggerNode
|
||||
) {
|
||||
return this.$locale.baseText('ndv.execute.listenForEvent');
|
||||
}
|
||||
|
||||
|
|
|
@ -4,14 +4,10 @@
|
|||
class="clickable headline"
|
||||
:class="{ expanded: !isMinimized }"
|
||||
@click="isMinimized = !isMinimized"
|
||||
:title="
|
||||
isMinimized
|
||||
? $locale.baseText('nodeWebhooks.clickToDisplayWebhookUrls')
|
||||
: $locale.baseText('nodeWebhooks.clickToHideWebhookUrls')
|
||||
"
|
||||
:title="isMinimized ? baseText.clickToDisplay : baseText.clickToHide"
|
||||
>
|
||||
<font-awesome-icon icon="angle-down" class="minimize-button minimize-icon" />
|
||||
{{ $locale.baseText('nodeWebhooks.webhookUrls') }}
|
||||
{{ baseText.toggleTitle }}
|
||||
</div>
|
||||
<el-collapse-transition>
|
||||
<div class="node-webhooks" v-if="!isMinimized">
|
||||
|
@ -21,9 +17,9 @@
|
|||
<n8n-radio-buttons
|
||||
v-model="showUrlFor"
|
||||
:options="[
|
||||
{ label: this.$locale.baseText('nodeWebhooks.testUrl'), value: 'test' },
|
||||
{ label: baseText.testUrl, value: 'test' },
|
||||
{
|
||||
label: this.$locale.baseText('nodeWebhooks.productionUrl'),
|
||||
label: baseText.productionUrl,
|
||||
value: 'production',
|
||||
},
|
||||
]"
|
||||
|
@ -33,13 +29,13 @@
|
|||
</div>
|
||||
|
||||
<n8n-tooltip
|
||||
v-for="(webhook, index) in webhooksNode"
|
||||
v-for="(webhook, index) in webhooksNode.filter((webhook) => !webhook.ndvHideUrl)"
|
||||
:key="index"
|
||||
class="item"
|
||||
:content="$locale.baseText('nodeWebhooks.clickToCopyWebhookUrls')"
|
||||
:content="baseText.clickToCopy"
|
||||
placement="left"
|
||||
>
|
||||
<div class="webhook-wrapper">
|
||||
<div v-if="!webhook.ndvHideMethod" class="webhook-wrapper">
|
||||
<div class="http-field">
|
||||
<div class="http-method">
|
||||
{{ getWebhookExpressionValue(webhook, 'httpMethod') }}<br />
|
||||
|
@ -51,6 +47,13 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="webhook-wrapper">
|
||||
<div class="url-field-full-width">
|
||||
<div class="webhook-url left-ellipsis clickable" @click="copyWebhookUrl(webhook)">
|
||||
{{ getWebhookUrlDisplay(webhook) }}<br />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</n8n-tooltip>
|
||||
</div>
|
||||
</el-collapse-transition>
|
||||
|
@ -61,7 +64,7 @@
|
|||
import { defineComponent } from 'vue';
|
||||
import type { INodeTypeDescription, IWebhookDescription } from 'n8n-workflow';
|
||||
|
||||
import { WEBHOOK_NODE_TYPE } from '@/constants';
|
||||
import { FORM_TRIGGER_NODE_TYPE, OPEN_URL_PANEL_TRIGGER_NODE_TYPES } from '@/constants';
|
||||
import { copyPaste } from '@/mixins/copyPaste';
|
||||
import { useToast } from '@/composables';
|
||||
import { workflowHelpers } from '@/mixins/workflowHelpers';
|
||||
|
@ -80,7 +83,7 @@ export default defineComponent({
|
|||
},
|
||||
data() {
|
||||
return {
|
||||
isMinimized: this.nodeType && this.nodeType.name !== WEBHOOK_NODE_TYPE,
|
||||
isMinimized: this.nodeType && !OPEN_URL_PANEL_TRIGGER_NODE_TYPES.includes(this.nodeType.name),
|
||||
showUrlFor: 'test',
|
||||
};
|
||||
},
|
||||
|
@ -94,6 +97,36 @@ export default defineComponent({
|
|||
(webhookData) => webhookData.restartWebhook !== true,
|
||||
);
|
||||
},
|
||||
baseText() {
|
||||
const nodeType = this.nodeType.name;
|
||||
switch (nodeType) {
|
||||
case FORM_TRIGGER_NODE_TYPE:
|
||||
return {
|
||||
toggleTitle: this.$locale.baseText('nodeWebhooks.webhookUrls.formTrigger'),
|
||||
clickToDisplay: this.$locale.baseText(
|
||||
'nodeWebhooks.clickToDisplayWebhookUrls.formTrigger',
|
||||
),
|
||||
clickToHide: this.$locale.baseText('nodeWebhooks.clickToHideWebhookUrls.formTrigger'),
|
||||
clickToCopy: this.$locale.baseText('nodeWebhooks.clickToCopyWebhookUrls.formTrigger'),
|
||||
testUrl: this.$locale.baseText('nodeWebhooks.testUrl'),
|
||||
productionUrl: this.$locale.baseText('nodeWebhooks.productionUrl'),
|
||||
copyTitle: this.$locale.baseText('nodeWebhooks.showMessage.title.formTrigger'),
|
||||
copyMessage: this.$locale.baseText('nodeWebhooks.showMessage.message.formTrigger'),
|
||||
};
|
||||
|
||||
default:
|
||||
return {
|
||||
toggleTitle: this.$locale.baseText('nodeWebhooks.webhookUrls'),
|
||||
clickToDisplay: this.$locale.baseText('nodeWebhooks.clickToDisplayWebhookUrls'),
|
||||
clickToHide: this.$locale.baseText('nodeWebhooks.clickToHideWebhookUrls'),
|
||||
clickToCopy: this.$locale.baseText('nodeWebhooks.clickToCopyWebhookUrls'),
|
||||
testUrl: this.$locale.baseText('nodeWebhooks.testUrl'),
|
||||
productionUrl: this.$locale.baseText('nodeWebhooks.productionUrl'),
|
||||
copyTitle: this.$locale.baseText('nodeWebhooks.showMessage.title'),
|
||||
copyMessage: undefined,
|
||||
};
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
copyWebhookUrl(webhookData: IWebhookDescription): void {
|
||||
|
@ -101,7 +134,8 @@ export default defineComponent({
|
|||
this.copyToClipboard(webhookUrl);
|
||||
|
||||
this.showMessage({
|
||||
title: this.$locale.baseText('nodeWebhooks.showMessage.title'),
|
||||
title: this.baseText.copyTitle,
|
||||
message: this.baseText.copyMessage,
|
||||
type: 'success',
|
||||
});
|
||||
this.$telemetry.track('User copied webhook URL', {
|
||||
|
@ -118,7 +152,7 @@ export default defineComponent({
|
|||
},
|
||||
watch: {
|
||||
node() {
|
||||
this.isMinimized = this.nodeType.name !== WEBHOOK_NODE_TYPE;
|
||||
this.isMinimized = !OPEN_URL_PANEL_TRIGGER_NODE_TYPES.includes(this.nodeType.name);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -175,6 +209,10 @@ export default defineComponent({
|
|||
width: calc(100% - 60px);
|
||||
margin-left: 55px;
|
||||
}
|
||||
.url-field-full-width {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.url-selection {
|
||||
margin-top: var(--spacing-xs);
|
||||
|
|
|
@ -38,15 +38,11 @@
|
|||
</div>
|
||||
<div v-else>
|
||||
<n8n-text tag="div" size="large" color="text-dark" class="mb-2xs" bold>{{
|
||||
$locale.baseText('ndv.trigger.webhookBasedNode.listening')
|
||||
listeningTitle
|
||||
}}</n8n-text>
|
||||
<div :class="[$style.shake, 'mb-xs']">
|
||||
<n8n-text tag="div">
|
||||
{{
|
||||
$locale.baseText('ndv.trigger.webhookBasedNode.serviceHint', {
|
||||
interpolate: { service: serviceName },
|
||||
})
|
||||
}}
|
||||
{{ listeningHint }}
|
||||
</n8n-text>
|
||||
</div>
|
||||
<NodeExecuteButton
|
||||
|
@ -108,7 +104,12 @@
|
|||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import { mapStores } from 'pinia';
|
||||
import { VIEWS, WEBHOOK_NODE_TYPE, WORKFLOW_SETTINGS_MODAL_KEY } from '@/constants';
|
||||
import {
|
||||
VIEWS,
|
||||
WEBHOOK_NODE_TYPE,
|
||||
WORKFLOW_SETTINGS_MODAL_KEY,
|
||||
FORM_TRIGGER_NODE_TYPE,
|
||||
} from '@/constants';
|
||||
import type { INodeUi } from '@/Interface';
|
||||
import type { INodeTypeDescription } from 'n8n-workflow';
|
||||
import { getTriggerNodeServiceName } from '@/utils';
|
||||
|
@ -150,7 +151,7 @@ export default defineComponent({
|
|||
computed: {
|
||||
...mapStores(useNodeTypesStore, useNDVStore, useUIStore, useWorkflowsStore),
|
||||
node(): INodeUi | null {
|
||||
return this.workflowsStore.getNodeByName(this.nodeName);
|
||||
return this.workflowsStore.getNodeByName(this.nodeName as string);
|
||||
},
|
||||
nodeType(): INodeTypeDescription | null {
|
||||
if (this.node) {
|
||||
|
@ -216,6 +217,18 @@ export default defineComponent({
|
|||
isWorkflowActive(): boolean {
|
||||
return this.workflowsStore.isWorkflowActive;
|
||||
},
|
||||
listeningTitle(): string {
|
||||
return this.nodeType?.name === FORM_TRIGGER_NODE_TYPE
|
||||
? this.$locale.baseText('ndv.trigger.webhookNode.formTrigger.listening')
|
||||
: this.$locale.baseText('ndv.trigger.webhookNode.listening');
|
||||
},
|
||||
listeningHint(): string {
|
||||
return this.nodeType?.name === FORM_TRIGGER_NODE_TYPE
|
||||
? this.$locale.baseText('ndv.trigger.webhookBasedNode.formTrigger.serviceHint')
|
||||
: this.$locale.baseText('ndv.trigger.webhookBasedNode.serviceHint', {
|
||||
interpolate: { service: this.serviceName },
|
||||
});
|
||||
},
|
||||
header(): string {
|
||||
const serviceName = this.nodeType ? getTriggerNodeServiceName(this.nodeType) : '';
|
||||
|
||||
|
|
|
@ -111,6 +111,7 @@ export const ELASTIC_SECURITY_NODE_TYPE = 'n8n-nodes-base.elasticSecurity';
|
|||
export const EMAIL_SEND_NODE_TYPE = 'n8n-nodes-base.emailSend';
|
||||
export const EMAIL_IMAP_NODE_TYPE = 'n8n-nodes-base.emailReadImap';
|
||||
export const EXECUTE_COMMAND_NODE_TYPE = 'n8n-nodes-base.executeCommand';
|
||||
export const FORM_TRIGGER_NODE_TYPE = 'n8n-nodes-base.formTrigger';
|
||||
export const HTML_NODE_TYPE = 'n8n-nodes-base.html';
|
||||
export const HTTP_REQUEST_NODE_TYPE = 'n8n-nodes-base.httpRequest';
|
||||
export const HUBSPOT_TRIGGER_NODE_TYPE = 'n8n-nodes-base.hubspotTrigger';
|
||||
|
@ -171,6 +172,8 @@ export const NODES_USING_CODE_NODE_EDITOR = [CODE_NODE_TYPE];
|
|||
|
||||
export const PIN_DATA_NODE_TYPES_DENYLIST = [SPLIT_IN_BATCHES_NODE_TYPE];
|
||||
|
||||
export const OPEN_URL_PANEL_TRIGGER_NODE_TYPES = [WEBHOOK_NODE_TYPE, FORM_TRIGGER_NODE_TYPE];
|
||||
|
||||
// Node creator
|
||||
export const NODE_CREATOR_OPEN_SOURCES: Record<
|
||||
Uppercase<NodeCreatorOpenSource>,
|
||||
|
@ -631,3 +634,6 @@ export const ASK_AI_LOADING_DURATION_MS = 12000;
|
|||
export const APPEND_ATTRIBUTION_DEFAULT_PATH = 'parameters.options.appendAttribution';
|
||||
|
||||
export const DRAG_EVENT_DATA_KEY = 'nodesAndConnections';
|
||||
|
||||
export const NOT_DUPLICATABE_NODE_TYPES = [FORM_TRIGGER_NODE_TYPE];
|
||||
export const UPDATE_WEBHOOK_ID_NODE_TYPES = [FORM_TRIGGER_NODE_TYPE];
|
||||
|
|
|
@ -3,7 +3,12 @@ import { mapStores } from 'pinia';
|
|||
import type { IExecutionPushResponse, IExecutionResponse, IStartRunData } from '@/Interface';
|
||||
|
||||
import type { IRunData, IRunExecutionData, ITaskData, IWorkflowBase } from 'n8n-workflow';
|
||||
import { NodeHelpers, NodeConnectionType, TelemetryHelpers } from 'n8n-workflow';
|
||||
import {
|
||||
NodeHelpers,
|
||||
NodeConnectionType,
|
||||
TelemetryHelpers,
|
||||
FORM_TRIGGER_PATH_IDENTIFIER,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { externalHooks } from '@/mixins/externalHooks';
|
||||
import { workflowHelpers } from '@/mixins/workflowHelpers';
|
||||
|
@ -13,6 +18,8 @@ import { useTitleChange } from '@/composables/useTitleChange';
|
|||
import { useUIStore } from '@/stores/ui.store';
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import { useRootStore } from '@/stores/n8nRoot.store';
|
||||
import { FORM_TRIGGER_NODE_TYPE } from '../constants';
|
||||
import { openPopUpWindow } from '@/utils/executionUtils';
|
||||
|
||||
export const workflowRun = defineComponent({
|
||||
mixins: [externalHooks, workflowHelpers],
|
||||
|
@ -259,6 +266,27 @@ export const workflowRun = defineComponent({
|
|||
|
||||
const runWorkflowApiResponse = await this.runWorkflowApi(startRunData);
|
||||
|
||||
if (runWorkflowApiResponse.waitingForWebhook) {
|
||||
for (const node of workflowData.nodes) {
|
||||
if (node.type !== FORM_TRIGGER_NODE_TYPE) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
options.destinationNode &&
|
||||
options.destinationNode !== node.name &&
|
||||
!directParentNodes.includes(node.name)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (node.name === options.destinationNode || !node.disabled) {
|
||||
const testUrl = `${this.rootStore.getWebhookTestUrl}/${node.webhookId}/${FORM_TRIGGER_PATH_IDENTIFIER}`;
|
||||
openPopUpWindow(testUrl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await this.$externalHooks().run('workflowRun.runWorkflow', {
|
||||
nodeName: options.destinationNode,
|
||||
source: options.source,
|
||||
|
|
|
@ -728,6 +728,7 @@
|
|||
"ndv.execute.fixPrevious": "Fix previous node first",
|
||||
"ndv.execute.listenForEvent": "Listen For Event",
|
||||
"ndv.execute.listenForTestEvent": "Listen For Test Event",
|
||||
"ndv.execute.testStep": "Test Step",
|
||||
"ndv.execute.stopListening": "Stop Listening",
|
||||
"ndv.execute.nodeIsDisabled": "Enable node to execute",
|
||||
"ndv.execute.requiredFieldsMissing": "Complete required fields first",
|
||||
|
@ -883,6 +884,8 @@
|
|||
"nodeCreator.triggerHelperPanel.scheduleTriggerDescription": "Runs the flow every day, hour, or custom interval",
|
||||
"nodeCreator.triggerHelperPanel.webhookTriggerDisplayName": "On webhook call",
|
||||
"nodeCreator.triggerHelperPanel.webhookTriggerDescription": "Runs the flow when another app sends a webhook",
|
||||
"nodeCreator.triggerHelperPanel.formTriggerDisplayName": "On form submission",
|
||||
"nodeCreator.triggerHelperPanel.formTriggerDescription": "Runs the flow when an n8n generated webform is submitted",
|
||||
"nodeCreator.triggerHelperPanel.manualTriggerDisplayName": "Manually",
|
||||
"nodeCreator.triggerHelperPanel.manualTriggerDescription": "Runs the flow on clicking a button in n8n",
|
||||
"nodeCreator.triggerHelperPanel.whatHappensNext": "What happens next?",
|
||||
|
@ -1041,13 +1044,19 @@
|
|||
"nodeView.zoomToFit": "Zoom to Fit",
|
||||
"nodeView.replaceMe": "Replace Me",
|
||||
"nodeWebhooks.clickToCopyWebhookUrls": "Click to copy webhook URLs",
|
||||
"nodeWebhooks.clickToCopyWebhookUrls.formTrigger": "Click to copy Form URL",
|
||||
"nodeWebhooks.clickToDisplayWebhookUrls": "Click to display webhook URLs",
|
||||
"nodeWebhooks.clickToDisplayWebhookUrls.formTrigger": "Click to display Form URL",
|
||||
"nodeWebhooks.clickToHideWebhookUrls": "Click to hide webhook URLs",
|
||||
"nodeWebhooks.clickToHideWebhookUrls.formTrigger": "Click to hide Form URL",
|
||||
"nodeWebhooks.invalidExpression": "[INVALID EXPRESSION]",
|
||||
"nodeWebhooks.productionUrl": "Production URL",
|
||||
"nodeWebhooks.showMessage.title": "URL copied",
|
||||
"nodeWebhooks.showMessage.title.formTrigger": "Form URL copied",
|
||||
"nodeWebhooks.showMessage.message.formTrigger": "Form submissions made via this URL will trigger the workflow when it's activated",
|
||||
"nodeWebhooks.testUrl": "Test URL",
|
||||
"nodeWebhooks.webhookUrls": "Webhook URLs",
|
||||
"nodeWebhooks.webhookUrls.formTrigger": "Form URLs",
|
||||
"onboardingCallSignupModal.title": "Your onboarding session",
|
||||
"onboardingCallSignupModal.description": "Pop in your email and we'll send you some scheduling options",
|
||||
"onboardingCallSignupModal.emailInput.placeholder": "Your work email",
|
||||
|
@ -1706,9 +1715,11 @@
|
|||
"ndv.trigger.webhookBasedNode.executionsHelp.inactive": "<b>While building your workflow</b>, click the 'listen' button, then go to {service} and make an event happen. This will trigger an execution, which will show up in this editor.<br /> <br /> <b>Once you're happy with your workflow</b>, <a data-key=\"activate\">activate</a> it. Then every time there's a matching event in {service}, the workflow will execute. These executions will show up in the <a data-key=\"executions\">executions list</a>, but not in the editor.",
|
||||
"ndv.trigger.webhookBasedNode.executionsHelp.active": "<b>While building your workflow</b>, click the 'listen' button, then go to {service} and make an event happen. This will trigger an execution, which will show up in this editor.<br /> <br /> <b>Your workflow will also execute automatically</b>, since it's activated. Every time there’s a matching event in {service}, this node will trigger an execution. These executions will show up in the <a data-key=\"executions\">executions list</a>, but not in the editor. ",
|
||||
"ndv.trigger.webhookNode.listening": "Listening for test event",
|
||||
"ndv.trigger.webhookNode.formTrigger.listening": "Listening for a test form submission",
|
||||
"ndv.trigger.webhookBasedNode.listening": "Listening for your trigger event",
|
||||
"ndv.trigger.webhookNode.requestHint": "Make a {type} request to:",
|
||||
"ndv.trigger.webhookBasedNode.serviceHint": "Go to {service} and create an event",
|
||||
"ndv.trigger.webhookBasedNode.formTrigger.serviceHint": "Submit the test form that just opened in a new tab",
|
||||
"ndv.trigger.webhookBasedNode.activationHint.inactive": "Once you’ve finished building your workflow, <a data-key=\"activate\">activate it</a> to have it also listen continuously (you just won’t see those executions here).",
|
||||
"ndv.trigger.webhookBasedNode.activationHint.active": "This node will also trigger automatically on new {service} events (but those executions won’t show up here).",
|
||||
"ndv.trigger.pollingNode.activationHint.inactive": "Once you’ve finished building your workflow, <a data-key=\"activate\">activate it</a> to have it also check for events regularly (you just won’t see those executions here).",
|
||||
|
|
|
@ -45,3 +45,22 @@ export const executionFilterToQueryFilter = (
|
|||
}
|
||||
return queryFilter;
|
||||
};
|
||||
|
||||
export const openPopUpWindow = (
|
||||
url: string,
|
||||
options?: { width?: number; height?: number; alwaysInNewTab?: boolean },
|
||||
) => {
|
||||
const windowWidth = window.innerWidth;
|
||||
const smallScreen = windowWidth <= 800;
|
||||
if (options?.alwaysInNewTab || smallScreen) {
|
||||
window.open(url, '_blank');
|
||||
} else {
|
||||
const height = options?.width || 700;
|
||||
const width = options?.height || window.innerHeight - 50;
|
||||
const left = (window.innerWidth - height) / 2;
|
||||
const top = 50;
|
||||
const features = `width=${height},height=${width},left=${left},top=${top},resizable=yes,scrollbars=yes`;
|
||||
|
||||
window.open(url, '_blank', features);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -344,6 +344,7 @@ import { EVENT_ADD_INPUT_ENDPOINT_CLICK } from '@/plugins/jsplumb/N8nAddInputEnd
|
|||
import { sourceControlEventBus } from '@/event-bus/source-control';
|
||||
import { getConnectorPaintStyleData, OVERLAY_ENDPOINT_ARROW_ID } from '@/utils/nodeViewUtils';
|
||||
import { useViewStacks } from '@/components/Node/NodeCreator/composables/useViewStacks';
|
||||
import { UPDATE_WEBHOOK_ID_NODE_TYPES } from '@/constants';
|
||||
|
||||
interface AddNodeOptions {
|
||||
position?: XYPosition;
|
||||
|
@ -1650,8 +1651,18 @@ export default defineComponent({
|
|||
try {
|
||||
const nodeIdMap: { [prev: string]: string } = {};
|
||||
if (workflowData.nodes) {
|
||||
// set all new ids when pasting/importing workflows
|
||||
workflowData.nodes.forEach((node: INode) => {
|
||||
//generate new webhookId if workflow already contains a node with the same webhookId
|
||||
if (node.webhookId && UPDATE_WEBHOOK_ID_NODE_TYPES.includes(node.type)) {
|
||||
const isDuplicate = Object.values(this.getCurrentWorkflow().nodes).some(
|
||||
(n) => n.webhookId === node.webhookId,
|
||||
);
|
||||
if (isDuplicate) {
|
||||
node.webhookId = uuid();
|
||||
}
|
||||
}
|
||||
|
||||
// set all new ids when pasting/importing workflows
|
||||
if (node.id) {
|
||||
const newId = uuid();
|
||||
nodeIdMap[newId] = node.id;
|
||||
|
|
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;
|
||||
};
|
|
@ -490,6 +490,7 @@
|
|||
"dist/nodes/Filter/Filter.node.js",
|
||||
"dist/nodes/Flow/Flow.node.js",
|
||||
"dist/nodes/Flow/FlowTrigger.node.js",
|
||||
"dist/nodes/Form/FormTrigger.node.js",
|
||||
"dist/nodes/FormIo/FormIoTrigger.node.js",
|
||||
"dist/nodes/Formstack/FormstackTrigger.node.js",
|
||||
"dist/nodes/Freshdesk/Freshdesk.node.js",
|
||||
|
|
|
@ -17,3 +17,5 @@ export const NODES_WITH_RENAMABLE_CONTENT = new Set([
|
|||
// Arbitrary value to represent an empty credential value
|
||||
export const CREDENTIAL_EMPTY_VALUE =
|
||||
'__n8n_EMPTY_VALUE_7b1af746-3729-4c60-9b9b-e08eb29e58da' as const;
|
||||
|
||||
export const FORM_TRIGGER_PATH_IDENTIFIER = 'n8n-form';
|
||||
|
|
|
@ -1630,6 +1630,8 @@ export interface IWebhookDescription {
|
|||
responseMode?: WebhookResponseMode | string;
|
||||
responseData?: WebhookResponseData | string;
|
||||
restartWebhook?: boolean;
|
||||
ndvHideUrl?: boolean; // If true the webhook will not be displayed in the editor
|
||||
ndvHideMethod?: boolean; // If true the method will not be displayed in the editor
|
||||
}
|
||||
|
||||
export interface ProxyInput {
|
||||
|
|
Loading…
Reference in a new issue