diff --git a/.prettierignore b/.prettierignore index 4e33d31572..4ac8f0dafb 100644 --- a/.prettierignore +++ b/.prettierignore @@ -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 diff --git a/cypress/e2e/16-form-trigger-node.cy.ts b/cypress/e2e/16-form-trigger-node.cy.ts new file mode 100644 index 0000000000..27198001a8 --- /dev/null +++ b/cypress/e2e/16-form-trigger-node.cy.ts @@ -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'); + }); +}); diff --git a/packages/cli/src/WebhookHelpers.ts b/packages/cli/src/WebhookHelpers.ts index 2c8d75edaf..2a1347c19f 100644 --- a/packages/cli/src/WebhookHelpers.ts +++ b/packages/cli/src/WebhookHelpers.ts @@ -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 diff --git a/packages/cli/templates/form-trigger-404.handlebars b/packages/cli/templates/form-trigger-404.handlebars new file mode 100644 index 0000000000..e4118fd810 --- /dev/null +++ b/packages/cli/templates/form-trigger-404.handlebars @@ -0,0 +1,86 @@ + + + + + + + + {{#if isTestWebhook}} + Form Trigger isn't listening yet + {{else}} + Problem loading form + {{/if}} + + + + +
+
+
+ {{#if isTestWebhook}} +
+

Form Trigger isn't listening yet

+

Click the "Test Step" button in your form trigger

+
+ {{else}} +
+

Problem loading form

+

This usually occurs if the n8n workflow serving this form is deactivated or no + longer exist

+
+ {{/if}} +
+ +
+
+ + + \ No newline at end of file diff --git a/packages/cli/templates/form-trigger.handlebars b/packages/cli/templates/form-trigger.handlebars new file mode 100644 index 0000000000..88eaa7f0c1 --- /dev/null +++ b/packages/cli/templates/form-trigger.handlebars @@ -0,0 +1,500 @@ + + + + + + + {{formTitle}} + + + + +
+
+ {{#if testRun}} +
+

This is test version of your form. Use it only for testing your Form Trigger.

+
+ {{/if}} + + {{#if validForm}} +
+
+

{{formTitle}}

+

{{formDescription}}

+
+ +
+ {{#each formFields}} + {{#if isMultiSelect}} +
+ +
+ {{#each multiSelectOptions}} +
+ + +
+ {{/each}} +
+

+ This field is required +

+
+ {{/if}} + + {{#if isSelect}} +
+ +
+ +
+

+ This field is required +

+
+ {{/if}} + + {{#if isInput}} +
+ + +

+ This field is required +

+
+ {{/if}} + {{/each}} +
+ + +
+ {{else}} +
+
+ {{#if testRun}} +

Please add at least one field to your form

+ {{else}} +

Problem loading form

+

+ This usually occurs if the n8n workflow serving this form is deactivated or no + longer exist +

+ {{/if}} +
+
+ {{/if}} + + + + +
+
+ + + diff --git a/packages/editor-ui/public/static/form-grey.svg b/packages/editor-ui/public/static/form-grey.svg new file mode 100644 index 0000000000..20e6d2dc26 --- /dev/null +++ b/packages/editor-ui/public/static/form-grey.svg @@ -0,0 +1,40 @@ + + + + + + diff --git a/packages/editor-ui/src/components/FixedCollectionParameter.vue b/packages/editor-ui/src/components/FixedCollectionParameter.vue index b67baa1587..6c8fe3ae42 100644 --- a/packages/editor-ui/src/components/FixedCollectionParameter.vue +++ b/packages/editor-ui/src/components/FixedCollectionParameter.vue @@ -24,7 +24,9 @@ :key="property.name + index" class="parameter-item" > -
+
{ 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 () => { diff --git a/packages/editor-ui/src/components/Node/NodeCreator/viewsData.ts b/packages/editor-ui/src/components/Node/NodeCreator/viewsData.ts index b244634b42..e767ccdd48 100644 --- a/packages/editor-ui/src/components/Node/NodeCreator/viewsData.ts +++ b/packages/editor-ui/src/components/Node/NodeCreator/viewsData.ts @@ -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', diff --git a/packages/editor-ui/src/components/NodeExecuteButton.vue b/packages/editor-ui/src/components/NodeExecuteButton.vue index c79acb65e8..6b3060787c 100644 --- a/packages/editor-ui/src/components/NodeExecuteButton.vue +++ b/packages/editor-ui/src/components/NodeExecuteButton.vue @@ -12,6 +12,7 @@ :label="buttonLabel" :type="type" :size="size" + :icon="isFormTriggerNode && 'flask'" :transparentBackground="transparent" @click="onClick" /> @@ -23,7 +24,12 @@