From cd3f5b5b1f48e42cb6fa5ebcc15527c28502ceb9 Mon Sep 17 00:00:00 2001 From: Tomi Turtiainen <10324676+tomi@users.noreply.github.com> Date: Thu, 4 Jan 2024 10:21:36 +0200 Subject: [PATCH] fix: Fix template credential setup for nodes that dont have credentials (#8208) Fix template credential setup for templates whose workflow includes nodes that require credentials but the workflow definition does not have them defined. Like for example https://n8n.io/workflows/1344-save-email-attachments-to-nextcloud/ --- .../e2e/34-template-credentials-setup.cy.ts | 53 +- cypress/fixtures/Test_Template_2.json | 182 + cypress/pages/template-credential-setup.ts | 29 + .../src/utils/nodeTypes/nodeTypeTransforms.ts | 5 +- .../src/utils/nodes/nodeTransforms.ts | 33 + .../__tests__/templateTransforms.test.ts | 18 +- .../src/utils/templates/templateActions.ts | 17 +- .../src/utils/templates/templateTransforms.ts | 62 +- .../utils/testData/credentialTypeTestData.ts | 36 + .../src/utils/testData/nodeTypeTestData.ts | 4720 +++++++++++++++++ .../src/utils/testData/templateTestData.ts | 278 +- .../__tests__/setupTemplate.store.test.ts | 213 +- .../__tests__/setupTemplate.store.testData.ts | 222 +- .../setupTemplate.store.ts | 70 +- packages/workflow/src/NodeHelpers.ts | 6 +- 15 files changed, 5596 insertions(+), 348 deletions(-) create mode 100644 cypress/fixtures/Test_Template_2.json create mode 100644 packages/editor-ui/src/utils/nodes/nodeTransforms.ts create mode 100644 packages/editor-ui/src/utils/testData/credentialTypeTestData.ts create mode 100644 packages/editor-ui/src/utils/testData/nodeTypeTestData.ts diff --git a/cypress/e2e/34-template-credentials-setup.cy.ts b/cypress/e2e/34-template-credentials-setup.cy.ts index a50c66714e..a65e3a55b2 100644 --- a/cypress/e2e/34-template-credentials-setup.cy.ts +++ b/cypress/e2e/34-template-credentials-setup.cy.ts @@ -1,4 +1,3 @@ -import { CredentialsModal, MessageBox } from '../pages/modals'; import { clickUseWorkflowButtonByTitle, visitTemplateCollectionPage, @@ -9,8 +8,6 @@ import { TemplateWorkflowPage } from '../pages/template-workflow'; import { WorkflowPage } from '../pages/workflow'; const templateWorkflowPage = new TemplateWorkflowPage(); -const credentialsModal = new CredentialsModal(); -const messageBox = new MessageBox(); const workflowPage = new WorkflowPage(); const testTemplate = templateCredentialsSetupPage.testData.simpleTemplate; @@ -95,24 +92,48 @@ describe('Template credentials setup', () => { // Continue button should be disabled if no credentials are created templateCredentialsSetupPage.getters.continueButton().should('be.disabled'); - templateCredentialsSetupPage.getters.createAppCredentialsButton('Shopify').click(); - credentialsModal.getters.editCredentialModal().find('input:first()').type('test'); - credentialsModal.actions.save(false); - credentialsModal.actions.close(); + templateCredentialsSetupPage.fillInDummyCredentialsForApp('Shopify'); // Continue button should be enabled if at least one has been created templateCredentialsSetupPage.getters.continueButton().should('be.enabled'); - templateCredentialsSetupPage.getters.createAppCredentialsButton('X (Formerly Twitter)').click(); - credentialsModal.getters.editCredentialModal().find('input:first()').type('test'); - credentialsModal.actions.save(false); - credentialsModal.actions.close(); - messageBox.actions.cancel(); + templateCredentialsSetupPage.fillInDummyCredentialsForAppWithConfirm('X (Formerly Twitter)'); + templateCredentialsSetupPage.fillInDummyCredentialsForApp('Telegram'); - templateCredentialsSetupPage.getters.createAppCredentialsButton('Telegram').click(); - credentialsModal.getters.editCredentialModal().find('input:first()').type('test'); - credentialsModal.actions.save(false); - credentialsModal.actions.close(); + cy.intercept('POST', '/rest/workflows').as('createWorkflow'); + templateCredentialsSetupPage.getters.continueButton().should('be.enabled'); + templateCredentialsSetupPage.getters.continueButton().click(); + cy.wait('@createWorkflow'); + + workflowPage.getters.canvasNodes().should('have.length', 3); + }); + + it('should work with a template that has no credentials (ADO-1603)', () => { + const templateWithoutCreds = templateCredentialsSetupPage.testData.templateWithoutCredentials; + cy.intercept('GET', `https://api.n8n.io/api/templates/workflows/${templateWithoutCreds.id}`, { + fixture: templateWithoutCreds.fixture, + }); + templateCredentialsSetupPage.visitTemplateCredentialSetupPage(templateWithoutCreds.id); + + const expectedAppNames = ['1. Email (IMAP)', '2. Nextcloud']; + const expectedAppDescriptions = [ + 'The credential you select will be used in the IMAP Email node of the workflow template.', + 'The credential you select will be used in the Nextcloud node of the workflow template.', + ]; + + templateCredentialsSetupPage.getters.appCredentialSteps().each(($el, index) => { + templateCredentialsSetupPage.getters + .stepHeading($el) + .should('have.text', expectedAppNames[index]); + templateCredentialsSetupPage.getters + .stepDescription($el) + .should('have.text', expectedAppDescriptions[index]); + }); + + templateCredentialsSetupPage.getters.continueButton().should('be.disabled'); + + templateCredentialsSetupPage.fillInDummyCredentialsForApp('Email (IMAP)'); + templateCredentialsSetupPage.fillInDummyCredentialsForApp('Nextcloud'); cy.intercept('POST', '/rest/workflows').as('createWorkflow'); templateCredentialsSetupPage.getters.continueButton().should('be.enabled'); diff --git a/cypress/fixtures/Test_Template_2.json b/cypress/fixtures/Test_Template_2.json new file mode 100644 index 0000000000..29001a78e3 --- /dev/null +++ b/cypress/fixtures/Test_Template_2.json @@ -0,0 +1,182 @@ +{ + "workflow": { + "id": 1344, + "name": "Save email attachments to Nextcloud", + "views": 650, + "recentViews": 9887, + "totalViews": 650, + "createdAt": "2021-11-29T13:59:16.771Z", + "description": "This workflow will take all emails you put into a certain folder, upload any attachements to Nextcloud, and mark the emails as read (configurable).\n\nAttachements will be saved with automatically generated filenames:\n`2021-01-01_From-Sender-Name_Filename-of-attachement.pdf`\n\nInstructions:\n1. **Allow lodash to be used in n8n** (or rewrite the code...)\n `NODE_FUNCTION_ALLOW_EXTERNAL=lodash` (environment variable)\n2. Import workflow\n3. Set credentials for Email & Nextcloud nodes\n4. Configure to use correct folder / custom filters\n5. Activate\n\nCustom filter examples:\n- Only unread emails:\n `Custom Email Config` = `[\"UNSEEN\"]`\n- Filter emails by 'to' address:\n `Custom Email Config` = `[[\"TO\", \"example+invoices@posteo.de\"]]`", + "workflow": { + "nodes": [ + { + "name": "IMAP Email", + "type": "n8n-nodes-base.emailReadImap", + "position": [ + 240, + 420 + ], + "parameters": { + "format": "resolved", + "mailbox": "Invoices", + "options": { + "customEmailConfig": "[\"ALL\"]" + } + }, + "typeVersion": 1 + }, + { + "name": "Nextcloud", + "type": "n8n-nodes-base.nextCloud", + "position": [ + 940, + 420 + ], + "parameters": { + "path": "=Documents/Invoices/{{$json[\"date\"]}}_{{$json[\"from\"]}}_{{$binary.file.fileName}}", + "binaryDataUpload": true, + "binaryPropertyName": "file" + }, + "typeVersion": 1 + }, + { + "name": "Map each attachment", + "type": "n8n-nodes-base.function", + "position": [ + 620, + 420 + ], + "parameters": { + "functionCode": "const _ = require('lodash')\n\nconst sanitize = str => _.chain(str)\n .replace(/[^A-Za-z0-9&.-]/g, '-') // sanitise via whitelist of characters\n .replace(/-(?=-)/g, '') // remove repeated dashes - https://regexr.com/6ag8h\n .trim('-') // trim any leading/trailing dashes\n .truncate({\n length: 60,\n omission: '-' // when the string ends with '-', you'll know it was truncated\n })\n .value()\n\nconst result = _.flatMap(items.map(item => {\n //console.log({item})\n\n // Maps each attachment to a separate item\n return _.values(item.binary).map(file => {\n console.log(\"Saving attachement:\", file.fileName, 'from:', ...item.json.from.value)\n \n // sanitize filename but exclude extension\n const filename_parts = file.fileName.split('.')\n const ext = _.slice(filename_parts, filename_parts.length-1)\n const filename_main = _.join(_.dropRight(filename_parts), '.')\n file.fileName = sanitize(filename_main) + '.' + ext\n \n return {\n json: {\n from: sanitize(item.json.from.value[0].name),\n date: sanitize(new Date(item.json.date).toISOString().split(\"T\")[0]) // get date part \"2020-01-01\"\n }, \n binary: { file }\n }\n })\n}))\n\n//console.log(result)\nreturn result" + }, + "typeVersion": 1 + } + ], + "connections": { + "IMAP Email": { + "main": [ + [ + { + "node": "Map each attachment", + "type": "main", + "index": 0 + } + ] + ] + }, + "Map each attachment": { + "main": [ + [ + { + "node": "Nextcloud", + "type": "main", + "index": 0 + } + ] + ] + } + } + }, + "lastUpdatedBy": 11, + "workflowInfo": { + "nodeCount": 3, + "nodeTypes": { + "n8n-nodes-base.function": { + "count": 1 + }, + "n8n-nodes-base.nextCloud": { + "count": 1 + }, + "n8n-nodes-base.emailReadImap": { + "count": 1 + } + } + }, + "user": { + "username": "tennox" + }, + "nodes": [ + { + "id": 10, + "icon": "fa:inbox", + "name": "n8n-nodes-base.emailReadImap", + "defaults": { + "name": "Email Trigger (IMAP)", + "color": "#44AA22" + }, + "iconData": { + "icon": "inbox", + "type": "icon" + }, + "categories": [ + { + "id": 6, + "name": "Communication" + }, + { + "id": 9, + "name": "Core Nodes" + } + ], + "displayName": "Email Trigger (IMAP)", + "typeVersion": 2 + }, + { + "id": 14, + "icon": "fa:code", + "name": "n8n-nodes-base.function", + "defaults": { + "name": "Function", + "color": "#FF9922" + }, + "iconData": { + "icon": "code", + "type": "icon" + }, + "categories": [ + { + "id": 5, + "name": "Development" + }, + { + "id": 9, + "name": "Core Nodes" + } + ], + "displayName": "Function", + "typeVersion": 1 + }, + { + "id": 25, + "icon": "file:nextcloud.svg", + "name": "n8n-nodes-base.nextCloud", + "defaults": { + "name": "Nextcloud" + }, + "iconData": { + "type": "file", + "fileBuffer": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSIwIDAgNzYgNTEiIGZpbGw9IiNmZmYiIGZpbGwtcnVsZT0iZXZlbm9kZCIgc3Ryb2tlPSIjMDAwIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjx1c2UgeGxpbms6aHJlZj0iI0EiIHg9Ii41IiB5PSIuNSIvPjxzeW1ib2wgaWQ9IkEiIG92ZXJmbG93PSJ2aXNpYmxlIj48cGF0aCBkPSJNMzcuNTMzIDBjLTcuNzcgMC0xNC4zNTUgNS4yNjgtMTYuMzk2IDEyLjM3OS0xLjc3OC0zLjgxOS01LjU5Ny02LjQ1My0xMC4wNzUtNi40NTNDNS4wMDQgNS45MjYgMCAxMC45MzEgMCAxNy4wNTRhMTEuMTYgMTEuMTYgMCAwIDAgMTEuMTI4IDExLjEyOGM0LjQxMiAwIDguMjk3LTIuNjM0IDEwLjA3NS02LjQ1M2ExNi45OSAxNi45OSAwIDAgMCAxNi4zMyAxMi4zNzljNy43MDQgMCAxNC4yODktNS4yMDIgMTYuMzk2LTEyLjI0OCAxLjc3OCAzLjY4NyA1LjU5NyA2LjI1NiA5Ljk0MyA2LjI1NkExMS4xNiAxMS4xNiAwIDAgMCA3NSAxNi45ODljMC02LjEyNC01LjAwNC0xMS4wNjItMTEuMTI4LTExLjA2Mi00LjM0NiAwLTguMTY1IDIuNTY4LTkuOTQzIDYuMjU2QzUxLjgyMiA1LjIwMiA0NS4zMDMgMCAzNy41MzMgMHptMCA2LjUxOWExMC40OCAxMC40OCAwIDAgMSAxMC41MzUgMTAuNTM2QTEwLjQ4IDEwLjQ4IDAgMCAxIDM3LjUzMyAyNy41OWExMC40OCAxMC40OCAwIDAgMS0xMC41MzYtMTAuNTM1QTEwLjQ4IDEwLjQ4IDAgMCAxIDM3LjUzMyA2LjUxOXptLTI2LjQwNSA1LjkyNmE0LjU4IDQuNTggMCAwIDEgNC42MDkgNC42MDkgNC41OCA0LjU4IDAgMCAxLTQuNjA5IDQuNjA5IDQuNTggNC41OCAwIDAgMS00LjYwOS00LjYwOSA0LjU4IDQuNTggMCAwIDEgNC42MDktNC42MDl6bTUyLjc0NCAwYTQuNTggNC41OCAwIDAgMSA0LjYwOSA0LjYwOSA0LjYwOSA0LjYwOSAwIDEgMS05LjIxOCAwYy4wNjYtMi41NjggMi4wNDEtNC42MDkgNC42MDktNC42MDl6TTE5LjE3NiA0MS45NTdjMS44MjcgMCAyLjg1IDEuMzAxIDIuODUgMy4yNTIgMCAuMTg2LS4xNTUuMzQxLS4zNDEuMzQxSDE2Ljc2Yy4wMzEgMS43MzQgMS4yMzkgMi43MjYgMi42MzMgMi43MjZhMi44OSAyLjg5IDAgMCAwIDEuNzk2LS42MTljLjE4Ni0uMTI0LjM0MS0uMDkzLjQzNC4wOTNsLjA5My4xNTVjLjA5My4xNTUuMDYyLjMxLS4wOTMuNDM0YTMuODQgMy44NCAwIDAgMS0yLjI2MS43NDNjLTIuMDEzIDAtMy41NjItMS40NTYtMy41NjItMy41NjIuMDMxLTIuMjMgMS41MTgtMy41NjIgMy4zNzYtMy41NjJ6bTEuODg5IDIuOTExYy0uMDYyLTEuNDI1LS45MjktMi4xMzctMS45Mi0yLjEzNy0xLjE0NiAwLTIuMTM3Ljc0My0yLjM1NCAyLjEzN2g0LjI3NHptMTAuMjUzLTEuOTJ2LS43NzQtMS42MTFjMC0uMjE3LjEyNC0uMzQxLjM0MS0uMzQxaC4yNDhjLjIxNyAwIC4zMS4xMjQuMzEuMzQxdjEuNjExaDEuMzk0Yy4yMTcgMCAuMzQxLjEyNC4zNDEuMzQxdi4wOTNjMCAuMjE3LS4xMjQuMzEtLjM0MS4zMWgtMS4zOTR2My40MDdjMCAxLjU4Ljk2IDEuNzY2IDEuNDg3IDEuNzk2LjI3OS4wMzEuMzcyLjA5My4zNzIuMzQxdi4xODZjMCAuMjE3LS4wOTMuMzEtLjM3Mi4zMS0xLjQ4NyAwLTIuMzg1LS44OTgtMi4zODUtMi41MDl2LTMuNXptNy4wOTMtLjk5MWMxLjE3NyAwIDEuOTIuNDk2IDIuMjYxLjc3NC4xNTUuMTI0LjE4Ni4yNzkuMDMxLjQ2NWwtLjA5My4xNTVjLS4xMjQuMTg2LS4yNzkuMTg2LS40NjUuMDYyLS4zMS0uMjE3LS44OTgtLjYxOS0xLjcwMy0uNjE5LTEuNDg3IDAtMi42NjQgMS4xMTUtMi42NjQgMi43NTcgMCAxLjYxMSAxLjE3NyAyLjcyNiAyLjY2NCAyLjcyNi45NiAwIDEuNjExLS40MzQgMS45Mi0uNzEyLjE4Ni0uMTI0LjMxLS4wOTMuNDM0LjA5M2wuMDkzLjEyNGMuMDkzLjE4Ni4wNjIuMzEtLjA5My40NjVhMy44MSAzLjgxIDAgMCAxLTIuNDE2Ljg2N2MtMi4wMTMgMC0zLjU2Mi0xLjQ1Ni0zLjU2Mi0zLjU2Mi4wMzEtMi4xMDYgMS41OC0zLjU5MyAzLjU5My0zLjU5M3ptNC4xMTktMi4xOTljMC0uMjE3LS4xMjQtLjM0MS4wOTMtLjM0MWguMjQ4Yy4yMTcgMCAuNTU4LjEyNC41NTguMzQxdjcuNDAzYzAgLjg2Ny40MDMuOTYuNzEyLjk5MS4xNTUgMCAuMjc5LjA5My4yNzkuMzF2LjIxN2MwIC4yMTctLjA5My4zNDEtLjM0MS4zNDEtLjU1NyAwLTEuNTQ5LS4xODYtMS41NDktMS42NzN2LTcuNTg5em02LjM1IDIuMTk5YzEuOTgyIDAgMy41OTMgMS41MTggMy41OTMgMy41MzEgMCAyLjA0NC0xLjYxMSAzLjU5My0zLjU5MyAzLjU5M3MtMy41OTMtMS41NDktMy41OTMtMy41OTNjMC0yLjAxMyAxLjYxMS0zLjUzMSAzLjU5My0zLjUzMXptMCA2LjMxOWMxLjQ1NiAwIDIuNjMzLTEuMTc3IDIuNjMzLTIuNzg4IDAtMS41NDktMS4xNzctMi42OTUtMi42MzMtMi42OTVhMi42NyAyLjY3IDAgMCAwLTIuNjY0IDIuNjk1Yy4wMzEgMS41OCAxLjIwOCAyLjc4OCAyLjY2NCAyLjc4OHptMTUuNDU2LTYuMzE5YTIuNDUgMi40NSAwIDAgMSAyLjIzIDEuMzYzaC4wMzFzLS4wMzEtLjIxNy0uMDMxLS41MjZ2LTMuMDY2YzAtLjIxNy0uMDkzLS4zNDEuMTI0LS4zNDFoLjI0OGMuMjE3IDAgLjU1OC4xMjQuNTU4LjM0MXY4LjgyN2MwIC4yMTctLjA5My4zNDEtLjMxLjM0MWgtLjIxN2MtLjIxNyAwLS4zNDEtLjA5My0uMzQxLS4zMXYtLjUyN2MwLS4yNDguMDYyLS40MzQuMDYyLS40MzRoLS4wMzFzLS41ODkgMS40MjUtMi4zNTQgMS40MjVjLTEuODI3IDAtMi45NzMtMS40NTYtMi45NzMtMy41NjItLjA2Mi0yLjEwNiAxLjIwOC0zLjUzMSAzLjAwNC0zLjUzMWgwem0uMDMxIDYuMzE5YzEuMTQ2IDAgMi4xOTktLjgwNSAyLjE5OS0yLjc1NyAwLTEuMzk0LS43MTItMi43MjYtMi4xNjgtMi43MjYtMS4yMDggMC0yLjE5OS45OTEtMi4xOTkgMi43MjYuMDMxIDEuNjczLjg5OCAyLjc1NyAyLjE2OCAyLjc1N3ptLTU2LjU1OC42NWguMjQ4Yy4yMTcgMCAuMzQxLS4xMjQuMzQxLS4zNDF2LTYuNjI4YzAtMS4wNTMgMS4xNDYtMS43OTYgMi40NDctMS43OTZzMi40NDcuNzQzIDIuNDQ3IDEuNzk2djYuNjU5YzAgLjIxNy4xMjQuMzQxLjM0MS4zNDFoLjI0OGMuMjE3IDAgLjMxLS4xMjQuMzEtLjM0MXYtNi43MjFjMC0xLjc2Ni0xLjc2NS0yLjYzMy0zLjM3Ni0yLjYzM2gwIDAgMCAwYy0xLjU0OSAwLTMuMzE0Ljg2Ny0zLjMxNCAyLjYzM3Y2LjY5YzAgLjIxNy4wOTMuMzQxLjMxLjM0MXptNTEuNjk1LTYuODE0aC0uMjQ4Yy0uMjE3IDAtLjM0MS4xMjQtLjM0MS4zNDF2My43NDhjMCAxLjA1My0uNjgxIDIuMDEzLTIuMDEzIDIuMDEzLTEuMzAxIDAtMi4wMTMtLjk2LTIuMDEzLTIuMDEzdi0zLjc0OGMwLS4yMTctLjEyNC0uMzQxLS4zNDEtLjM0MUg1NC4zYy0uMjE3IDAtLjMxLjEyNC0uMzEuMzQxdjMuOTk2YzAgMS43NjUgMS4zMDEgMi42MzMgMi45MTIgMi42MzNoMCAwIDAgMGMxLjYxMSAwIDIuOTExLS44NjcgMi45MTEtMi42MzN2LTMuOTk2Yy4wMzEtLjIxNy0uMDkzLS4zNDEtLjMxLS4zNDFoMHptLTMwLjY2NC0uMDMxYy0uMDYyIDAtLjE1NS4wNjItLjIxNy4xNTVsLTEuMjM5IDEuNDg3LS45MjkgMS4xMTUtMS40MjUtMS43MDQtLjc3NC0uOTI5Yy0uMDYyLS4wOTMtLjE1NS0uMTI0LS4yMTctLjEyNHMtLjE1NS4wMzEtLjI0OC4wOTNsLS4xODYuMTU1Yy0uMTU1LjEyNC0uMTU1LjI3OS0uMDMxLjQ2NWwxLjIzOSAxLjQ4NyAxLjA1MyAxLjIzOS0xLjUxOCAxLjgyN2gwbC0uNzc0LjkyOWMtLjEyNC4xNTUtLjEyNC4zNDEuMDMxLjQ5NmwuMTg2LjE1NWMuMTU1LjEyNC4zMS4wOTMuNDY1LS4wNjJsMS4yMzktMS40ODcuOTI5LTEuMTE1IDEuNDI1IDEuNzA0aDBsLjc3NC45MjljLjEyNC4xNTUuMzEuMTg2LjQ2NS4wMzFsLjE4Ni0uMTU1Yy4xNTUtLjEyNC4xNTUtLjI3OS4wMzEtLjQ2NWwtMS4yMzktMS40ODctMS4wNTMtMS4yMzkgMS41MTgtMS44MjdoMGwuNzc0LS45MjljLjEyNC0uMTU1LjEyNC0uMzQxLS4wMzEtLjQ5NWwtLjE4Ni0uMTg2Yy0uMDkzLS4wNjItLjE1NS0uMDkzLS4yNDgtLjA2MmgweiIgZmlsbD0iIzAwODJjOSIgZmlsbC1ydWxlPSJub256ZXJvIiBzdHJva2U9Im5vbmUiLz48L3N5bWJvbD48L3N2Zz4=" + }, + "categories": [ + { + "id": 3, + "name": "Data & Storage" + } + ], + "displayName": "Nextcloud", + "typeVersion": 1 + } + ], + "categories": [ + { + "id": 2, + "name": "Sales" + }, + { + "id": 8, + "name": "Finance & Accounting" + } + ], + "image": [] + } +} diff --git a/cypress/pages/template-credential-setup.ts b/cypress/pages/template-credential-setup.ts index b015285da5..41fdcfc295 100644 --- a/cypress/pages/template-credential-setup.ts +++ b/cypress/pages/template-credential-setup.ts @@ -1,3 +1,5 @@ +import { CredentialsModal, MessageBox } from './modals'; + export type TemplateTestData = { id: number; fixture: string; @@ -8,8 +10,15 @@ export const testData = { id: 1205, fixture: 'Test_Template_1.json', }, + templateWithoutCredentials: { + id: 1344, + fixture: 'Test_Template_2.json', + }, }; +const credentialsModal = new CredentialsModal(); +const messageBox = new MessageBox(); + export const getters = { continueButton: () => cy.getByTestId('continue-button'), skipLink: () => cy.get('a:contains("Skip")'), @@ -33,3 +42,23 @@ export const enableTemplateCredentialSetupFeatureFlag = () => { win.featureFlags.override('016_template_credential_setup', true); }); }; + +/** + * Fills in dummy credentials for the given app name. + */ +export const fillInDummyCredentialsForApp = (appName: string) => { + getters.createAppCredentialsButton(appName).click(); + credentialsModal.getters.editCredentialModal().find('input:first()').type('test'); + credentialsModal.actions.save(false); + credentialsModal.actions.close(); +}; + +/** + * Fills in dummy credentials for the given app name. Assumes + * that a confirmation message box will be shown, which will be + * handled. + */ +export const fillInDummyCredentialsForAppWithConfirm = (appName: string) => { + fillInDummyCredentialsForApp(appName); + messageBox.actions.cancel(); +}; diff --git a/packages/editor-ui/src/utils/nodeTypes/nodeTypeTransforms.ts b/packages/editor-ui/src/utils/nodeTypes/nodeTypeTransforms.ts index a068f86c59..c3a6dbbe4a 100644 --- a/packages/editor-ui/src/utils/nodeTypes/nodeTypeTransforms.ts +++ b/packages/editor-ui/src/utils/nodeTypes/nodeTypeTransforms.ts @@ -1,6 +1,9 @@ -import type { INodeTypeDescription } from 'n8n-workflow'; +import { type INodeTypeDescription } from 'n8n-workflow'; import type { NodeTypesByTypeNameAndVersion } from '@/Interface'; import { DEFAULT_NODETYPE_VERSION } from '@/constants'; +import type { NodeTypesStore } from '@/stores/nodeTypes.store'; + +export type NodeTypeProvider = Pick; export function getNodeVersions(nodeType: INodeTypeDescription) { return Array.isArray(nodeType.version) ? nodeType.version : [nodeType.version]; diff --git a/packages/editor-ui/src/utils/nodes/nodeTransforms.ts b/packages/editor-ui/src/utils/nodes/nodeTransforms.ts new file mode 100644 index 0000000000..80c4a4a2a6 --- /dev/null +++ b/packages/editor-ui/src/utils/nodes/nodeTransforms.ts @@ -0,0 +1,33 @@ +import type { INodeUi } from '@/Interface'; +import type { NodeTypeProvider } from '@/utils/nodeTypes/nodeTypeTransforms'; +import type { INodeCredentialDescription } from 'n8n-workflow'; +import { NodeHelpers } from 'n8n-workflow'; + +/** + * Returns the credentials that are displayable for the given node. + */ +export function getNodeTypeDisplayableCredentials( + nodeTypeProvider: NodeTypeProvider, + node: Pick, +): INodeCredentialDescription[] { + const nodeType = nodeTypeProvider.getNodeType(node.type, node.typeVersion); + if (!nodeType?.credentials) { + return []; + } + + const nodeTypeCreds = nodeType.credentials; + + // We must populate the node's parameters with the default values + // before we can check which credentials are available, because + // credentials can have conditional requirements that depend on + // node parameters. + const nodeParameters = + NodeHelpers.getNodeParameters(nodeType.properties, node.parameters, true, false, node) ?? + node.parameters; + + const displayableCredentials = nodeTypeCreds.filter((credentialTypeDescription) => { + return NodeHelpers.displayParameter(nodeParameters, credentialTypeDescription, node); + }); + + return displayableCredentials; +} diff --git a/packages/editor-ui/src/utils/templates/__tests__/templateTransforms.test.ts b/packages/editor-ui/src/utils/templates/__tests__/templateTransforms.test.ts index e19a4081a5..d91bb9b66f 100644 --- a/packages/editor-ui/src/utils/templates/__tests__/templateTransforms.test.ts +++ b/packages/editor-ui/src/utils/templates/__tests__/templateTransforms.test.ts @@ -7,6 +7,9 @@ import { newWorkflowTemplateNode } from '@/utils/testData/templateTestData'; describe('templateTransforms', () => { describe('replaceAllTemplateNodeCredentials', () => { it('should replace credentials of nodes that have credentials', () => { + const nodeTypeProvider = { + getNodeType: vitest.fn(), + }; const node = newWorkflowTemplateNode({ type: 'n8n-nodes-base.twitter', credentials: { @@ -21,7 +24,11 @@ describe('templateTransforms', () => { }, }; - const [replacedNode] = replaceAllTemplateNodeCredentials([node], toReplaceWith); + const [replacedNode] = replaceAllTemplateNodeCredentials( + nodeTypeProvider, + [node], + toReplaceWith, + ); expect(replacedNode.credentials).toEqual({ twitterOAuth1Api: { id: 'new1', name: 'Twitter creds' }, @@ -29,6 +36,9 @@ describe('templateTransforms', () => { }); it('should not replace credentials of nodes that do not have credentials', () => { + const nodeTypeProvider = { + getNodeType: vitest.fn(), + }; const node = newWorkflowTemplateNode({ type: 'n8n-nodes-base.twitter', }); @@ -39,7 +49,11 @@ describe('templateTransforms', () => { }, }; - const [replacedNode] = replaceAllTemplateNodeCredentials([node], toReplaceWith); + const [replacedNode] = replaceAllTemplateNodeCredentials( + nodeTypeProvider, + [node], + toReplaceWith, + ); expect(replacedNode.credentials).toBeUndefined(); }); diff --git a/packages/editor-ui/src/utils/templates/templateActions.ts b/packages/editor-ui/src/utils/templates/templateActions.ts index 4290f19642..ec81a94744 100644 --- a/packages/editor-ui/src/utils/templates/templateActions.ts +++ b/packages/editor-ui/src/utils/templates/templateActions.ts @@ -5,6 +5,7 @@ import type { useRootStore } from '@/stores/n8nRoot.store'; import type { PosthogStore } from '@/stores/posthog.store'; import type { useWorkflowsStore } from '@/stores/workflows.store'; import { getFixedNodesList } from '@/utils/nodeViewUtils'; +import type { NodeTypeProvider } from '@/utils/nodeTypes/nodeTypeTransforms'; import type { TemplateCredentialKey } from '@/utils/templates/templateTransforms'; import { replaceAllTemplateNodeCredentials } from '@/utils/templates/templateTransforms'; import type { INodeCredentialsDetails } from 'n8n-workflow'; @@ -13,14 +14,18 @@ import type { RouteLocationRaw, Router } from 'vue-router'; /** * Creates a new workflow from a template */ -export async function createWorkflowFromTemplate( - template: IWorkflowTemplate, - credentialOverrides: Record, - rootStore: ReturnType, - workflowsStore: ReturnType, -) { +export async function createWorkflowFromTemplate(opts: { + template: IWorkflowTemplate; + credentialOverrides: Record; + rootStore: ReturnType; + workflowsStore: ReturnType; + nodeTypeProvider: NodeTypeProvider; +}) { + const { credentialOverrides, nodeTypeProvider, rootStore, template, workflowsStore } = opts; + const workflowData = await getNewWorkflow(rootStore.getRestApiContext, template.name); const nodesWithCreds = replaceAllTemplateNodeCredentials( + nodeTypeProvider, template.workflow.nodes, credentialOverrides, ); diff --git a/packages/editor-ui/src/utils/templates/templateTransforms.ts b/packages/editor-ui/src/utils/templates/templateTransforms.ts index 22ec022a80..8be851865a 100644 --- a/packages/editor-ui/src/utils/templates/templateTransforms.ts +++ b/packages/editor-ui/src/utils/templates/templateTransforms.ts @@ -1,4 +1,6 @@ import type { IWorkflowTemplateNode, IWorkflowTemplateNodeCredentials } from '@/Interface'; +import type { NodeTypeProvider } from '@/utils/nodeTypes/nodeTypeTransforms'; +import { getNodeTypeDisplayableCredentials } from '@/utils/nodes/nodeTransforms'; import type { NormalizedTemplateNodeCredentials } from '@/utils/templates/templateTypes'; import type { INodeCredentials, INodeCredentialsDetails } from 'n8n-workflow'; @@ -23,14 +25,6 @@ export const keyFromCredentialTypeAndName = ( credentialName: string, ): TemplateCredentialKey => `${credentialTypeName}-${credentialName}` as TemplateCredentialKey; -/** - * Checks if a template workflow node has credentials defined - */ -export const hasNodeCredentials = ( - node: IWorkflowTemplateNode, -): node is IWorkflowTemplateNodeWithCredentials => - !!node.credentials && Object.keys(node.credentials).length > 0; - /** * Normalizes the credentials of a template node. Templates created with * different versions of n8n may have different credential formats. @@ -57,26 +51,49 @@ export const normalizeTemplateNodeCredentials = ( * replaceTemplateNodeCredentials(nodeCredentials, toReplaceByKey); * // => { twitterOAuth1Api: { id: "BrEOZ5Cje6VYh9Pc", name: "X OAuth account" } } */ -export const replaceTemplateNodeCredentials = ( - nodeCredentials: IWorkflowTemplateNodeCredentials, +export const getReplacedTemplateNodeCredentials = ( + nodeCredentials: IWorkflowTemplateNodeCredentials | undefined, toReplaceByKey: Record, ) => { if (!nodeCredentials) { return undefined; } - const newNodeCredentials: INodeCredentials = {}; + const replacedNodeCredentials: INodeCredentials = {}; const normalizedCredentials = normalizeTemplateNodeCredentials(nodeCredentials); for (const credentialType in normalizedCredentials) { const credentialNameInTemplate = normalizedCredentials[credentialType]; const key = keyFromCredentialTypeAndName(credentialType, credentialNameInTemplate); const toReplaceWith = toReplaceByKey[key]; if (toReplaceWith) { - newNodeCredentials[credentialType] = toReplaceWith; + replacedNodeCredentials[credentialType] = toReplaceWith; } } - return newNodeCredentials; + return replacedNodeCredentials; +}; + +/** + * Returns credentials for the given node that are missing from it + * but are present in the given replacements + */ +export const getMissingTemplateNodeCredentials = ( + nodeTypeProvider: NodeTypeProvider, + node: IWorkflowTemplateNode, + replacementsByKey: Record, +): INodeCredentials => { + const nodeCredentialsToAdd: INodeCredentials = {}; + const usableCredentials = getNodeTypeDisplayableCredentials(nodeTypeProvider, node); + + for (const usableCred of usableCredentials) { + const credentialKey = keyFromCredentialTypeAndName(usableCred.name, ''); + + if (replacementsByKey[credentialKey]) { + nodeCredentialsToAdd[usableCred.name] = replacementsByKey[credentialKey]; + } + } + + return nodeCredentialsToAdd; }; /** @@ -84,17 +101,22 @@ export const replaceTemplateNodeCredentials = ( * replacements */ export const replaceAllTemplateNodeCredentials = ( + nodeTypeProvider: NodeTypeProvider, nodes: IWorkflowTemplateNode[], toReplaceWith: Record, ) => { return nodes.map((node) => { - if (hasNodeCredentials(node)) { - return { - ...node, - credentials: replaceTemplateNodeCredentials(node.credentials, toReplaceWith), - }; - } + const replacedCredentials = getReplacedTemplateNodeCredentials(node.credentials, toReplaceWith); + const newCredentials = getMissingTemplateNodeCredentials(nodeTypeProvider, node, toReplaceWith); - return node; + const credentials = { + ...replacedCredentials, + ...newCredentials, + }; + + return { + ...node, + credentials: Object.keys(credentials).length > 0 ? credentials : undefined, + }; }); }; diff --git a/packages/editor-ui/src/utils/testData/credentialTypeTestData.ts b/packages/editor-ui/src/utils/testData/credentialTypeTestData.ts new file mode 100644 index 0000000000..b49301a856 --- /dev/null +++ b/packages/editor-ui/src/utils/testData/credentialTypeTestData.ts @@ -0,0 +1,36 @@ +/** + * Credential type test data + */ +import type { ICredentialType } from 'n8n-workflow'; + +export const newCredentialType = (name: string): ICredentialType => ({ + name, + displayName: name, + documentationUrl: name, + properties: [], +}); + +export const credentialTypeTelegram = { + name: 'telegramApi', + displayName: 'Telegram API', + documentationUrl: 'telegram', + properties: [ + { + displayName: 'Access Token', + name: 'accessToken', + type: 'string', + typeOptions: { + password: true, + }, + default: '', + description: + 'Chat with the bot father to obtain the access token', + }, + ], + test: { + request: { + baseURL: '=https://api.telegram.org/bot{{$credentials.accessToken}}', + url: '/getMe', + }, + }, +} satisfies ICredentialType; diff --git a/packages/editor-ui/src/utils/testData/nodeTypeTestData.ts b/packages/editor-ui/src/utils/testData/nodeTypeTestData.ts new file mode 100644 index 0000000000..e623b05460 --- /dev/null +++ b/packages/editor-ui/src/utils/testData/nodeTypeTestData.ts @@ -0,0 +1,4720 @@ +import type { INodeTypeDescription } from 'n8n-workflow'; + +export const nodeTypeTwitterV1 = { + displayName: 'X (Formerly Twitter)', + name: 'n8n-nodes-base.twitter', + group: ['output'], + subtitle: '={{$parameter["operation"] + ":" + $parameter["resource"]}}', + description: 'Consume Twitter API', + defaultVersion: 2, + version: 1, + defaults: { name: 'Twitter' }, + inputs: ['main'], + outputs: ['main'], + credentials: [{ name: 'twitterOAuth1Api', required: true }], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + noDataExpression: true, + options: [ + { name: 'Direct Message', value: 'directMessage' }, + { name: 'Tweet', value: 'tweet' }, + ], + default: 'tweet', + }, + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { show: { resource: ['directMessage'] } }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a direct message', + action: 'Create a direct message', + }, + ], + default: 'create', + }, + { + displayName: 'User ID', + name: 'userId', + type: 'string', + required: true, + default: '', + displayOptions: { show: { operation: ['create'], resource: ['directMessage'] } }, + description: 'The ID of the user who should receive the direct message', + }, + { + displayName: 'Text', + name: 'text', + type: 'string', + required: true, + default: '', + displayOptions: { show: { operation: ['create'], resource: ['directMessage'] } }, + description: + 'The text of your Direct Message. URL encode as necessary. Max length of 10,000 characters.', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { show: { operation: ['create'], resource: ['directMessage'] } }, + options: [ + { + displayName: 'Attachment', + name: 'attachment', + type: 'string', + default: 'data', + description: + 'Name of the binary property which contain data that should be added to the direct message as attachment', + }, + ], + }, + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { show: { resource: ['tweet'] } }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create or reply a tweet', + action: 'Create a tweet', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a tweet', + action: 'Delete a tweet', + }, + { name: 'Like', value: 'like', description: 'Like a tweet', action: 'Like a tweet' }, + { + name: 'Retweet', + value: 'retweet', + description: 'Retweet a tweet', + action: 'Retweet a tweet', + }, + { + name: 'Search', + value: 'search', + description: 'Search tweets', + action: 'Search for tweets', + }, + ], + default: 'create', + }, + { + displayName: 'Text', + name: 'text', + type: 'string', + required: true, + default: '', + displayOptions: { show: { operation: ['create'], resource: ['tweet'] } }, + description: + 'The text of the status update. URL encode as necessary. t.co link wrapping will affect character counts.', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { show: { operation: ['create'], resource: ['tweet'] } }, + options: [ + { + displayName: 'Attachments', + name: 'attachments', + type: 'string', + default: 'data', + description: + 'Name of the binary properties which contain data which should be added to tweet as attachment. Multiple ones can be comma-separated.', + }, + { + displayName: 'Display Coordinates', + name: 'displayCoordinates', + type: 'boolean', + default: false, + description: + 'Whether or not to put a pin on the exact coordinates a Tweet has been sent from', + }, + { + displayName: 'In Reply to Tweet', + name: 'inReplyToStatusId', + type: 'string', + default: '', + description: 'The ID of an existing status that the update is in reply to', + }, + { + displayName: 'Location', + name: 'locationFieldsUi', + type: 'fixedCollection', + placeholder: 'Add Location', + default: {}, + description: 'Subscriber location information.n', + options: [ + { + name: 'locationFieldsValues', + displayName: 'Location', + values: [ + { + displayName: 'Latitude', + name: 'latitude', + type: 'string', + required: true, + description: 'The location latitude', + default: '', + }, + { + displayName: 'Longitude', + name: 'longitude', + type: 'string', + required: true, + description: 'The location longitude', + default: '', + }, + ], + }, + ], + }, + { + displayName: 'Possibly Sensitive', + name: 'possiblySensitive', + type: 'boolean', + default: false, + description: + 'Whether you are uploading Tweet media that might be considered sensitive content such as nudity, or medical procedures', + }, + ], + }, + { + displayName: 'Tweet ID', + name: 'tweetId', + type: 'string', + required: true, + default: '', + displayOptions: { show: { operation: ['delete'], resource: ['tweet'] } }, + description: 'The ID of the tweet to delete', + }, + { + displayName: 'Search Text', + name: 'searchText', + type: 'string', + required: true, + default: '', + displayOptions: { show: { operation: ['search'], resource: ['tweet'] } }, + description: + 'A UTF-8, URL-encoded search query of 500 characters maximum, including operators. Queries may additionally be limited by complexity. Check the searching examples here.', + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { show: { operation: ['search'], resource: ['tweet'] } }, + default: false, + description: 'Whether to return all results or only up to a given limit', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { operation: ['search'], resource: ['tweet'], returnAll: [false] }, + }, + typeOptions: { minValue: 1 }, + default: 50, + description: 'Max number of results to return', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { show: { operation: ['search'], resource: ['tweet'] } }, + options: [ + { + displayName: 'Include Entities', + name: 'includeEntities', + type: 'boolean', + default: false, + description: 'Whether the entities node will be included', + }, + { + displayName: 'Language Name or ID', + name: 'lang', + type: 'options', + typeOptions: { loadOptionsMethod: 'getLanguages' }, + default: '', + description: + 'Restricts tweets to the given language, given by an ISO 639-1 code. Language detection is best-effort. Choose from the list, or specify an ID using an expression.', + }, + { + displayName: 'Location', + name: 'locationFieldsUi', + type: 'fixedCollection', + placeholder: 'Add Location', + default: {}, + description: 'Subscriber location information.n', + options: [ + { + name: 'locationFieldsValues', + displayName: 'Location', + values: [ + { + displayName: 'Latitude', + name: 'latitude', + type: 'string', + required: true, + description: 'The location latitude', + default: '', + }, + { + displayName: 'Longitude', + name: 'longitude', + type: 'string', + required: true, + description: 'The location longitude', + default: '', + }, + { + displayName: 'Radius', + name: 'radius', + type: 'options', + options: [ + { name: 'Milles', value: 'mi' }, + { name: 'Kilometers', value: 'km' }, + ], + required: true, + description: + 'Returns tweets by users located within a given radius of the given latitude/longitude', + default: '', + }, + { + displayName: 'Distance', + name: 'distance', + type: 'number', + typeOptions: { minValue: 0 }, + required: true, + default: '', + }, + ], + }, + ], + }, + { + displayName: 'Result Type', + name: 'resultType', + type: 'options', + options: [ + { + name: 'Mixed', + value: 'mixed', + description: 'Include both popular and real time results in the response', + }, + { + name: 'Recent', + value: 'recent', + description: 'Return only the most recent results in the response', + }, + { + name: 'Popular', + value: 'popular', + description: 'Return only the most popular results in the response', + }, + ], + default: 'mixed', + description: 'Specifies what type of search results you would prefer to receive', + }, + { + displayName: 'Tweet Mode', + name: 'tweetMode', + type: 'options', + options: [ + { name: 'Compatibility', value: 'compat' }, + { name: 'Extended', value: 'extended' }, + ], + default: 'compat', + description: + 'When the extended mode is selected, the response contains the entire untruncated text of the Tweet', + }, + { + displayName: 'Until', + name: 'until', + type: 'dateTime', + default: '', + description: 'Returns tweets created before the given date', + }, + ], + }, + { + displayName: 'Tweet ID', + name: 'tweetId', + type: 'string', + required: true, + default: '', + displayOptions: { show: { operation: ['like'], resource: ['tweet'] } }, + description: 'The ID of the tweet', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { show: { operation: ['like'], resource: ['tweet'] } }, + options: [ + { + displayName: 'Include Entities', + name: 'includeEntities', + type: 'boolean', + default: false, + description: 'Whether the entities will be omitted', + }, + ], + }, + { + displayName: 'Tweet ID', + name: 'tweetId', + type: 'string', + required: true, + default: '', + displayOptions: { show: { operation: ['retweet'], resource: ['tweet'] } }, + description: 'The ID of the tweet', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { show: { operation: ['retweet'], resource: ['tweet'] } }, + options: [ + { + displayName: 'Trim User', + name: 'trimUser', + type: 'boolean', + default: false, + description: + 'Whether each tweet returned in a timeline will include a user object including only the status authors numerical ID', + }, + ], + }, + ], + iconUrl: 'icons/n8n-nodes-base/dist/nodes/Twitter/x.svg', + codex: { + categories: ['Marketing'], + resources: { + primaryDocumentation: [ + { url: 'https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.twitter/' }, + ], + credentialDocumentation: [{ url: 'https://docs.n8n.io/credentials/twitter' }], + }, + alias: ['Tweet', 'Twitter', 'X', 'X API'], + }, +} satisfies INodeTypeDescription; + +export const nodeTypeTwitterV2 = { + displayName: 'X (Formerly Twitter)', + name: 'n8n-nodes-base.twitter', + group: ['output'], + subtitle: '={{$parameter["operation"] + ":" + $parameter["resource"]}}', + description: 'Post, like, and search tweets, send messages, search users, and add users to lists', + defaultVersion: 2, + version: 2, + defaults: { name: 'X' }, + inputs: ['main'], + outputs: ['main'], + credentials: [{ name: 'twitterOAuth2Api', required: true }], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + noDataExpression: true, + options: [ + { + name: 'Direct Message', + value: 'directMessage', + description: 'Send a direct message to a user', + }, + { name: 'List', value: 'list', description: 'Add a user to a list' }, + { name: 'Tweet', value: 'tweet', description: 'Create, like, search, or delete a tweet' }, + { name: 'User', value: 'user', description: 'Search users by username' }, + { name: 'Custom API Call', value: '__CUSTOM_API_CALL__' }, + ], + default: 'tweet', + }, + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { show: { resource: ['directMessage'] } }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Send a direct message to a user', + action: 'Create Direct Message', + }, + { name: 'Custom API Call', value: '__CUSTOM_API_CALL__' }, + ], + default: 'create', + }, + { + displayName: 'User', + name: 'user', + type: 'resourceLocator', + default: { mode: 'username', value: '' }, + required: true, + description: 'The user you want to send the message to', + displayOptions: { show: { operation: ['create'], resource: ['directMessage'] } }, + modes: [ + { + displayName: 'By Username', + name: 'username', + type: 'string', + validation: [], + placeholder: 'e.g. n8n', + url: '', + }, + { + displayName: 'By ID', + name: 'id', + type: 'string', + validation: [], + placeholder: 'e.g. 1068479892537384960', + url: '', + }, + ], + }, + { + displayName: 'Text', + name: 'text', + type: 'string', + required: true, + default: '', + typeOptions: { rows: 2 }, + displayOptions: { show: { operation: ['create'], resource: ['directMessage'] } }, + description: + 'The text of the direct message. URL encoding is required. Max length of 10,000 characters.', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { show: { operation: ['create'], resource: ['directMessage'] } }, + options: [ + { + displayName: 'Attachment ID', + name: 'attachments', + type: 'string', + default: '', + placeholder: '1664279886239010824', + description: 'The attachment ID to associate with the message', + }, + ], + }, + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { show: { resource: ['list'] } }, + options: [ + { + name: 'Add Member', + value: 'add', + description: 'Add a member to a list', + action: 'Add Member to List', + }, + { name: 'Custom API Call', value: '__CUSTOM_API_CALL__' }, + ], + default: 'add', + }, + { + displayName: 'List', + name: 'list', + type: 'resourceLocator', + default: { mode: 'id', value: '' }, + required: true, + description: 'The list you want to add the user to', + displayOptions: { show: { operation: ['add'], resource: ['list'] } }, + modes: [ + { + displayName: 'By ID', + name: 'id', + type: 'string', + validation: [], + placeholder: 'e.g. 99923132', + url: '', + }, + { + displayName: 'By URL', + name: 'url', + type: 'string', + validation: [], + placeholder: 'e.g. https://twitter.com/i/lists/99923132', + url: '', + }, + ], + }, + { + displayName: 'User', + name: 'user', + type: 'resourceLocator', + default: { mode: 'username', value: '' }, + required: true, + description: 'The user you want to add to the list', + displayOptions: { show: { operation: ['add'], resource: ['list'] } }, + modes: [ + { + displayName: 'By Username', + name: 'username', + type: 'string', + validation: [], + placeholder: 'e.g. n8n', + url: '', + }, + { + displayName: 'By ID', + name: 'id', + type: 'string', + validation: [], + placeholder: 'e.g. 1068479892537384960', + url: '', + }, + ], + }, + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { show: { resource: ['tweet'] } }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create, quote, or reply to a tweet', + action: 'Create Tweet', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a tweet', + action: 'Delete Tweet', + }, + { name: 'Like', value: 'like', description: 'Like a tweet', action: 'Like Tweet' }, + { + name: 'Retweet', + value: 'retweet', + description: 'Retweet a tweet', + action: 'Retweet Tweet', + }, + { + name: 'Search', + value: 'search', + description: 'Search for tweets from the last seven days', + action: 'Search Tweets', + }, + { name: 'Custom API Call', value: '__CUSTOM_API_CALL__' }, + ], + default: 'create', + }, + { + displayName: 'Text', + name: 'text', + type: 'string', + typeOptions: { rows: 2 }, + default: '', + required: true, + displayOptions: { show: { operation: ['create'], resource: ['tweet'] } }, + description: + 'The text of the status update. URLs must be encoded. Links wrapped with the t.co shortener will affect character count', + }, + { + displayName: 'Options', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { show: { operation: ['create'], resource: ['tweet'] } }, + options: [ + { + displayName: 'Location ID', + name: 'location', + type: 'string', + placeholder: '4e696bef7e24d378', + default: '', + description: 'Location information for the tweet', + }, + { + displayName: 'Media ID', + name: 'attachments', + type: 'string', + default: '', + placeholder: '1664279886239010824', + description: 'The attachment ID to associate with the message', + }, + { + displayName: 'Quote a Tweet', + name: 'inQuoteToStatusId', + type: 'resourceLocator', + default: { mode: 'id', value: '' }, + description: 'The tweet being quoted', + modes: [ + { + displayName: 'By ID', + name: 'id', + type: 'string', + validation: [], + placeholder: 'e.g. 1187836157394112513', + url: '', + }, + { + displayName: 'By URL', + name: 'url', + type: 'string', + validation: [], + placeholder: 'e.g. https://twitter.com/n8n_io/status/1187836157394112513', + url: '', + }, + ], + }, + { + displayName: 'Reply to Tweet', + name: 'inReplyToStatusId', + type: 'resourceLocator', + default: { mode: 'id', value: '' }, + description: 'The tweet being replied to', + modes: [ + { + displayName: 'By ID', + name: 'id', + type: 'string', + validation: [], + placeholder: 'e.g. 1187836157394112513', + url: '', + }, + { + displayName: 'By URL', + name: 'url', + type: 'string', + validation: [], + placeholder: 'e.g. https://twitter.com/n8n_io/status/1187836157394112513', + url: '', + }, + ], + }, + ], + }, + { + displayName: 'Locations are not supported due to Twitter V2 API limitations', + name: 'noticeLocation', + type: 'notice', + displayOptions: { show: { '/additionalFields.location': [''] } }, + default: '', + }, + { + displayName: 'Attachements are not supported due to Twitter V2 API limitations', + name: 'noticeAttachments', + type: 'notice', + displayOptions: { show: { '/additionalFields.attachments': [''] } }, + default: '', + }, + { + displayName: 'Tweet', + name: 'tweetDeleteId', + type: 'resourceLocator', + default: { mode: 'id', value: '' }, + required: true, + description: 'The tweet to delete', + displayOptions: { show: { resource: ['tweet'], operation: ['delete'] } }, + modes: [ + { + displayName: 'By ID', + name: 'id', + type: 'string', + validation: [], + placeholder: 'e.g. 1187836157394112513', + url: '', + }, + { + displayName: 'By URL', + name: 'url', + type: 'string', + validation: [], + placeholder: 'e.g. https://twitter.com/n8n_io/status/1187836157394112513', + url: '', + }, + ], + }, + { + displayName: 'Tweet', + name: 'tweetId', + type: 'resourceLocator', + default: { mode: 'id', value: '' }, + required: true, + description: 'The tweet to like', + displayOptions: { show: { operation: ['like'], resource: ['tweet'] } }, + modes: [ + { + displayName: 'By ID', + name: 'id', + type: 'string', + validation: [], + placeholder: 'e.g. 1187836157394112513', + url: '', + }, + { + displayName: 'By URL', + name: 'url', + type: 'string', + validation: [], + placeholder: 'e.g. https://twitter.com/n8n_io/status/1187836157394112513', + url: '', + }, + ], + }, + { + displayName: 'Search Term', + name: 'searchText', + type: 'string', + required: true, + default: '', + placeholder: 'e.g. automation', + displayOptions: { show: { operation: ['search'], resource: ['tweet'] } }, + description: + 'A UTF-8, URL-encoded search query of 500 characters maximum, including operators. Queries may additionally be limited by complexity. Check the searching examples here.', + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + default: false, + description: 'Whether to return all results or only up to a given limit', + displayOptions: { show: { resource: ['tweet'], operation: ['search'] } }, + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + default: 50, + description: 'Max number of results to return', + typeOptions: { minValue: 1 }, + displayOptions: { + show: { resource: ['tweet'], operation: ['search'], returnAll: [false] }, + }, + }, + { + displayName: 'Options', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { show: { operation: ['search'], resource: ['tweet'] } }, + options: [ + { + displayName: 'Sort Order', + name: 'sortOrder', + type: 'options', + options: [ + { name: 'Recent', value: 'recency' }, + { name: 'Relevant', value: 'relevancy' }, + ], + description: 'The order in which to return results', + default: 'recency', + }, + { + displayName: 'After', + name: 'startTime', + type: 'dateTime', + default: '', + description: + "Tweets before this date will not be returned. This date must be within the last 7 days if you don't have Academic Research access.", + }, + { + displayName: 'Before', + name: 'endTime', + type: 'dateTime', + default: '', + description: + "Tweets after this date will not be returned. This date must be within the last 7 days if you don't have Academic Research access.", + }, + { + displayName: 'Tweet Fields', + name: 'tweetFieldsObject', + type: 'multiOptions', + options: [ + { name: 'Attachments', value: 'attachments' }, + { name: 'Author ID', value: 'author_id' }, + { name: 'Context Annotations', value: 'context_annotations' }, + { name: 'Conversation ID', value: 'conversation_id' }, + { name: 'Created At', value: 'created_at' }, + { name: 'Edit Controls', value: 'edit_controls' }, + { name: 'Entities', value: 'entities' }, + { name: 'Geo', value: 'geo' }, + { name: 'ID', value: 'id' }, + { name: 'In Reply To User ID', value: 'in_reply_to_user_id' }, + { name: 'Lang', value: 'lang' }, + { name: 'Non Public Metrics', value: 'non_public_metrics' }, + { name: 'Public Metrics', value: 'public_metrics' }, + { name: 'Organic Metrics', value: 'organic_metrics' }, + { name: 'Promoted Metrics', value: 'promoted_metrics' }, + { name: 'Possibly Sensitive', value: 'possibly_sensitive' }, + { name: 'Referenced Tweets', value: 'referenced_tweets' }, + { name: 'Reply Settings', value: 'reply_settings' }, + { name: 'Source', value: 'source' }, + { name: 'Text', value: 'text' }, + { name: 'Withheld', value: 'withheld' }, + ], + default: [], + description: + 'The fields to add to each returned tweet object. Default fields are: ID, text, edit_history_tweet_ids.', + }, + ], + }, + { + displayName: 'Tweet', + name: 'tweetId', + type: 'resourceLocator', + default: { mode: 'id', value: '' }, + required: true, + description: 'The tweet to retweet', + displayOptions: { show: { operation: ['retweet'], resource: ['tweet'] } }, + modes: [ + { + displayName: 'By ID', + name: 'id', + type: 'string', + validation: [], + placeholder: 'e.g. 1187836157394112513', + url: '', + }, + { + displayName: 'By URL', + name: 'url', + type: 'string', + validation: [], + placeholder: 'e.g. https://twitter.com/n8n_io/status/1187836157394112513', + url: '', + }, + ], + }, + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { show: { resource: ['user'] } }, + options: [ + { + name: 'Get', + value: 'searchUser', + description: 'Retrieve a user by username', + action: 'Get User', + }, + { name: 'Custom API Call', value: '__CUSTOM_API_CALL__' }, + ], + default: 'searchUser', + }, + { + displayName: 'User', + name: 'user', + type: 'resourceLocator', + default: { mode: 'username', value: '' }, + required: true, + description: 'The user you want to search', + displayOptions: { + show: { operation: ['searchUser'], resource: ['user'] }, + hide: { me: [true] }, + }, + modes: [ + { + displayName: 'By Username', + name: 'username', + type: 'string', + validation: [], + placeholder: 'e.g. n8n', + url: '', + }, + { + displayName: 'By ID', + name: 'id', + type: 'string', + validation: [], + placeholder: 'e.g. 1068479892537384960', + url: '', + }, + ], + }, + { + displayName: 'Me', + name: 'me', + type: 'boolean', + displayOptions: { show: { operation: ['searchUser'], resource: ['user'] } }, + default: false, + description: 'Whether you want to search the authenticated user', + }, + ], + iconUrl: 'icons/n8n-nodes-base/dist/nodes/Twitter/x.svg', + codex: { + categories: ['Marketing'], + resources: { + primaryDocumentation: [ + { url: 'https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.twitter/' }, + ], + credentialDocumentation: [{ url: 'https://docs.n8n.io/credentials/twitter' }], + }, + alias: ['Tweet', 'Twitter', 'X', 'X API'], + }, +} satisfies INodeTypeDescription; + +export const nodeTypeReadImapV1 = { + displayName: 'Email Trigger (IMAP)', + name: 'n8n-nodes-base.emailReadImap', + icon: 'fa:inbox', + group: ['trigger'], + description: 'Triggers the workflow when a new email is received', + defaultVersion: 2, + version: 1, + eventTriggerDescription: 'Waiting for you to receive an email', + defaults: { name: 'Email Trigger (IMAP)', color: '#44AA22' }, + triggerPanel: { + header: '', + executionsHelp: { + inactive: + "While building your workflow, click the 'listen' button, then send an email to make an event happen. This will trigger an execution, which will show up in this editor.

Once you're happy with your workflow, activate it. Then every time an email is received, the workflow will execute. These executions will show up in the executions list, but not in the editor.", + active: + "While building your workflow, click the 'listen' button, then send an email to make an event happen. This will trigger an execution, which will show up in this editor.

Your workflow will also execute automatically, since it's activated. Every time an email is received, this node will trigger an execution. These executions will show up in the executions list, but not in the editor.", + }, + activationHint: + "Once you’ve finished building your workflow, activate it to have it also listen continuously (you just won’t see those executions here).", + }, + inputs: [], + outputs: ['main'], + credentials: [{ name: 'imap', required: true, testedBy: 'imapConnectionTest' }], + properties: [ + { displayName: 'Mailbox Name', name: 'mailbox', type: 'string', default: 'INBOX' }, + { + displayName: 'Action', + name: 'postProcessAction', + type: 'options', + options: [ + { name: 'Mark as Read', value: 'read' }, + { name: 'Nothing', value: 'nothing' }, + ], + default: 'read', + description: + 'What to do after the email has been received. If "nothing" gets selected it will be processed multiple times.', + }, + { + displayName: 'Download Attachments', + name: 'downloadAttachments', + type: 'boolean', + default: false, + displayOptions: { show: { format: ['simple'] } }, + description: + 'Whether attachments of emails should be downloaded. Only set if needed as it increases processing.', + }, + { + displayName: 'Format', + name: 'format', + type: 'options', + options: [ + { + name: 'RAW', + value: 'raw', + description: + 'Returns the full email message data with body content in the raw field as a base64url encoded string; the payload field is not used', + }, + { + name: 'Resolved', + value: 'resolved', + description: + 'Returns the full email with all data resolved and attachments saved as binary data', + }, + { + name: 'Simple', + value: 'simple', + description: + 'Returns the full email; do not use if you wish to gather inline attachments', + }, + ], + default: 'simple', + description: 'The format to return the message in', + }, + { + displayName: 'Property Prefix Name', + name: 'dataPropertyAttachmentsPrefixName', + type: 'string', + default: 'attachment_', + displayOptions: { show: { format: ['resolved'] } }, + description: + 'Prefix for name of the binary property to which to write the attachments. An index starting with 0 will be added. So if name is "attachment_" the first attachment is saved to "attachment_0"', + }, + { + displayName: 'Property Prefix Name', + name: 'dataPropertyAttachmentsPrefixName', + type: 'string', + default: 'attachment_', + displayOptions: { show: { format: ['simple'], downloadAttachments: [true] } }, + description: + 'Prefix for name of the binary property to which to write the attachments. An index starting with 0 will be added. So if name is "attachment_" the first attachment is saved to "attachment_0"', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + options: [ + { + displayName: 'Custom Email Rules', + name: 'customEmailConfig', + type: 'string', + default: '["UNSEEN"]', + description: + 'Custom email fetching rules. See node-imap\'s search function for more details.', + }, + { + displayName: 'Ignore SSL Issues', + name: 'allowUnauthorizedCerts', + type: 'boolean', + default: false, + description: 'Whether to connect even if SSL certificate validation is not possible', + }, + { + displayName: 'Force Reconnect', + name: 'forceReconnect', + type: 'number', + default: 60, + description: 'Sets an interval (in minutes) to force a reconnection', + }, + ], + }, + ], + codex: { + categories: ['Communication', 'Core Nodes'], + subcategories: { 'Core Nodes': ['Other Trigger Nodes'] }, + resources: { + primaryDocumentation: [ + { + url: 'https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.emailimap/', + }, + ], + credentialDocumentation: [{ url: 'https://docs.n8n.io/credentials/imap' }], + }, + }, +} satisfies INodeTypeDescription; + +export const nodeTypeReadImapV2 = { + displayName: 'Email Trigger (IMAP)', + name: 'n8n-nodes-base.emailReadImap', + icon: 'fa:inbox', + group: ['trigger'], + description: 'Triggers the workflow when a new email is received', + defaultVersion: 2, + version: 2, + eventTriggerDescription: 'Waiting for you to receive an email', + defaults: { name: 'Email Trigger (IMAP)', color: '#44AA22' }, + triggerPanel: { + header: '', + executionsHelp: { + inactive: + "While building your workflow, click the 'listen' button, then send an email to make an event happen. This will trigger an execution, which will show up in this editor.

Once you're happy with your workflow, activate it. Then every time an email is received, the workflow will execute. These executions will show up in the executions list, but not in the editor.", + active: + "While building your workflow, click the 'listen' button, then send an email to make an event happen. This will trigger an execution, which will show up in this editor.

Your workflow will also execute automatically, since it's activated. Every time an email is received, this node will trigger an execution. These executions will show up in the executions list, but not in the editor.", + }, + activationHint: + "Once you’ve finished building your workflow, activate it to have it also listen continuously (you just won’t see those executions here).", + }, + inputs: [], + outputs: ['main'], + credentials: [{ name: 'imap', required: true, testedBy: 'imapConnectionTest' }], + properties: [ + { displayName: 'Mailbox Name', name: 'mailbox', type: 'string', default: 'INBOX' }, + { + displayName: 'Action', + name: 'postProcessAction', + type: 'options', + options: [ + { name: 'Mark as Read', value: 'read' }, + { name: 'Nothing', value: 'nothing' }, + ], + default: 'read', + description: + 'What to do after the email has been received. If "nothing" gets selected it will be processed multiple times.', + }, + { + displayName: 'Download Attachments', + name: 'downloadAttachments', + type: 'boolean', + default: false, + displayOptions: { show: { format: ['simple'] } }, + description: + 'Whether attachments of emails should be downloaded. Only set if needed as it increases processing.', + }, + { + displayName: 'Format', + name: 'format', + type: 'options', + options: [ + { + name: 'RAW', + value: 'raw', + description: + 'Returns the full email message data with body content in the raw field as a base64url encoded string; the payload field is not used', + }, + { + name: 'Resolved', + value: 'resolved', + description: + 'Returns the full email with all data resolved and attachments saved as binary data', + }, + { + name: 'Simple', + value: 'simple', + description: + 'Returns the full email; do not use if you wish to gather inline attachments', + }, + ], + default: 'simple', + description: 'The format to return the message in', + }, + { + displayName: 'Property Prefix Name', + name: 'dataPropertyAttachmentsPrefixName', + type: 'string', + default: 'attachment_', + displayOptions: { show: { format: ['resolved'] } }, + description: + 'Prefix for name of the binary property to which to write the attachments. An index starting with 0 will be added. So if name is "attachment_" the first attachment is saved to "attachment_0"', + }, + { + displayName: 'Property Prefix Name', + name: 'dataPropertyAttachmentsPrefixName', + type: 'string', + default: 'attachment_', + displayOptions: { show: { format: ['simple'], downloadAttachments: [true] } }, + description: + 'Prefix for name of the binary property to which to write the attachments. An index starting with 0 will be added. So if name is "attachment_" the first attachment is saved to "attachment_0"', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + options: [ + { + displayName: 'Custom Email Rules', + name: 'customEmailConfig', + type: 'string', + default: '["UNSEEN"]', + description: + 'Custom email fetching rules. See node-imap\'s search function for more details.', + }, + { + displayName: 'Force Reconnect Every Minutes', + name: 'forceReconnect', + type: 'number', + default: 60, + description: 'Sets an interval (in minutes) to force a reconnection', + }, + ], + }, + ], + codex: { + categories: ['Communication', 'Core Nodes'], + subcategories: { 'Core Nodes': ['Other Trigger Nodes'] }, + resources: { + primaryDocumentation: [ + { + url: 'https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.emailimap/', + }, + ], + credentialDocumentation: [{ url: 'https://docs.n8n.io/credentials/imap' }], + }, + }, +} satisfies INodeTypeDescription; + +export const nodeTypeNextCloudV1 = { + displayName: 'Nextcloud', + name: 'n8n-nodes-base.nextCloud', + group: ['input'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Access data on Nextcloud', + defaults: { name: 'Nextcloud' }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'nextCloudApi', + required: true, + displayOptions: { show: { authentication: ['accessToken'] } }, + }, + { + name: 'nextCloudOAuth2Api', + required: true, + displayOptions: { show: { authentication: ['oAuth2'] } }, + }, + ], + properties: [ + { + displayName: 'Authentication', + name: 'authentication', + type: 'options', + options: [ + { name: 'Access Token', value: 'accessToken' }, + { name: 'OAuth2', value: 'oAuth2' }, + ], + default: 'accessToken', + }, + { + displayName: 'Resource', + name: 'resource', + type: 'options', + noDataExpression: true, + options: [ + { name: 'File', value: 'file' }, + { name: 'Folder', value: 'folder' }, + { name: 'User', value: 'user' }, + { name: 'Custom API Call', value: '__CUSTOM_API_CALL__' }, + ], + default: 'file', + }, + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { show: { resource: ['file'] } }, + options: [ + { name: 'Copy', value: 'copy', description: 'Copy a file', action: 'Copy a file' }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a file', + action: 'Delete a file', + }, + { + name: 'Download', + value: 'download', + description: 'Download a file', + action: 'Download a file', + }, + { name: 'Move', value: 'move', description: 'Move a file', action: 'Move a file' }, + { name: 'Share', value: 'share', description: 'Share a file', action: 'Share a file' }, + { + name: 'Upload', + value: 'upload', + description: 'Upload a file', + action: 'Upload a file', + }, + { name: 'Custom API Call', value: '__CUSTOM_API_CALL__' }, + ], + default: 'upload', + }, + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { show: { resource: ['folder'] } }, + options: [ + { name: 'Copy', value: 'copy', description: 'Copy a folder', action: 'Copy a folder' }, + { + name: 'Create', + value: 'create', + description: 'Create a folder', + action: 'Create a folder', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a folder', + action: 'Delete a folder', + }, + { + name: 'List', + value: 'list', + description: 'Return the contents of a given folder', + action: 'List a folder', + }, + { name: 'Move', value: 'move', description: 'Move a folder', action: 'Move a folder' }, + { + name: 'Share', + value: 'share', + description: 'Share a folder', + action: 'Share a folder', + }, + { name: 'Custom API Call', value: '__CUSTOM_API_CALL__' }, + ], + default: 'create', + }, + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { show: { resource: ['user'] } }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Invite a user to a NextCloud organization', + action: 'Create a user', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a user', + action: 'Delete a user', + }, + { + name: 'Get', + value: 'get', + description: 'Retrieve information about a single user', + action: 'Get a user', + }, + { + name: 'Get Many', + value: 'getAll', + description: 'Retrieve a list of users', + action: 'Get many users', + }, + { + name: 'Update', + value: 'update', + description: 'Edit attributes related to a user', + action: 'Update a user', + }, + { name: 'Custom API Call', value: '__CUSTOM_API_CALL__' }, + ], + default: 'create', + }, + { + displayName: 'From Path', + name: 'path', + type: 'string', + default: '', + required: true, + displayOptions: { show: { operation: ['copy'], resource: ['file', 'folder'] } }, + placeholder: '/invoices/original.txt', + description: 'The path of file or folder to copy. The path should start with "/".', + }, + { + displayName: 'To Path', + name: 'toPath', + type: 'string', + default: '', + required: true, + displayOptions: { show: { operation: ['copy'], resource: ['file', 'folder'] } }, + placeholder: '/invoices/copy.txt', + description: 'The destination path of file or folder. The path should start with "/".', + }, + { + displayName: 'Delete Path', + name: 'path', + type: 'string', + default: '', + required: true, + displayOptions: { show: { operation: ['delete'], resource: ['file', 'folder'] } }, + placeholder: '/invoices/2019/invoice_1.pdf', + description: + 'The path to delete. Can be a single file or a whole folder. The path should start with "/".', + }, + { + displayName: 'From Path', + name: 'path', + type: 'string', + default: '', + required: true, + displayOptions: { show: { operation: ['move'], resource: ['file', 'folder'] } }, + placeholder: '/invoices/old_name.txt', + description: 'The path of file or folder to move. The path should start with "/".', + }, + { + displayName: 'To Path', + name: 'toPath', + type: 'string', + default: '', + required: true, + displayOptions: { show: { operation: ['move'], resource: ['file', 'folder'] } }, + placeholder: '/invoices/new_name.txt', + description: 'The new path of file or folder. The path should start with "/".', + }, + { + displayName: 'File Path', + name: 'path', + type: 'string', + default: '', + required: true, + displayOptions: { show: { operation: ['download'], resource: ['file'] } }, + placeholder: '/invoices/2019/invoice_1.pdf', + description: + 'The file path of the file to download. Has to contain the full path. The path should start with "/".', + }, + { + displayName: 'Binary Property', + name: 'binaryPropertyName', + type: 'string', + default: 'data', + required: true, + displayOptions: { show: { operation: ['download'], resource: ['file'] } }, + description: 'Name of the binary property to which to write the data of the read file', + }, + { + displayName: 'File Path', + name: 'path', + type: 'string', + default: '', + required: true, + displayOptions: { show: { operation: ['upload'], resource: ['file'] } }, + placeholder: '/invoices/2019/invoice_1.pdf', + description: + 'The absolute file path of the file to upload. Has to contain the full path. The parent folder has to exist. Existing files get overwritten.', + }, + { + displayName: 'Binary Data', + name: 'binaryDataUpload', + type: 'boolean', + default: false, + required: true, + displayOptions: { show: { operation: ['upload'], resource: ['file'] } }, + }, + { + displayName: 'File Content', + name: 'fileContent', + type: 'string', + default: '', + displayOptions: { + show: { binaryDataUpload: [false], operation: ['upload'], resource: ['file'] }, + }, + placeholder: '', + description: 'The text content of the file to upload', + }, + { + displayName: 'Binary Property', + name: 'binaryPropertyName', + type: 'string', + default: 'data', + required: true, + displayOptions: { + show: { binaryDataUpload: [true], operation: ['upload'], resource: ['file'] }, + }, + placeholder: '', + description: + 'Name of the binary property which contains the data for the file to be uploaded', + }, + { + displayName: 'File Path', + name: 'path', + type: 'string', + default: '', + required: true, + displayOptions: { show: { operation: ['share'], resource: ['file', 'folder'] } }, + placeholder: '/invoices/2019/invoice_1.pdf', + description: + 'The file path of the file to share. Has to contain the full path. The path should start with "/".', + }, + { + displayName: 'Share Type', + name: 'shareType', + type: 'options', + displayOptions: { show: { operation: ['share'], resource: ['file', 'folder'] } }, + options: [ + { name: 'Circle', value: 7 }, + { name: 'Email', value: 4 }, + { name: 'Group', value: 1 }, + { name: 'Public Link', value: 3 }, + { name: 'User', value: 0 }, + ], + default: 0, + description: 'The share permissions to set', + }, + { + displayName: 'Circle ID', + name: 'circleId', + type: 'string', + displayOptions: { + show: { resource: ['file', 'folder'], operation: ['share'], shareType: [7] }, + }, + default: '', + description: 'The ID of the circle to share with', + }, + { + displayName: 'Email', + name: 'email', + type: 'string', + placeholder: 'name@email.com', + displayOptions: { + show: { resource: ['file', 'folder'], operation: ['share'], shareType: [4] }, + }, + default: '', + description: 'The Email address to share with', + }, + { + displayName: 'Group ID', + name: 'groupId', + type: 'string', + displayOptions: { + show: { resource: ['file', 'folder'], operation: ['share'], shareType: [1] }, + }, + default: '', + description: 'The ID of the group to share with', + }, + { + displayName: 'User', + name: 'user', + type: 'string', + displayOptions: { + show: { resource: ['file', 'folder'], operation: ['share'], shareType: [0] }, + }, + default: '', + description: 'The user to share with', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + displayOptions: { show: { resource: ['file', 'folder'], operation: ['share'] } }, + options: [ + { + displayName: 'Password', + name: 'password', + type: 'string', + typeOptions: { password: true }, + displayOptions: { + show: { '/resource': ['file', 'folder'], '/operation': ['share'], '/shareType': [3] }, + }, + default: '', + description: 'Optional search string', + }, + { + displayName: 'Permissions', + name: 'permissions', + type: 'options', + options: [ + { name: 'All', value: 31 }, + { name: 'Create', value: 4 }, + { name: 'Delete', value: 8 }, + { name: 'Read', value: 1 }, + { name: 'Update', value: 2 }, + ], + default: 1, + description: 'The share permissions to set', + }, + ], + }, + { + displayName: 'Folder', + name: 'path', + type: 'string', + default: '', + required: true, + displayOptions: { show: { operation: ['create'], resource: ['folder'] } }, + placeholder: '/invoices/2019', + description: + 'The folder to create. The parent folder has to exist. The path should start with "/".', + }, + { + displayName: 'Folder Path', + name: 'path', + type: 'string', + default: '', + displayOptions: { show: { operation: ['list'], resource: ['folder'] } }, + placeholder: '/invoices/2019/', + description: 'The path of which to list the content. The path should start with "/".', + }, + { + displayName: 'Username', + name: 'userId', + type: 'string', + default: '', + required: true, + displayOptions: { show: { resource: ['user'], operation: ['create'] } }, + placeholder: 'john', + description: 'Username the user will have', + }, + { + displayName: 'Email', + name: 'email', + type: 'string', + default: '', + required: true, + displayOptions: { show: { resource: ['user'], operation: ['create'] } }, + placeholder: 'john@email.com', + description: 'The email of the user to invite', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { show: { resource: ['user'], operation: ['create'] } }, + options: [ + { + displayName: 'Display Name', + name: 'displayName', + type: 'string', + default: '', + description: 'The display name of the user to invite', + }, + ], + }, + { + displayName: 'Username', + name: 'userId', + type: 'string', + default: '', + required: true, + displayOptions: { show: { resource: ['user'], operation: ['delete', 'get', 'update'] } }, + placeholder: 'john', + description: 'Username the user will have', + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { show: { resource: ['user'], operation: ['getAll'] } }, + default: false, + description: 'Whether to return all results or only up to a given limit', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { show: { resource: ['user'], operation: ['getAll'], returnAll: [false] } }, + typeOptions: { minValue: 1, maxValue: 100 }, + default: 50, + description: 'Max number of results to return', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + displayOptions: { show: { resource: ['user'], operation: ['getAll'] } }, + options: [ + { + displayName: 'Search', + name: 'search', + type: 'string', + default: '', + description: 'Optional search string', + }, + { + displayName: 'Offset', + name: 'offset', + type: 'number', + default: '', + description: 'Optional offset value', + }, + ], + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'fixedCollection', + typeOptions: { multipleValues: false }, + placeholder: 'Add Option', + default: {}, + displayOptions: { show: { resource: ['user'], operation: ['update'] } }, + options: [ + { + displayName: 'Fields', + name: 'field', + values: [ + { + displayName: 'Key', + name: 'key', + type: 'options', + default: 'email', + options: [ + { + name: 'Address', + value: 'address', + description: 'The new address for the user', + }, + { + name: 'Display Name', + value: 'displayname', + description: 'The new display name for the user', + }, + { name: 'Email', value: 'email', description: 'The new email for the user' }, + { + name: 'Password', + value: 'password', + description: 'The new password for the user', + }, + { + name: 'Twitter', + value: 'twitter', + description: 'The new twitter handle for the user', + }, + { + name: 'Website', + value: 'website', + description: 'The new website for the user', + }, + ], + description: 'Key of the updated attribute', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + description: 'Value of the updated attribute', + }, + ], + }, + ], + }, + ], + codex: { + categories: ['Data & Storage'], + resources: { + primaryDocumentation: [ + { url: 'https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.nextcloud/' }, + ], + credentialDocumentation: [{ url: 'https://docs.n8n.io/credentials/nextCloud' }], + }, + }, + iconUrl: 'icons/n8n-nodes-base/dist/nodes/NextCloud/nextcloud.svg', +} satisfies INodeTypeDescription; + +export const nodeTypeTelegramV1 = { + displayName: 'Telegram', + name: 'n8n-nodes-base.telegram', + group: ['output'], + version: [1, 1.1], + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Sends data to Telegram', + defaults: { name: 'Telegram' }, + inputs: ['main'], + outputs: ['main'], + credentials: [{ name: 'telegramApi', required: true }], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + noDataExpression: true, + options: [ + { name: 'Chat', value: 'chat' }, + { name: 'Callback', value: 'callback' }, + { name: 'File', value: 'file' }, + { name: 'Message', value: 'message' }, + ], + default: 'message', + }, + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { show: { resource: ['chat'] } }, + options: [ + { + name: 'Get', + value: 'get', + description: 'Get up to date information about a chat', + action: 'Get a chat', + }, + { + name: 'Get Administrators', + value: 'administrators', + description: 'Get the Administrators of a chat', + action: 'Get all administrators in a chat', + }, + { + name: 'Get Member', + value: 'member', + description: 'Get the member of a chat', + action: 'Get a member in a chat', + }, + { + name: 'Leave', + value: 'leave', + description: 'Leave a group, supergroup or channel', + action: 'Leave a chat', + }, + { + name: 'Set Description', + value: 'setDescription', + description: 'Set the description of a chat', + action: 'Set description on a chat', + }, + { + name: 'Set Title', + value: 'setTitle', + description: 'Set the title of a chat', + action: 'Set a title on a chat', + }, + ], + default: 'get', + }, + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { show: { resource: ['callback'] } }, + options: [ + { + name: 'Answer Query', + value: 'answerQuery', + description: 'Send answer to callback query sent from inline keyboard', + action: 'Answer Query a callback', + }, + { + name: 'Answer Inline Query', + value: 'answerInlineQuery', + description: 'Send answer to callback query sent from inline bot', + action: 'Answer an inline query callback', + }, + ], + default: 'answerQuery', + }, + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { show: { resource: ['file'] } }, + options: [{ name: 'Get', value: 'get', description: 'Get a file', action: 'Get a file' }], + default: 'get', + }, + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { show: { resource: ['message'] } }, + options: [ + { + name: 'Delete Chat Message', + value: 'deleteMessage', + description: 'Delete a chat message', + action: 'Delete a chat message', + }, + { + name: 'Edit Message Text', + value: 'editMessageText', + description: 'Edit a text message', + action: 'Edit a test message', + }, + { + name: 'Pin Chat Message', + value: 'pinChatMessage', + description: 'Pin a chat message', + action: 'Pin a chat message', + }, + { + name: 'Send Animation', + value: 'sendAnimation', + description: 'Send an animated file', + action: 'Send an animated file', + }, + { + name: 'Send Audio', + value: 'sendAudio', + description: 'Send a audio file', + action: 'Send an audio file', + }, + { + name: 'Send Chat Action', + value: 'sendChatAction', + description: 'Send a chat action', + action: 'Send a chat action', + }, + { + name: 'Send Document', + value: 'sendDocument', + description: 'Send a document', + action: 'Send a document', + }, + { + name: 'Send Location', + value: 'sendLocation', + description: 'Send a location', + action: 'Send a location', + }, + { + name: 'Send Media Group', + value: 'sendMediaGroup', + description: 'Send group of photos or videos to album', + action: 'Send a media group message', + }, + { + name: 'Send Message', + value: 'sendMessage', + description: 'Send a text message', + action: 'Send a text message', + }, + { + name: 'Send Photo', + value: 'sendPhoto', + description: 'Send a photo', + action: 'Send a photo message', + }, + { + name: 'Send Sticker', + value: 'sendSticker', + description: 'Send a sticker', + action: 'Send a sticker', + }, + { + name: 'Send Video', + value: 'sendVideo', + description: 'Send a video', + action: 'Send a video', + }, + { + name: 'Unpin Chat Message', + value: 'unpinChatMessage', + description: 'Unpin a chat message', + action: 'Unpin a chat message', + }, + ], + default: 'sendMessage', + }, + { + displayName: 'Chat ID', + name: 'chatId', + type: 'string', + default: '', + displayOptions: { + show: { + operation: [ + 'administrators', + 'deleteMessage', + 'get', + 'leave', + 'member', + 'pinChatMessage', + 'setDescription', + 'setTitle', + 'sendAnimation', + 'sendAudio', + 'sendChatAction', + 'sendDocument', + 'sendLocation', + 'sendMessage', + 'sendMediaGroup', + 'sendPhoto', + 'sendSticker', + 'sendVideo', + 'unpinChatMessage', + ], + resource: ['chat', 'message'], + }, + }, + required: true, + description: + 'Unique identifier for the target chat or username of the target channel (in the format @channelusername)', + }, + { + displayName: 'Message ID', + name: 'messageId', + type: 'string', + default: '', + displayOptions: { show: { operation: ['deleteMessage'], resource: ['message'] } }, + required: true, + description: 'Unique identifier of the message to delete', + }, + { + displayName: 'Message ID', + name: 'messageId', + type: 'string', + default: '', + displayOptions: { + show: { operation: ['pinChatMessage', 'unpinChatMessage'], resource: ['message'] }, + }, + required: true, + description: 'Unique identifier of the message to pin or unpin', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + displayOptions: { show: { operation: ['pinChatMessage'], resource: ['message'] } }, + default: {}, + options: [ + { + displayName: 'Disable Notification', + name: 'disable_notification', + type: 'boolean', + default: false, + description: + 'Whether to send a notification to all chat members about the new pinned message', + }, + ], + }, + { + displayName: 'User ID', + name: 'userId', + type: 'string', + default: '', + displayOptions: { show: { operation: ['member'], resource: ['chat'] } }, + required: true, + description: 'Unique identifier of the target user', + }, + { + displayName: 'Description', + name: 'description', + type: 'string', + default: '', + displayOptions: { show: { operation: ['setDescription'], resource: ['chat'] } }, + required: true, + description: 'New chat description, 0-255 characters', + }, + { + displayName: 'Title', + name: 'title', + type: 'string', + default: '', + displayOptions: { show: { operation: ['setTitle'], resource: ['chat'] } }, + required: true, + description: 'New chat title, 1-255 characters', + }, + { + displayName: 'Query ID', + name: 'queryId', + type: 'string', + default: '', + displayOptions: { show: { operation: ['answerQuery'], resource: ['callback'] } }, + required: true, + description: 'Unique identifier for the query to be answered', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + displayOptions: { show: { operation: ['answerQuery'], resource: ['callback'] } }, + default: {}, + options: [ + { + displayName: 'Cache Time', + name: 'cache_time', + type: 'number', + typeOptions: { minValue: 0 }, + default: 0, + description: + 'The maximum amount of time in seconds that the result of the callback query may be cached client-side', + }, + { + displayName: 'Show Alert', + name: 'show_alert', + type: 'boolean', + default: false, + description: + 'Whether an alert will be shown by the client instead of a notification at the top of the chat screen', + }, + { + displayName: 'Text', + name: 'text', + type: 'string', + default: '', + description: + 'Text of the notification. If not specified, nothing will be shown to the user, 0-200 characters.', + }, + { + displayName: 'URL', + name: 'url', + type: 'string', + default: '', + description: "URL that will be opened by the user's client", + }, + ], + }, + { + displayName: 'Query ID', + name: 'queryId', + type: 'string', + default: '', + displayOptions: { show: { operation: ['answerInlineQuery'], resource: ['callback'] } }, + required: true, + description: 'Unique identifier for the answered query', + }, + { + displayName: 'Results', + name: 'results', + type: 'string', + default: '', + displayOptions: { show: { operation: ['answerInlineQuery'], resource: ['callback'] } }, + required: true, + description: 'A JSON-serialized array of results for the inline query', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + displayOptions: { show: { operation: ['answerInlineQuery'], resource: ['callback'] } }, + default: {}, + options: [ + { + displayName: 'Cache Time', + name: 'cache_time', + type: 'number', + typeOptions: { minValue: 0 }, + default: 0, + description: + 'The maximum amount of time in seconds that the result of the callback query may be cached client-side', + }, + { + displayName: 'Show Alert', + name: 'show_alert', + type: 'boolean', + default: false, + description: + 'Whether an alert will be shown by the client instead of a notification at the top of the chat screen', + }, + { + displayName: 'Text', + name: 'text', + type: 'string', + default: '', + description: + 'Text of the notification. If not specified, nothing will be shown to the user, 0-200 characters.', + }, + { + displayName: 'URL', + name: 'url', + type: 'string', + default: '', + description: "URL that will be opened by the user's client", + }, + ], + }, + { + displayName: 'File ID', + name: 'fileId', + type: 'string', + default: '', + displayOptions: { show: { operation: ['get'], resource: ['file'] } }, + required: true, + description: 'The ID of the file', + }, + { + displayName: 'Download', + name: 'download', + type: 'boolean', + displayOptions: { show: { operation: ['get'], resource: ['file'] } }, + default: true, + description: 'Whether to download the file', + }, + { + displayName: 'Message Type', + name: 'messageType', + type: 'options', + displayOptions: { show: { operation: ['editMessageText'], resource: ['message'] } }, + options: [ + { name: 'Inline Message', value: 'inlineMessage' }, + { name: 'Message', value: 'message' }, + ], + default: 'message', + description: 'The type of the message to edit', + }, + { + displayName: 'Chat ID', + name: 'chatId', + type: 'string', + default: '', + displayOptions: { + show: { messageType: ['message'], operation: ['editMessageText'], resource: ['message'] }, + }, + required: true, + description: + 'Unique identifier for the target chat or username of the target channel (in the format @channelusername). To find your chat ID ask @get_id_bot.', + }, + { + displayName: 'Binary Data', + name: 'binaryData', + type: 'boolean', + default: false, + required: true, + displayOptions: { + show: { + operation: [ + 'sendAnimation', + 'sendAudio', + 'sendDocument', + 'sendPhoto', + 'sendVideo', + 'sendSticker', + ], + resource: ['message'], + }, + }, + description: 'Whether the data to upload should be taken from binary field', + }, + { + displayName: 'Binary Property', + name: 'binaryPropertyName', + type: 'string', + default: 'data', + required: true, + displayOptions: { + show: { + operation: [ + 'sendAnimation', + 'sendAudio', + 'sendDocument', + 'sendPhoto', + 'sendVideo', + 'sendSticker', + ], + resource: ['message'], + binaryData: [true], + }, + }, + placeholder: '', + description: 'Name of the binary property that contains the data to upload', + }, + { + displayName: 'Message ID', + name: 'messageId', + type: 'string', + default: '', + displayOptions: { + show: { messageType: ['message'], operation: ['editMessageText'], resource: ['message'] }, + }, + required: true, + description: 'Unique identifier of the message to edit', + }, + { + displayName: 'Inline Message ID', + name: 'inlineMessageId', + type: 'string', + default: '', + displayOptions: { + show: { + messageType: ['inlineMessage'], + operation: ['editMessageText'], + resource: ['message'], + }, + }, + required: true, + description: 'Unique identifier of the inline message to edit', + }, + { + displayName: 'Reply Markup', + name: 'replyMarkup', + displayOptions: { show: { operation: ['editMessageText'], resource: ['message'] } }, + type: 'options', + options: [ + { name: 'None', value: 'none' }, + { name: 'Inline Keyboard', value: 'inlineKeyboard' }, + ], + default: 'none', + description: 'Additional interface options', + }, + { + displayName: 'Animation', + name: 'file', + type: 'string', + default: '', + displayOptions: { + show: { operation: ['sendAnimation'], resource: ['message'], binaryData: [false] }, + }, + description: + 'Animation to send. Pass a file_id to send an animation that exists on the Telegram servers (recommended), an HTTP URL for Telegram to get an animation from the Internet.', + }, + { + displayName: 'Audio', + name: 'file', + type: 'string', + default: '', + displayOptions: { + show: { operation: ['sendAudio'], resource: ['message'], binaryData: [false] }, + }, + description: + 'Audio file to send. Pass a file_id to send a file that exists on the Telegram servers (recommended), an HTTP URL for Telegram to get a file from the Internet.', + }, + { + displayName: 'Action', + name: 'action', + type: 'options', + displayOptions: { show: { operation: ['sendChatAction'], resource: ['message'] } }, + options: [ + { name: 'Find Location', value: 'find_location', action: 'Find location' }, + { name: 'Record Audio', value: 'record_audio', action: 'Record audio' }, + { name: 'Record Video', value: 'record_video', action: 'Record video' }, + { name: 'Record Video Note', value: 'record_video_note', action: 'Record video note' }, + { name: 'Typing', value: 'typing', action: 'Typing a message' }, + { name: 'Upload Audio', value: 'upload_audio', action: 'Upload audio' }, + { name: 'Upload Document', value: 'upload_document', action: 'Upload document' }, + { name: 'Upload Photo', value: 'upload_photo', action: 'Upload photo' }, + { name: 'Upload Video', value: 'upload_video', action: 'Upload video' }, + { name: 'Upload Video Note', value: 'upload_video_note', action: 'Upload video note' }, + ], + default: 'typing', + description: + 'Type of action to broadcast. Choose one, depending on what the user is about to receive. The status is set for 5 seconds or less (when a message arrives from your bot).', + }, + { + displayName: 'Document', + name: 'file', + type: 'string', + default: '', + displayOptions: { + show: { operation: ['sendDocument'], resource: ['message'], binaryData: [false] }, + }, + description: + 'Document to send. Pass a file_id to send a file that exists on the Telegram servers (recommended), an HTTP URL for Telegram to get a file from the Internet.', + }, + { + displayName: 'Latitude', + name: 'latitude', + type: 'number', + default: 0, + typeOptions: { numberPrecision: 10, minValue: -90, maxValue: 90 }, + displayOptions: { show: { operation: ['sendLocation'], resource: ['message'] } }, + description: 'Location latitude', + }, + { + displayName: 'Longitude', + name: 'longitude', + type: 'number', + typeOptions: { numberPrecision: 10, minValue: -180, maxValue: 180 }, + default: 0, + displayOptions: { show: { operation: ['sendLocation'], resource: ['message'] } }, + description: 'Location longitude', + }, + { + displayName: 'Media', + name: 'media', + type: 'fixedCollection', + displayOptions: { show: { operation: ['sendMediaGroup'], resource: ['message'] } }, + description: 'The media to add', + placeholder: 'Add Media', + typeOptions: { multipleValues: true }, + default: {}, + options: [ + { + displayName: 'Media', + name: 'media', + values: [ + { + displayName: 'Type', + name: 'type', + type: 'options', + options: [ + { name: 'Photo', value: 'photo' }, + { name: 'Video', value: 'video' }, + ], + default: 'photo', + description: 'The type of the media to add', + }, + { + displayName: 'Media File', + name: 'media', + type: 'string', + default: '', + description: + 'Media to send. Pass a file_id to send a file that exists on the Telegram servers (recommended) or pass an HTTP URL for Telegram to get a file from the Internet.', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + options: [ + { + displayName: 'Caption', + name: 'caption', + type: 'string', + default: '', + description: 'Caption text to set, 0-1024 characters', + }, + { + displayName: 'Parse Mode', + name: 'parse_mode', + type: 'options', + options: [ + { name: 'Markdown (Legacy)', value: 'Markdown' }, + { name: 'MarkdownV2', value: 'MarkdownV2' }, + { name: 'HTML', value: 'HTML' }, + ], + default: 'HTML', + description: 'How to parse the text', + }, + ], + }, + ], + }, + ], + }, + { + displayName: 'Text', + name: 'text', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { operation: ['editMessageText', 'sendMessage'], resource: ['message'] }, + }, + description: 'Text of the message to be sent', + }, + { + displayName: 'Photo', + name: 'file', + type: 'string', + default: '', + displayOptions: { + show: { operation: ['sendPhoto'], resource: ['message'], binaryData: [false] }, + }, + description: + 'Photo to send. Pass a file_id to send a photo that exists on the Telegram servers (recommended), an HTTP URL for Telegram to get a photo from the Internet.', + }, + { + displayName: 'Sticker', + name: 'file', + type: 'string', + default: '', + displayOptions: { + show: { operation: ['sendSticker'], resource: ['message'], binaryData: [false] }, + }, + description: + 'Sticker to send. Pass a file_id to send a file that exists on the Telegram servers (recommended), an HTTP URL for Telegram to get a .webp file from the Internet.', + }, + { + displayName: 'Video', + name: 'file', + type: 'string', + default: '', + displayOptions: { + show: { operation: ['sendVideo'], resource: ['message'], binaryData: [false] }, + }, + description: + 'Video file to send. Pass a file_id to send a file that exists on the Telegram servers (recommended), an HTTP URL for Telegram to get a file from the Internet.', + }, + { + displayName: 'Reply Markup', + name: 'replyMarkup', + displayOptions: { + show: { + operation: [ + 'sendAnimation', + 'sendDocument', + 'sendMessage', + 'sendPhoto', + 'sendSticker', + 'sendVideo', + 'sendAudio', + 'sendLocation', + ], + resource: ['message'], + }, + }, + type: 'options', + options: [ + { name: 'Force Reply', value: 'forceReply' }, + { name: 'Inline Keyboard', value: 'inlineKeyboard' }, + { name: 'None', value: 'none' }, + { name: 'Reply Keyboard', value: 'replyKeyboard' }, + { name: 'Reply Keyboard Remove', value: 'replyKeyboardRemove' }, + ], + default: 'none', + description: 'Additional interface options', + }, + { + displayName: 'Force Reply', + name: 'forceReply', + type: 'collection', + placeholder: 'Add Field', + displayOptions: { show: { replyMarkup: ['forceReply'], resource: ['message'] } }, + default: {}, + options: [ + { + displayName: 'Force Reply', + name: 'force_reply', + type: 'boolean', + default: false, + description: + 'Whether to show reply interface to the user, as if they manually selected the bot‘s message and tapped ’Reply', + }, + { + displayName: 'Selective', + name: 'selective', + type: 'boolean', + default: false, + description: 'Whether to force reply from specific users only', + }, + ], + }, + { + displayName: 'Inline Keyboard', + name: 'inlineKeyboard', + placeholder: 'Add Keyboard Row', + description: 'Adds an inline keyboard that appears right next to the message it belongs to', + type: 'fixedCollection', + typeOptions: { multipleValues: true }, + displayOptions: { show: { replyMarkup: ['inlineKeyboard'], resource: ['message'] } }, + default: {}, + options: [ + { + displayName: 'Rows', + name: 'rows', + values: [ + { + displayName: 'Row', + name: 'row', + type: 'fixedCollection', + description: 'The value to set', + placeholder: 'Add Button', + typeOptions: { multipleValues: true }, + default: {}, + options: [ + { + displayName: 'Buttons', + name: 'buttons', + values: [ + { + displayName: 'Text', + name: 'text', + type: 'string', + default: '', + description: 'Label text on the button', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + options: [ + { + displayName: 'Callback Data', + name: 'callback_data', + type: 'string', + default: '', + description: + 'Data to be sent in a callback query to the bot when button is pressed, 1-64 bytes', + }, + { + displayName: 'Pay', + name: 'pay', + type: 'boolean', + default: false, + description: 'Whether to send a Pay button', + }, + { + displayName: 'Switch Inline Query Current Chat', + name: 'switch_inline_query_current_chat', + type: 'string', + default: '', + description: + "If set, pressing the button will insert the bot‘s username and the specified inline query in the current chat's input field.Can be empty, in which case only the bot’s username will be inserted", + }, + { + displayName: 'Switch Inline Query', + name: 'switch_inline_query', + type: 'string', + default: '', + description: + 'If set, pressing the button will prompt the user to select one of their chats, open that chat and insert the bot‘s username and the specified inline query in the input field. Can be empty, in which case just the bot’s username will be inserted.', + }, + { + displayName: 'URL', + name: 'url', + type: 'string', + default: '', + description: 'HTTP or tg:// URL to be opened when button is pressed', + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], + }, + { + displayName: 'Reply Keyboard', + name: 'replyKeyboard', + placeholder: 'Add Reply Keyboard Row', + description: 'Adds a custom keyboard with reply options', + type: 'fixedCollection', + typeOptions: { multipleValues: true }, + displayOptions: { show: { replyMarkup: ['replyKeyboard'] } }, + default: {}, + options: [ + { + displayName: 'Rows', + name: 'rows', + values: [ + { + displayName: 'Row', + name: 'row', + type: 'fixedCollection', + description: 'The value to set', + placeholder: 'Add Button', + typeOptions: { multipleValues: true }, + default: {}, + options: [ + { + displayName: 'Buttons', + name: 'buttons', + values: [ + { + displayName: 'Text', + name: 'text', + type: 'string', + default: '', + description: + 'Text of the button. If none of the optional fields are used, it will be sent as a message when the button is pressed.', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + options: [ + { + displayName: 'Request Contact', + name: 'request_contact', + type: 'boolean', + default: false, + description: + "Whether the user's phone number will be sent as a contact when the button is pressed.Available in private chats only", + }, + { + displayName: 'Request Location', + name: 'request_location', + type: 'boolean', + default: false, + description: "Whether the user's request_location", + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], + }, + { + displayName: 'Reply Keyboard Options', + name: 'replyKeyboardOptions', + type: 'collection', + placeholder: 'Add Option', + displayOptions: { show: { replyMarkup: ['replyKeyboard'] } }, + default: {}, + options: [ + { + displayName: 'Resize Keyboard', + name: 'resize_keyboard', + type: 'boolean', + default: false, + description: + 'Whether to request clients to resize the keyboard vertically for optimal fit', + }, + { + displayName: 'One Time Keyboard', + name: 'one_time_keyboard', + type: 'boolean', + default: false, + description: "Whether to request clients to hide the keyboard as soon as it's been used", + }, + { + displayName: 'Selective', + name: 'selective', + type: 'boolean', + default: false, + description: 'Whether to show the keyboard to specific users only', + }, + ], + }, + { + displayName: 'Reply Keyboard Remove', + name: 'replyKeyboardRemove', + type: 'collection', + placeholder: 'Add Field', + displayOptions: { show: { replyMarkup: ['replyKeyboardRemove'] } }, + default: {}, + options: [ + { + displayName: 'Remove Keyboard', + name: 'remove_keyboard', + type: 'boolean', + default: false, + description: 'Whether to request clients to remove the custom keyboard', + }, + { + displayName: 'Selective', + name: 'selective', + type: 'boolean', + default: false, + description: 'Whether to force reply from specific users only', + }, + ], + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + displayOptions: { + show: { + operation: [ + 'editMessageText', + 'sendAnimation', + 'sendAudio', + 'sendDocument', + 'sendLocation', + 'sendMessage', + 'sendMediaGroup', + 'sendPhoto', + 'sendSticker', + 'sendVideo', + ], + resource: ['message'], + }, + }, + default: {}, + options: [ + { + displayName: 'Append n8n Attribution', + name: 'appendAttribution', + type: 'boolean', + default: true, + description: + 'Whether to include the phrase “This message was sent automatically with n8n” to the end of the message', + displayOptions: { show: { '/operation': ['sendMessage'] } }, + }, + { + displayName: 'Caption', + name: 'caption', + type: 'string', + displayOptions: { + show: { + '/operation': [ + 'sendAnimation', + 'sendAudio', + 'sendDocument', + 'sendPhoto', + 'sendVideo', + ], + }, + }, + default: '', + description: 'Caption text to set, 0-1024 characters', + }, + { + displayName: 'Disable Notification', + name: 'disable_notification', + type: 'boolean', + default: false, + displayOptions: { hide: { '/operation': ['editMessageText'] } }, + description: + 'Whether to send the message silently. Users will receive a notification with no sound.', + }, + { + displayName: 'Disable WebPage Preview', + name: 'disable_web_page_preview', + type: 'boolean', + displayOptions: { show: { '/operation': ['editMessageText', 'sendMessage'] } }, + default: false, + description: 'Whether to disable link previews for links in this message', + }, + { + displayName: 'Duration', + name: 'duration', + type: 'number', + typeOptions: { minValue: 0 }, + displayOptions: { show: { '/operation': ['sendAnimation', 'sendAudio', 'sendVideo'] } }, + default: 0, + description: 'Duration of clip in seconds', + }, + { + displayName: 'File Name', + name: 'fileName', + type: 'string', + default: '', + displayOptions: { + show: { + '/operation': [ + 'sendAnimation', + 'sendAudio', + 'sendDocument', + 'sendPhoto', + 'sendVideo', + 'sendSticker', + ], + '/resource': ['message'], + '/binaryData': [true], + }, + }, + placeholder: 'image.jpeg', + }, + { + displayName: 'Height', + name: 'height', + type: 'number', + typeOptions: { minValue: 0 }, + displayOptions: { show: { '/operation': ['sendAnimation', 'sendVideo'] } }, + default: 0, + description: 'Height of the video', + }, + { + displayName: 'Parse Mode', + name: 'parse_mode', + type: 'options', + options: [ + { name: 'Markdown (Legacy)', value: 'Markdown' }, + { name: 'MarkdownV2', value: 'MarkdownV2' }, + { name: 'HTML', value: 'HTML' }, + ], + displayOptions: { + show: { + '/operation': [ + 'editMessageText', + 'sendAnimation', + 'sendAudio', + 'sendMessage', + 'sendPhoto', + 'sendVideo', + 'sendDocument', + ], + }, + }, + default: 'HTML', + description: 'How to parse the text', + }, + { + displayName: 'Performer', + name: 'performer', + type: 'string', + displayOptions: { show: { '/operation': ['sendAudio'] } }, + default: '', + description: 'Name of the performer', + }, + { + displayName: 'Reply To Message ID', + name: 'reply_to_message_id', + type: 'number', + displayOptions: { hide: { '/operation': ['editMessageText'] } }, + default: 0, + description: 'If the message is a reply, ID of the original message', + }, + { + displayName: 'Message Thread ID', + name: 'message_thread_id', + type: 'number', + displayOptions: { + show: { + '/operation': [ + 'sendAnimation', + 'sendAudio', + 'sendChatAction', + 'sendDocument', + 'sendLocation', + 'sendMediaGroup', + 'sendMessage', + 'sendPhoto', + 'sendSticker', + 'sendVideo', + ], + }, + }, + default: 0, + description: 'The unique identifier of the forum topic', + }, + { + displayName: 'Title', + name: 'title', + type: 'string', + displayOptions: { show: { '/operation': ['sendAudio'] } }, + default: '', + description: 'Title of the track', + }, + { + displayName: 'Thumbnail', + name: 'thumb', + type: 'string', + displayOptions: { + show: { '/operation': ['sendAnimation', 'sendAudio', 'sendDocument', 'sendVideo'] }, + }, + default: '', + description: + 'Thumbnail of the file sent; can be ignored if thumbnail generation for the file is supported server-side. The thumbnail should be in JPEG format and less than 200 kB in size. A thumbnail‘s width and height should not exceed 320.', + }, + { + displayName: 'Width', + name: 'width', + type: 'number', + typeOptions: { minValue: 0 }, + displayOptions: { show: { '/operation': ['sendAnimation', 'sendVideo'] } }, + default: 0, + description: 'Width of the video', + }, + ], + }, + ], + codex: { + categories: ['Communication'], + resources: { + primaryDocumentation: [ + { url: 'https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.telegram/' }, + ], + credentialDocumentation: [{ url: 'https://docs.n8n.io/credentials/telegram' }], + }, + }, + iconUrl: 'icons/n8n-nodes-base/dist/nodes/Telegram/telegram.svg', +} satisfies INodeTypeDescription; + +export const nodeTypeTelegramV1_1 = { + displayName: 'Telegram', + name: 'n8n-nodes-base.telegram', + group: ['output'], + version: [1, 1.1], + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Sends data to Telegram', + defaults: { name: 'Telegram' }, + inputs: ['main'], + outputs: ['main'], + credentials: [{ name: 'telegramApi', required: true }], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + noDataExpression: true, + options: [ + { name: 'Chat', value: 'chat' }, + { name: 'Callback', value: 'callback' }, + { name: 'File', value: 'file' }, + { name: 'Message', value: 'message' }, + ], + default: 'message', + }, + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { show: { resource: ['chat'] } }, + options: [ + { + name: 'Get', + value: 'get', + description: 'Get up to date information about a chat', + action: 'Get a chat', + }, + { + name: 'Get Administrators', + value: 'administrators', + description: 'Get the Administrators of a chat', + action: 'Get all administrators in a chat', + }, + { + name: 'Get Member', + value: 'member', + description: 'Get the member of a chat', + action: 'Get a member in a chat', + }, + { + name: 'Leave', + value: 'leave', + description: 'Leave a group, supergroup or channel', + action: 'Leave a chat', + }, + { + name: 'Set Description', + value: 'setDescription', + description: 'Set the description of a chat', + action: 'Set description on a chat', + }, + { + name: 'Set Title', + value: 'setTitle', + description: 'Set the title of a chat', + action: 'Set a title on a chat', + }, + ], + default: 'get', + }, + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { show: { resource: ['callback'] } }, + options: [ + { + name: 'Answer Query', + value: 'answerQuery', + description: 'Send answer to callback query sent from inline keyboard', + action: 'Answer Query a callback', + }, + { + name: 'Answer Inline Query', + value: 'answerInlineQuery', + description: 'Send answer to callback query sent from inline bot', + action: 'Answer an inline query callback', + }, + ], + default: 'answerQuery', + }, + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { show: { resource: ['file'] } }, + options: [{ name: 'Get', value: 'get', description: 'Get a file', action: 'Get a file' }], + default: 'get', + }, + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { show: { resource: ['message'] } }, + options: [ + { + name: 'Delete Chat Message', + value: 'deleteMessage', + description: 'Delete a chat message', + action: 'Delete a chat message', + }, + { + name: 'Edit Message Text', + value: 'editMessageText', + description: 'Edit a text message', + action: 'Edit a test message', + }, + { + name: 'Pin Chat Message', + value: 'pinChatMessage', + description: 'Pin a chat message', + action: 'Pin a chat message', + }, + { + name: 'Send Animation', + value: 'sendAnimation', + description: 'Send an animated file', + action: 'Send an animated file', + }, + { + name: 'Send Audio', + value: 'sendAudio', + description: 'Send a audio file', + action: 'Send an audio file', + }, + { + name: 'Send Chat Action', + value: 'sendChatAction', + description: 'Send a chat action', + action: 'Send a chat action', + }, + { + name: 'Send Document', + value: 'sendDocument', + description: 'Send a document', + action: 'Send a document', + }, + { + name: 'Send Location', + value: 'sendLocation', + description: 'Send a location', + action: 'Send a location', + }, + { + name: 'Send Media Group', + value: 'sendMediaGroup', + description: 'Send group of photos or videos to album', + action: 'Send a media group message', + }, + { + name: 'Send Message', + value: 'sendMessage', + description: 'Send a text message', + action: 'Send a text message', + }, + { + name: 'Send Photo', + value: 'sendPhoto', + description: 'Send a photo', + action: 'Send a photo message', + }, + { + name: 'Send Sticker', + value: 'sendSticker', + description: 'Send a sticker', + action: 'Send a sticker', + }, + { + name: 'Send Video', + value: 'sendVideo', + description: 'Send a video', + action: 'Send a video', + }, + { + name: 'Unpin Chat Message', + value: 'unpinChatMessage', + description: 'Unpin a chat message', + action: 'Unpin a chat message', + }, + ], + default: 'sendMessage', + }, + { + displayName: 'Chat ID', + name: 'chatId', + type: 'string', + default: '', + displayOptions: { + show: { + operation: [ + 'administrators', + 'deleteMessage', + 'get', + 'leave', + 'member', + 'pinChatMessage', + 'setDescription', + 'setTitle', + 'sendAnimation', + 'sendAudio', + 'sendChatAction', + 'sendDocument', + 'sendLocation', + 'sendMessage', + 'sendMediaGroup', + 'sendPhoto', + 'sendSticker', + 'sendVideo', + 'unpinChatMessage', + ], + resource: ['chat', 'message'], + }, + }, + required: true, + description: + 'Unique identifier for the target chat or username of the target channel (in the format @channelusername)', + }, + { + displayName: 'Message ID', + name: 'messageId', + type: 'string', + default: '', + displayOptions: { show: { operation: ['deleteMessage'], resource: ['message'] } }, + required: true, + description: 'Unique identifier of the message to delete', + }, + { + displayName: 'Message ID', + name: 'messageId', + type: 'string', + default: '', + displayOptions: { + show: { operation: ['pinChatMessage', 'unpinChatMessage'], resource: ['message'] }, + }, + required: true, + description: 'Unique identifier of the message to pin or unpin', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + displayOptions: { show: { operation: ['pinChatMessage'], resource: ['message'] } }, + default: {}, + options: [ + { + displayName: 'Disable Notification', + name: 'disable_notification', + type: 'boolean', + default: false, + description: + 'Whether to send a notification to all chat members about the new pinned message', + }, + ], + }, + { + displayName: 'User ID', + name: 'userId', + type: 'string', + default: '', + displayOptions: { show: { operation: ['member'], resource: ['chat'] } }, + required: true, + description: 'Unique identifier of the target user', + }, + { + displayName: 'Description', + name: 'description', + type: 'string', + default: '', + displayOptions: { show: { operation: ['setDescription'], resource: ['chat'] } }, + required: true, + description: 'New chat description, 0-255 characters', + }, + { + displayName: 'Title', + name: 'title', + type: 'string', + default: '', + displayOptions: { show: { operation: ['setTitle'], resource: ['chat'] } }, + required: true, + description: 'New chat title, 1-255 characters', + }, + { + displayName: 'Query ID', + name: 'queryId', + type: 'string', + default: '', + displayOptions: { show: { operation: ['answerQuery'], resource: ['callback'] } }, + required: true, + description: 'Unique identifier for the query to be answered', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + displayOptions: { show: { operation: ['answerQuery'], resource: ['callback'] } }, + default: {}, + options: [ + { + displayName: 'Cache Time', + name: 'cache_time', + type: 'number', + typeOptions: { minValue: 0 }, + default: 0, + description: + 'The maximum amount of time in seconds that the result of the callback query may be cached client-side', + }, + { + displayName: 'Show Alert', + name: 'show_alert', + type: 'boolean', + default: false, + description: + 'Whether an alert will be shown by the client instead of a notification at the top of the chat screen', + }, + { + displayName: 'Text', + name: 'text', + type: 'string', + default: '', + description: + 'Text of the notification. If not specified, nothing will be shown to the user, 0-200 characters.', + }, + { + displayName: 'URL', + name: 'url', + type: 'string', + default: '', + description: "URL that will be opened by the user's client", + }, + ], + }, + { + displayName: 'Query ID', + name: 'queryId', + type: 'string', + default: '', + displayOptions: { show: { operation: ['answerInlineQuery'], resource: ['callback'] } }, + required: true, + description: 'Unique identifier for the answered query', + }, + { + displayName: 'Results', + name: 'results', + type: 'string', + default: '', + displayOptions: { show: { operation: ['answerInlineQuery'], resource: ['callback'] } }, + required: true, + description: 'A JSON-serialized array of results for the inline query', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + displayOptions: { show: { operation: ['answerInlineQuery'], resource: ['callback'] } }, + default: {}, + options: [ + { + displayName: 'Cache Time', + name: 'cache_time', + type: 'number', + typeOptions: { minValue: 0 }, + default: 0, + description: + 'The maximum amount of time in seconds that the result of the callback query may be cached client-side', + }, + { + displayName: 'Show Alert', + name: 'show_alert', + type: 'boolean', + default: false, + description: + 'Whether an alert will be shown by the client instead of a notification at the top of the chat screen', + }, + { + displayName: 'Text', + name: 'text', + type: 'string', + default: '', + description: + 'Text of the notification. If not specified, nothing will be shown to the user, 0-200 characters.', + }, + { + displayName: 'URL', + name: 'url', + type: 'string', + default: '', + description: "URL that will be opened by the user's client", + }, + ], + }, + { + displayName: 'File ID', + name: 'fileId', + type: 'string', + default: '', + displayOptions: { show: { operation: ['get'], resource: ['file'] } }, + required: true, + description: 'The ID of the file', + }, + { + displayName: 'Download', + name: 'download', + type: 'boolean', + displayOptions: { show: { operation: ['get'], resource: ['file'] } }, + default: true, + description: 'Whether to download the file', + }, + { + displayName: 'Message Type', + name: 'messageType', + type: 'options', + displayOptions: { show: { operation: ['editMessageText'], resource: ['message'] } }, + options: [ + { name: 'Inline Message', value: 'inlineMessage' }, + { name: 'Message', value: 'message' }, + ], + default: 'message', + description: 'The type of the message to edit', + }, + { + displayName: 'Chat ID', + name: 'chatId', + type: 'string', + default: '', + displayOptions: { + show: { messageType: ['message'], operation: ['editMessageText'], resource: ['message'] }, + }, + required: true, + description: + 'Unique identifier for the target chat or username of the target channel (in the format @channelusername). To find your chat ID ask @get_id_bot.', + }, + { + displayName: 'Binary Data', + name: 'binaryData', + type: 'boolean', + default: false, + required: true, + displayOptions: { + show: { + operation: [ + 'sendAnimation', + 'sendAudio', + 'sendDocument', + 'sendPhoto', + 'sendVideo', + 'sendSticker', + ], + resource: ['message'], + }, + }, + description: 'Whether the data to upload should be taken from binary field', + }, + { + displayName: 'Binary Property', + name: 'binaryPropertyName', + type: 'string', + default: 'data', + required: true, + displayOptions: { + show: { + operation: [ + 'sendAnimation', + 'sendAudio', + 'sendDocument', + 'sendPhoto', + 'sendVideo', + 'sendSticker', + ], + resource: ['message'], + binaryData: [true], + }, + }, + placeholder: '', + description: 'Name of the binary property that contains the data to upload', + }, + { + displayName: 'Message ID', + name: 'messageId', + type: 'string', + default: '', + displayOptions: { + show: { messageType: ['message'], operation: ['editMessageText'], resource: ['message'] }, + }, + required: true, + description: 'Unique identifier of the message to edit', + }, + { + displayName: 'Inline Message ID', + name: 'inlineMessageId', + type: 'string', + default: '', + displayOptions: { + show: { + messageType: ['inlineMessage'], + operation: ['editMessageText'], + resource: ['message'], + }, + }, + required: true, + description: 'Unique identifier of the inline message to edit', + }, + { + displayName: 'Reply Markup', + name: 'replyMarkup', + displayOptions: { show: { operation: ['editMessageText'], resource: ['message'] } }, + type: 'options', + options: [ + { name: 'None', value: 'none' }, + { name: 'Inline Keyboard', value: 'inlineKeyboard' }, + ], + default: 'none', + description: 'Additional interface options', + }, + { + displayName: 'Animation', + name: 'file', + type: 'string', + default: '', + displayOptions: { + show: { operation: ['sendAnimation'], resource: ['message'], binaryData: [false] }, + }, + description: + 'Animation to send. Pass a file_id to send an animation that exists on the Telegram servers (recommended), an HTTP URL for Telegram to get an animation from the Internet.', + }, + { + displayName: 'Audio', + name: 'file', + type: 'string', + default: '', + displayOptions: { + show: { operation: ['sendAudio'], resource: ['message'], binaryData: [false] }, + }, + description: + 'Audio file to send. Pass a file_id to send a file that exists on the Telegram servers (recommended), an HTTP URL for Telegram to get a file from the Internet.', + }, + { + displayName: 'Action', + name: 'action', + type: 'options', + displayOptions: { show: { operation: ['sendChatAction'], resource: ['message'] } }, + options: [ + { name: 'Find Location', value: 'find_location', action: 'Find location' }, + { name: 'Record Audio', value: 'record_audio', action: 'Record audio' }, + { name: 'Record Video', value: 'record_video', action: 'Record video' }, + { name: 'Record Video Note', value: 'record_video_note', action: 'Record video note' }, + { name: 'Typing', value: 'typing', action: 'Typing a message' }, + { name: 'Upload Audio', value: 'upload_audio', action: 'Upload audio' }, + { name: 'Upload Document', value: 'upload_document', action: 'Upload document' }, + { name: 'Upload Photo', value: 'upload_photo', action: 'Upload photo' }, + { name: 'Upload Video', value: 'upload_video', action: 'Upload video' }, + { name: 'Upload Video Note', value: 'upload_video_note', action: 'Upload video note' }, + ], + default: 'typing', + description: + 'Type of action to broadcast. Choose one, depending on what the user is about to receive. The status is set for 5 seconds or less (when a message arrives from your bot).', + }, + { + displayName: 'Document', + name: 'file', + type: 'string', + default: '', + displayOptions: { + show: { operation: ['sendDocument'], resource: ['message'], binaryData: [false] }, + }, + description: + 'Document to send. Pass a file_id to send a file that exists on the Telegram servers (recommended), an HTTP URL for Telegram to get a file from the Internet.', + }, + { + displayName: 'Latitude', + name: 'latitude', + type: 'number', + default: 0, + typeOptions: { numberPrecision: 10, minValue: -90, maxValue: 90 }, + displayOptions: { show: { operation: ['sendLocation'], resource: ['message'] } }, + description: 'Location latitude', + }, + { + displayName: 'Longitude', + name: 'longitude', + type: 'number', + typeOptions: { numberPrecision: 10, minValue: -180, maxValue: 180 }, + default: 0, + displayOptions: { show: { operation: ['sendLocation'], resource: ['message'] } }, + description: 'Location longitude', + }, + { + displayName: 'Media', + name: 'media', + type: 'fixedCollection', + displayOptions: { show: { operation: ['sendMediaGroup'], resource: ['message'] } }, + description: 'The media to add', + placeholder: 'Add Media', + typeOptions: { multipleValues: true }, + default: {}, + options: [ + { + displayName: 'Media', + name: 'media', + values: [ + { + displayName: 'Type', + name: 'type', + type: 'options', + options: [ + { name: 'Photo', value: 'photo' }, + { name: 'Video', value: 'video' }, + ], + default: 'photo', + description: 'The type of the media to add', + }, + { + displayName: 'Media File', + name: 'media', + type: 'string', + default: '', + description: + 'Media to send. Pass a file_id to send a file that exists on the Telegram servers (recommended) or pass an HTTP URL for Telegram to get a file from the Internet.', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + options: [ + { + displayName: 'Caption', + name: 'caption', + type: 'string', + default: '', + description: 'Caption text to set, 0-1024 characters', + }, + { + displayName: 'Parse Mode', + name: 'parse_mode', + type: 'options', + options: [ + { name: 'Markdown (Legacy)', value: 'Markdown' }, + { name: 'MarkdownV2', value: 'MarkdownV2' }, + { name: 'HTML', value: 'HTML' }, + ], + default: 'HTML', + description: 'How to parse the text', + }, + ], + }, + ], + }, + ], + }, + { + displayName: 'Text', + name: 'text', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { operation: ['editMessageText', 'sendMessage'], resource: ['message'] }, + }, + description: 'Text of the message to be sent', + }, + { + displayName: 'Photo', + name: 'file', + type: 'string', + default: '', + displayOptions: { + show: { operation: ['sendPhoto'], resource: ['message'], binaryData: [false] }, + }, + description: + 'Photo to send. Pass a file_id to send a photo that exists on the Telegram servers (recommended), an HTTP URL for Telegram to get a photo from the Internet.', + }, + { + displayName: 'Sticker', + name: 'file', + type: 'string', + default: '', + displayOptions: { + show: { operation: ['sendSticker'], resource: ['message'], binaryData: [false] }, + }, + description: + 'Sticker to send. Pass a file_id to send a file that exists on the Telegram servers (recommended), an HTTP URL for Telegram to get a .webp file from the Internet.', + }, + { + displayName: 'Video', + name: 'file', + type: 'string', + default: '', + displayOptions: { + show: { operation: ['sendVideo'], resource: ['message'], binaryData: [false] }, + }, + description: + 'Video file to send. Pass a file_id to send a file that exists on the Telegram servers (recommended), an HTTP URL for Telegram to get a file from the Internet.', + }, + { + displayName: 'Reply Markup', + name: 'replyMarkup', + displayOptions: { + show: { + operation: [ + 'sendAnimation', + 'sendDocument', + 'sendMessage', + 'sendPhoto', + 'sendSticker', + 'sendVideo', + 'sendAudio', + 'sendLocation', + ], + resource: ['message'], + }, + }, + type: 'options', + options: [ + { name: 'Force Reply', value: 'forceReply' }, + { name: 'Inline Keyboard', value: 'inlineKeyboard' }, + { name: 'None', value: 'none' }, + { name: 'Reply Keyboard', value: 'replyKeyboard' }, + { name: 'Reply Keyboard Remove', value: 'replyKeyboardRemove' }, + ], + default: 'none', + description: 'Additional interface options', + }, + { + displayName: 'Force Reply', + name: 'forceReply', + type: 'collection', + placeholder: 'Add Field', + displayOptions: { show: { replyMarkup: ['forceReply'], resource: ['message'] } }, + default: {}, + options: [ + { + displayName: 'Force Reply', + name: 'force_reply', + type: 'boolean', + default: false, + description: + 'Whether to show reply interface to the user, as if they manually selected the bot‘s message and tapped ’Reply', + }, + { + displayName: 'Selective', + name: 'selective', + type: 'boolean', + default: false, + description: 'Whether to force reply from specific users only', + }, + ], + }, + { + displayName: 'Inline Keyboard', + name: 'inlineKeyboard', + placeholder: 'Add Keyboard Row', + description: 'Adds an inline keyboard that appears right next to the message it belongs to', + type: 'fixedCollection', + typeOptions: { multipleValues: true }, + displayOptions: { show: { replyMarkup: ['inlineKeyboard'], resource: ['message'] } }, + default: {}, + options: [ + { + displayName: 'Rows', + name: 'rows', + values: [ + { + displayName: 'Row', + name: 'row', + type: 'fixedCollection', + description: 'The value to set', + placeholder: 'Add Button', + typeOptions: { multipleValues: true }, + default: {}, + options: [ + { + displayName: 'Buttons', + name: 'buttons', + values: [ + { + displayName: 'Text', + name: 'text', + type: 'string', + default: '', + description: 'Label text on the button', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + options: [ + { + displayName: 'Callback Data', + name: 'callback_data', + type: 'string', + default: '', + description: + 'Data to be sent in a callback query to the bot when button is pressed, 1-64 bytes', + }, + { + displayName: 'Pay', + name: 'pay', + type: 'boolean', + default: false, + description: 'Whether to send a Pay button', + }, + { + displayName: 'Switch Inline Query Current Chat', + name: 'switch_inline_query_current_chat', + type: 'string', + default: '', + description: + "If set, pressing the button will insert the bot‘s username and the specified inline query in the current chat's input field.Can be empty, in which case only the bot’s username will be inserted", + }, + { + displayName: 'Switch Inline Query', + name: 'switch_inline_query', + type: 'string', + default: '', + description: + 'If set, pressing the button will prompt the user to select one of their chats, open that chat and insert the bot‘s username and the specified inline query in the input field. Can be empty, in which case just the bot’s username will be inserted.', + }, + { + displayName: 'URL', + name: 'url', + type: 'string', + default: '', + description: 'HTTP or tg:// URL to be opened when button is pressed', + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], + }, + { + displayName: 'Reply Keyboard', + name: 'replyKeyboard', + placeholder: 'Add Reply Keyboard Row', + description: 'Adds a custom keyboard with reply options', + type: 'fixedCollection', + typeOptions: { multipleValues: true }, + displayOptions: { show: { replyMarkup: ['replyKeyboard'] } }, + default: {}, + options: [ + { + displayName: 'Rows', + name: 'rows', + values: [ + { + displayName: 'Row', + name: 'row', + type: 'fixedCollection', + description: 'The value to set', + placeholder: 'Add Button', + typeOptions: { multipleValues: true }, + default: {}, + options: [ + { + displayName: 'Buttons', + name: 'buttons', + values: [ + { + displayName: 'Text', + name: 'text', + type: 'string', + default: '', + description: + 'Text of the button. If none of the optional fields are used, it will be sent as a message when the button is pressed.', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + options: [ + { + displayName: 'Request Contact', + name: 'request_contact', + type: 'boolean', + default: false, + description: + "Whether the user's phone number will be sent as a contact when the button is pressed.Available in private chats only", + }, + { + displayName: 'Request Location', + name: 'request_location', + type: 'boolean', + default: false, + description: "Whether the user's request_location", + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], + }, + { + displayName: 'Reply Keyboard Options', + name: 'replyKeyboardOptions', + type: 'collection', + placeholder: 'Add Option', + displayOptions: { show: { replyMarkup: ['replyKeyboard'] } }, + default: {}, + options: [ + { + displayName: 'Resize Keyboard', + name: 'resize_keyboard', + type: 'boolean', + default: false, + description: + 'Whether to request clients to resize the keyboard vertically for optimal fit', + }, + { + displayName: 'One Time Keyboard', + name: 'one_time_keyboard', + type: 'boolean', + default: false, + description: "Whether to request clients to hide the keyboard as soon as it's been used", + }, + { + displayName: 'Selective', + name: 'selective', + type: 'boolean', + default: false, + description: 'Whether to show the keyboard to specific users only', + }, + ], + }, + { + displayName: 'Reply Keyboard Remove', + name: 'replyKeyboardRemove', + type: 'collection', + placeholder: 'Add Field', + displayOptions: { show: { replyMarkup: ['replyKeyboardRemove'] } }, + default: {}, + options: [ + { + displayName: 'Remove Keyboard', + name: 'remove_keyboard', + type: 'boolean', + default: false, + description: 'Whether to request clients to remove the custom keyboard', + }, + { + displayName: 'Selective', + name: 'selective', + type: 'boolean', + default: false, + description: 'Whether to force reply from specific users only', + }, + ], + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + displayOptions: { + show: { + operation: [ + 'editMessageText', + 'sendAnimation', + 'sendAudio', + 'sendDocument', + 'sendLocation', + 'sendMessage', + 'sendMediaGroup', + 'sendPhoto', + 'sendSticker', + 'sendVideo', + ], + resource: ['message'], + }, + }, + default: {}, + options: [ + { + displayName: 'Append n8n Attribution', + name: 'appendAttribution', + type: 'boolean', + default: true, + description: + 'Whether to include the phrase “This message was sent automatically with n8n” to the end of the message', + displayOptions: { show: { '/operation': ['sendMessage'] } }, + }, + { + displayName: 'Caption', + name: 'caption', + type: 'string', + displayOptions: { + show: { + '/operation': [ + 'sendAnimation', + 'sendAudio', + 'sendDocument', + 'sendPhoto', + 'sendVideo', + ], + }, + }, + default: '', + description: 'Caption text to set, 0-1024 characters', + }, + { + displayName: 'Disable Notification', + name: 'disable_notification', + type: 'boolean', + default: false, + displayOptions: { hide: { '/operation': ['editMessageText'] } }, + description: + 'Whether to send the message silently. Users will receive a notification with no sound.', + }, + { + displayName: 'Disable WebPage Preview', + name: 'disable_web_page_preview', + type: 'boolean', + displayOptions: { show: { '/operation': ['editMessageText', 'sendMessage'] } }, + default: false, + description: 'Whether to disable link previews for links in this message', + }, + { + displayName: 'Duration', + name: 'duration', + type: 'number', + typeOptions: { minValue: 0 }, + displayOptions: { show: { '/operation': ['sendAnimation', 'sendAudio', 'sendVideo'] } }, + default: 0, + description: 'Duration of clip in seconds', + }, + { + displayName: 'File Name', + name: 'fileName', + type: 'string', + default: '', + displayOptions: { + show: { + '/operation': [ + 'sendAnimation', + 'sendAudio', + 'sendDocument', + 'sendPhoto', + 'sendVideo', + 'sendSticker', + ], + '/resource': ['message'], + '/binaryData': [true], + }, + }, + placeholder: 'image.jpeg', + }, + { + displayName: 'Height', + name: 'height', + type: 'number', + typeOptions: { minValue: 0 }, + displayOptions: { show: { '/operation': ['sendAnimation', 'sendVideo'] } }, + default: 0, + description: 'Height of the video', + }, + { + displayName: 'Parse Mode', + name: 'parse_mode', + type: 'options', + options: [ + { name: 'Markdown (Legacy)', value: 'Markdown' }, + { name: 'MarkdownV2', value: 'MarkdownV2' }, + { name: 'HTML', value: 'HTML' }, + ], + displayOptions: { + show: { + '/operation': [ + 'editMessageText', + 'sendAnimation', + 'sendAudio', + 'sendMessage', + 'sendPhoto', + 'sendVideo', + 'sendDocument', + ], + }, + }, + default: 'HTML', + description: 'How to parse the text', + }, + { + displayName: 'Performer', + name: 'performer', + type: 'string', + displayOptions: { show: { '/operation': ['sendAudio'] } }, + default: '', + description: 'Name of the performer', + }, + { + displayName: 'Reply To Message ID', + name: 'reply_to_message_id', + type: 'number', + displayOptions: { hide: { '/operation': ['editMessageText'] } }, + default: 0, + description: 'If the message is a reply, ID of the original message', + }, + { + displayName: 'Message Thread ID', + name: 'message_thread_id', + type: 'number', + displayOptions: { + show: { + '/operation': [ + 'sendAnimation', + 'sendAudio', + 'sendChatAction', + 'sendDocument', + 'sendLocation', + 'sendMediaGroup', + 'sendMessage', + 'sendPhoto', + 'sendSticker', + 'sendVideo', + ], + }, + }, + default: 0, + description: 'The unique identifier of the forum topic', + }, + { + displayName: 'Title', + name: 'title', + type: 'string', + displayOptions: { show: { '/operation': ['sendAudio'] } }, + default: '', + description: 'Title of the track', + }, + { + displayName: 'Thumbnail', + name: 'thumb', + type: 'string', + displayOptions: { + show: { '/operation': ['sendAnimation', 'sendAudio', 'sendDocument', 'sendVideo'] }, + }, + default: '', + description: + 'Thumbnail of the file sent; can be ignored if thumbnail generation for the file is supported server-side. The thumbnail should be in JPEG format and less than 200 kB in size. A thumbnail‘s width and height should not exceed 320.', + }, + { + displayName: 'Width', + name: 'width', + type: 'number', + typeOptions: { minValue: 0 }, + displayOptions: { show: { '/operation': ['sendAnimation', 'sendVideo'] } }, + default: 0, + description: 'Width of the video', + }, + ], + }, + ], + codex: { + categories: ['Communication'], + resources: { + primaryDocumentation: [ + { url: 'https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.telegram/' }, + ], + credentialDocumentation: [{ url: 'https://docs.n8n.io/credentials/telegram' }], + }, + }, + iconUrl: 'icons/n8n-nodes-base/dist/nodes/Telegram/telegram.svg', +} satisfies INodeTypeDescription; + +export const nodeTypeShopifyTriggerV1 = { + displayName: 'Shopify Trigger', + name: 'n8n-nodes-base.shopifyTrigger', + group: ['trigger'], + version: 1, + subtitle: '={{$parameter["event"]}}', + description: 'Handle Shopify events via webhooks', + defaults: { name: 'Shopify Trigger' }, + inputs: [], + outputs: ['main'], + credentials: [ + { + name: 'shopifyApi', + required: true, + displayOptions: { show: { authentication: ['apiKey'] } }, + }, + { + name: 'shopifyAccessTokenApi', + required: true, + displayOptions: { show: { authentication: ['accessToken'] } }, + }, + { + name: 'shopifyOAuth2Api', + required: true, + displayOptions: { show: { authentication: ['oAuth2'] } }, + }, + ], + webhooks: [{ name: 'default', httpMethod: 'POST', responseMode: 'onReceived', path: 'webhook' }], + properties: [ + { + displayName: 'Authentication', + name: 'authentication', + type: 'options', + options: [ + { name: 'Access Token', value: 'accessToken' }, + { name: 'OAuth2', value: 'oAuth2' }, + { name: 'API Key', value: 'apiKey' }, + ], + default: 'apiKey', + }, + { + displayName: 'Trigger On', + name: 'topic', + type: 'options', + default: '', + options: [ + { name: 'App Uninstalled', value: 'app/uninstalled' }, + { name: 'Cart Created', value: 'carts/create' }, + { name: 'Cart Updated', value: 'carts/update' }, + { name: 'Checkout Created', value: 'checkouts/create' }, + { name: 'Checkout Delete', value: 'checkouts/delete' }, + { name: 'Checkout Update', value: 'checkouts/update' }, + { name: 'Collection Created', value: 'collections/create' }, + { name: 'Collection Deleted', value: 'collections/delete' }, + { name: 'Collection Listings Added', value: 'collection_listings/add' }, + { name: 'Collection Listings Removed', value: 'collection_listings/remove' }, + { name: 'Collection Listings Updated', value: 'collection_listings/update' }, + { name: 'Collection Updated', value: 'collections/update' }, + { name: 'Customer Created', value: 'customers/create' }, + { name: 'Customer Deleted', value: 'customers/delete' }, + { name: 'Customer Disabled', value: 'customers/disable' }, + { name: 'Customer Enabled', value: 'customers/enable' }, + { name: 'Customer Groups Created', value: 'customer_groups/create' }, + { name: 'Customer Groups Deleted', value: 'customer_groups/delete' }, + { name: 'Customer Groups Updated', value: 'customer_groups/update' }, + { name: 'Customer Updated', value: 'customers/update' }, + { name: 'Draft Orders Created', value: 'draft_orders/create' }, + { name: 'Draft Orders Deleted', value: 'draft_orders/delete' }, + { name: 'Draft Orders Updated', value: 'draft_orders/update' }, + { name: 'Fulfillment Created', value: 'fulfillments/create' }, + { name: 'Fulfillment Events Created', value: 'fulfillment_events/create' }, + { name: 'Fulfillment Events Deleted', value: 'fulfillment_events/delete' }, + { name: 'Fulfillment Updated', value: 'fulfillments/update' }, + { name: 'Inventory Items Created', value: 'inventory_items/create' }, + { name: 'Inventory Items Deleted', value: 'inventory_items/delete' }, + { name: 'Inventory Items Updated', value: 'inventory_items/update' }, + { name: 'Inventory Levels Connected', value: 'inventory_levels/connect' }, + { name: 'Inventory Levels Disconnected', value: 'inventory_levels/disconnect' }, + { name: 'Inventory Levels Updated', value: 'inventory_levels/update' }, + { name: 'Locale Created', value: 'locales/create' }, + { name: 'Locale Updated', value: 'locales/update' }, + { name: 'Location Created', value: 'locations/create' }, + { name: 'Location Deleted', value: 'locations/delete' }, + { name: 'Location Updated', value: 'locations/update' }, + { name: 'Order Cancelled', value: 'orders/cancelled' }, + { name: 'Order Created', value: 'orders/create' }, + { name: 'Order Fulfilled', value: 'orders/fulfilled' }, + { name: 'Order Paid', value: 'orders/paid' }, + { name: 'Order Partially Fulfilled', value: 'orders/partially_fulfilled' }, + { name: 'Order Transactions Created', value: 'order_transactions/create' }, + { name: 'Order Updated', value: 'orders/updated' }, + { name: 'Orders Deleted', value: 'orders/delete' }, + { name: 'Product Created', value: 'products/create' }, + { name: 'Product Deleted', value: 'products/delete' }, + { name: 'Product Listings Added', value: 'product_listings/add' }, + { name: 'Product Listings Removed', value: 'product_listings/remove' }, + { name: 'Product Listings Updated', value: 'product_listings/update' }, + { name: 'Product Updated', value: 'products/update' }, + { name: 'Refund Created', value: 'refunds/create' }, + { name: 'Shop Updated', value: 'shop/update' }, + { name: 'Tender Transactions Created', value: 'tender_transactions/create' }, + { name: 'Theme Created', value: 'themes/create' }, + { name: 'Theme Deleted', value: 'themes/delete' }, + { name: 'Theme Published', value: 'themes/publish' }, + { name: 'Theme Updated', value: 'themes/update' }, + ], + }, + ], + codex: { + categories: ['Sales'], + resources: { + primaryDocumentation: [ + { + url: 'https://docs.n8n.io/integrations/builtin/trigger-nodes/n8n-nodes-base.shopifytrigger/', + }, + ], + credentialDocumentation: [{ url: 'https://docs.n8n.io/credentials/shopify' }], + }, + }, + iconUrl: 'icons/n8n-nodes-base/dist/nodes/Shopify/shopify.svg', +} satisfies INodeTypeDescription; + +export const nodeTypeHttpRequestV1 = { + displayName: 'HTTP Request', + name: 'n8n-nodes-base.httpRequest', + group: ['output'], + subtitle: '={{$parameter["requestMethod"] + ": " + $parameter["url"]}}', + description: 'Makes an HTTP request and returns the response data', + defaultVersion: 4.1, + version: 1, + defaults: { name: 'HTTP Request', color: '#2200DD' }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'httpBasicAuth', + required: true, + displayOptions: { show: { authentication: ['basicAuth'] } }, + }, + { + name: 'httpDigestAuth', + required: true, + displayOptions: { show: { authentication: ['digestAuth'] } }, + }, + { + name: 'httpHeaderAuth', + required: true, + displayOptions: { show: { authentication: ['headerAuth'] } }, + }, + { + name: 'httpQueryAuth', + required: true, + displayOptions: { show: { authentication: ['queryAuth'] } }, + }, + { + name: 'oAuth1Api', + required: true, + displayOptions: { show: { authentication: ['oAuth1'] } }, + }, + { + name: 'oAuth2Api', + required: true, + displayOptions: { show: { authentication: ['oAuth2'] } }, + }, + ], + properties: [ + { + displayName: 'Authentication', + name: 'authentication', + type: 'options', + options: [ + { name: 'Basic Auth', value: 'basicAuth' }, + { name: 'Digest Auth', value: 'digestAuth' }, + { name: 'Header Auth', value: 'headerAuth' }, + { name: 'None', value: 'none' }, + { name: 'OAuth1', value: 'oAuth1' }, + { name: 'OAuth2', value: 'oAuth2' }, + { name: 'Query Auth', value: 'queryAuth' }, + ], + default: 'none', + description: 'The way to authenticate', + }, + { + displayName: 'Request Method', + name: 'requestMethod', + type: 'options', + options: [ + { name: 'DELETE', value: 'DELETE' }, + { name: 'GET', value: 'GET' }, + { name: 'HEAD', value: 'HEAD' }, + { name: 'OPTIONS', value: 'OPTIONS' }, + { name: 'PATCH', value: 'PATCH' }, + { name: 'POST', value: 'POST' }, + { name: 'PUT', value: 'PUT' }, + ], + default: 'GET', + description: 'The request method to use', + }, + { + displayName: 'URL', + name: 'url', + type: 'string', + default: '', + placeholder: 'http://example.com/index.html', + description: 'The URL to make the request to', + required: true, + }, + { + displayName: 'Ignore SSL Issues', + name: 'allowUnauthorizedCerts', + type: 'boolean', + default: false, + description: + 'Whether to download the response even if SSL certificate validation is not possible', + }, + { + displayName: 'Response Format', + name: 'responseFormat', + type: 'options', + options: [ + { name: 'File', value: 'file' }, + { name: 'JSON', value: 'json' }, + { name: 'String', value: 'string' }, + ], + default: 'json', + description: 'The format in which the data gets returned from the URL', + }, + { + displayName: 'Property Name', + name: 'dataPropertyName', + type: 'string', + default: 'data', + required: true, + displayOptions: { show: { responseFormat: ['string'] } }, + description: 'Name of the property to which to write the response data', + }, + { + displayName: 'Binary Property', + name: 'dataPropertyName', + type: 'string', + default: 'data', + required: true, + displayOptions: { show: { responseFormat: ['file'] } }, + description: 'Name of the binary property to which to write the data of the read file', + }, + { + displayName: 'JSON/RAW Parameters', + name: 'jsonParameters', + type: 'boolean', + default: false, + description: + 'Whether the query and/or body parameter should be set via the value-key pair UI or JSON/RAW', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + options: [ + { + displayName: 'Batch Interval', + name: 'batchInterval', + type: 'number', + typeOptions: { minValue: 0 }, + default: 1000, + description: 'Time (in milliseconds) between each batch of requests. 0 for disabled.', + }, + { + displayName: 'Batch Size', + name: 'batchSize', + type: 'number', + typeOptions: { minValue: -1 }, + default: 50, + description: + 'Input will be split in batches to throttle requests. -1 for disabled. 0 will be treated as 1.', + }, + { + displayName: 'Body Content Type', + name: 'bodyContentType', + type: 'options', + displayOptions: { show: { '/requestMethod': ['PATCH', 'POST', 'PUT'] } }, + options: [ + { name: 'JSON', value: 'json' }, + { name: 'RAW/Custom', value: 'raw' }, + { name: 'Form-Data Multipart', value: 'multipart-form-data' }, + { name: 'Form Urlencoded', value: 'form-urlencoded' }, + ], + default: 'json', + description: 'Content-Type to use to send body parameters', + }, + { + displayName: 'Full Response', + name: 'fullResponse', + type: 'boolean', + default: false, + description: 'Whether to return the full response data instead of only the body', + }, + { + displayName: 'Follow All Redirects', + name: 'followAllRedirects', + type: 'boolean', + default: false, + description: 'Whether to follow non-GET HTTP 3xx redirects', + }, + { + displayName: 'Follow GET Redirect', + name: 'followRedirect', + type: 'boolean', + default: true, + description: 'Whether to follow GET HTTP 3xx redirects', + }, + { + displayName: 'Ignore Response Code', + name: 'ignoreResponseCode', + type: 'boolean', + default: false, + description: 'Whether to succeeds also when status code is not 2xx', + }, + { + displayName: 'MIME Type', + name: 'bodyContentCustomMimeType', + type: 'string', + default: '', + placeholder: 'text/xml', + description: 'Specify the mime type for raw/custom body type', + displayOptions: { show: { '/requestMethod': ['PATCH', 'POST', 'PUT'] } }, + }, + { + displayName: 'Proxy', + name: 'proxy', + type: 'string', + default: '', + placeholder: 'http://myproxy:3128', + description: 'HTTP proxy to use', + }, + { + displayName: 'Split Into Items', + name: 'splitIntoItems', + type: 'boolean', + default: false, + description: 'Whether to output each element of an array as own item', + displayOptions: { show: { '/responseFormat': ['json'] } }, + }, + { + displayName: 'Timeout', + name: 'timeout', + type: 'number', + typeOptions: { minValue: 1 }, + default: 10000, + description: + 'Time in ms to wait for the server to send response headers (and start the response body) before aborting the request', + }, + { + displayName: 'Use Querystring', + name: 'useQueryString', + type: 'boolean', + default: false, + description: + 'Whether you need arrays to be serialized as foo=bar&foo=baz instead of the default foo[0]=bar&foo[1]=baz', + }, + ], + }, + { + displayName: 'Send Binary Data', + name: 'sendBinaryData', + type: 'boolean', + displayOptions: { + show: { jsonParameters: [true], requestMethod: ['PATCH', 'POST', 'PUT'] }, + }, + default: false, + description: 'Whether binary data should be send as body', + }, + { + displayName: 'Binary Property', + name: 'binaryPropertyName', + type: 'string', + required: true, + default: 'data', + displayOptions: { + hide: { sendBinaryData: [false] }, + show: { jsonParameters: [true], requestMethod: ['PATCH', 'POST', 'PUT'] }, + }, + description: + 'Name of the binary property which contains the data for the file to be uploaded. For Form-Data Multipart, they can be provided in the format: "sendKey1:binaryProperty1,sendKey2:binaryProperty2', + }, + { + displayName: 'Body Parameters', + name: 'bodyParametersJson', + type: 'json', + displayOptions: { + hide: { sendBinaryData: [true] }, + show: { jsonParameters: [true], requestMethod: ['PATCH', 'POST', 'PUT', 'DELETE'] }, + }, + default: '', + description: 'Body parameters as JSON or RAW', + }, + { + displayName: 'Body Parameters', + name: 'bodyParametersUi', + placeholder: 'Add Parameter', + type: 'fixedCollection', + typeOptions: { multipleValues: true }, + displayOptions: { + show: { jsonParameters: [false], requestMethod: ['PATCH', 'POST', 'PUT', 'DELETE'] }, + }, + description: 'The body parameter to send', + default: {}, + options: [ + { + name: 'parameter', + displayName: 'Parameter', + values: [ + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + description: 'Name of the parameter', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + description: 'Value of the parameter', + }, + ], + }, + ], + }, + { + displayName: 'Headers', + name: 'headerParametersJson', + type: 'json', + displayOptions: { show: { jsonParameters: [true] } }, + default: '', + description: 'Header parameters as JSON or RAW', + }, + { + displayName: 'Headers', + name: 'headerParametersUi', + placeholder: 'Add Header', + type: 'fixedCollection', + typeOptions: { multipleValues: true }, + displayOptions: { show: { jsonParameters: [false] } }, + description: 'The headers to send', + default: {}, + options: [ + { + name: 'parameter', + displayName: 'Header', + values: [ + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + description: 'Name of the header', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + description: 'Value to set for the header', + }, + ], + }, + ], + }, + { + displayName: 'Query Parameters', + name: 'queryParametersJson', + type: 'json', + displayOptions: { show: { jsonParameters: [true] } }, + default: '', + description: 'Query parameters as JSON (flat object)', + }, + { + displayName: 'Query Parameters', + name: 'queryParametersUi', + placeholder: 'Add Parameter', + type: 'fixedCollection', + typeOptions: { multipleValues: true }, + displayOptions: { show: { jsonParameters: [false] } }, + description: 'The query parameter to send', + default: {}, + options: [ + { + name: 'parameter', + displayName: 'Parameter', + values: [ + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + description: 'Name of the parameter', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + description: 'Value of the parameter', + }, + ], + }, + ], + }, + { + displayName: + "You can view the raw requests this node makes in your browser's developer console", + name: 'infoMessage', + type: 'notice', + default: '', + }, + ], + iconUrl: 'icons/n8n-nodes-base/dist/nodes/HttpRequest/httprequest.svg', + codex: { + categories: ['Development', 'Core Nodes'], + subcategories: { 'Core Nodes': ['Helpers'] }, + resources: { + primaryDocumentation: [ + { + url: 'https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.httprequest/', + }, + ], + }, + alias: ['API', 'Request', 'URL', 'Build', 'cURL'], + }, +} satisfies INodeTypeDescription; diff --git a/packages/editor-ui/src/utils/testData/templateTestData.ts b/packages/editor-ui/src/utils/testData/templateTestData.ts index 92325e4602..4243d1871b 100644 --- a/packages/editor-ui/src/utils/testData/templateTestData.ts +++ b/packages/editor-ui/src/utils/testData/templateTestData.ts @@ -1,6 +1,6 @@ // eslint-disable-next-line import/no-extraneous-dependencies import { faker } from '@faker-js/faker/locale/en'; -import type { IWorkflowTemplateNode } from '@/Interface'; +import type { ITemplatesWorkflowFull, IWorkflowTemplateNode } from '@/Interface'; export const newWorkflowTemplateNode = ({ type, @@ -14,3 +14,279 @@ export const newWorkflowTemplateNode = ({ typeVersion: 1, ...optionalOpts, }); + +export const fullShopifyTelegramTwitterTemplate = { + full: true, + id: 1205, + name: 'Promote new Shopify products on Twitter and Telegram', + totalViews: 485, + createdAt: '2021-08-24T10:40:50.007Z', + description: + 'This workflow automatically promotes your new Shopify products on Twitter and Telegram. This workflow is also featured in the blog post [*6 e-commerce workflows to power up your Shopify store*](https://n8n.io/blog/no-code-ecommerce-workflow-automations/#promote-your-new-products-on-social-media).\n\n## Prerequisites\n\n- A Shopify account and [credentials](https://docs.n8n.io/integrations/credentials/shopify/)\n- A Twitter account and [credentials](https://docs.n8n.io/integrations/credentials/twitter/)\n- A Telegram account and [credentials](https://docs.n8n.io/integrations/credentials/telegram/) for the channel you want to send messages to.\n\n## Nodes\n\n- [Shopify Trigger node](https://docs.n8n.io/integrations/trigger-nodes/n8n-nodes-base.shopifytrigger/) triggers the workflow when you create a new product in Shopify.\n- [Twitter node](https://docs.n8n.io/integrations/nodes/n8n-nodes-base.twitter/) posts a tweet with the text "Hey there, my design is now on a new product! Visit my {shop name} to get this cool {product title} (and check out more {product type})".\n- [Telegram node](https://docs.n8n.io/integrations/nodes/n8n-nodes-base.telegram/) posts a message with the same text as above in a Telegram channel.', + workflow: { + nodes: [ + { + name: 'Twitter', + type: 'n8n-nodes-base.twitter', + position: [720, -220], + parameters: { + text: '=Hey there, my design is now on a new product ✨\nVisit my {{$json["vendor"]}} shop to get this cool{{$json["title"]}} (and check out more {{$json["product_type"]}}) 🛍️', + additionalFields: {}, + }, + credentials: { + twitterOAuth1Api: 'twitter', + }, + typeVersion: 1, + }, + { + name: 'Telegram', + type: 'n8n-nodes-base.telegram', + position: [720, -20], + parameters: { + text: '=Hey there, my design is now on a new product!\nVisit my {{$json["vendor"]}} shop to get this cool{{$json["title"]}} (and check out more {{$json["product_type"]}})', + chatId: '123456', + additionalFields: {}, + }, + credentials: { + telegramApi: 'telegram_habot', + }, + typeVersion: 1, + }, + { + name: 'product created', + type: 'n8n-nodes-base.shopifyTrigger', + position: [540, -110], + webhookId: '2a7e0e50-8f09-4a2b-bf54-a849a6ac4fe0', + parameters: { + topic: 'products/create', + }, + credentials: { + shopifyApi: 'shopify_nodeqa', + }, + typeVersion: 1, + }, + ], + connections: { + 'product created': { + main: [ + [ + { + node: 'Twitter', + type: 'main', + index: 0, + }, + { + node: 'Telegram', + type: 'main', + index: 0, + }, + ], + ], + }, + }, + }, + workflowInfo: { + nodeCount: 3, + nodeTypes: { + 'n8n-nodes-base.twitter': { + count: 1, + }, + 'n8n-nodes-base.telegram': { + count: 1, + }, + 'n8n-nodes-base.shopifyTrigger': { + count: 1, + }, + }, + }, + user: { + username: 'lorenanda', + }, + nodes: [ + { + id: 49, + icon: 'file:telegram.svg', + name: 'n8n-nodes-base.telegram', + defaults: { + name: 'Telegram', + }, + iconData: { + type: 'file', + fileBuffer: + 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSIwIDAgNjYgNjYiIGZpbGw9IiNmZmYiIGZpbGwtcnVsZT0iZXZlbm9kZCIgc3Ryb2tlPSIjMDAwIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjx1c2UgeGxpbms6aHJlZj0iI2EiIHg9Ii41IiB5PSIuNSIvPjxzeW1ib2wgaWQ9ImEiIG92ZXJmbG93PSJ2aXNpYmxlIj48ZyBzdHJva2U9Im5vbmUiIGZpbGwtcnVsZT0ibm9uemVybyI+PHBhdGggZD0iTTAgMzJjMCAxNy42NzMgMTQuMzI3IDMyIDMyIDMyczMyLTE0LjMyNyAzMi0zMlM0OS42NzMgMCAzMiAwIDAgMTQuMzI3IDAgMzIiIGZpbGw9IiMzN2FlZTIiLz48cGF0aCBkPSJNMjEuNjYxIDM0LjMzOGwzLjc5NyAxMC41MDhzLjQ3NS45ODMuOTgzLjk4MyA4LjA2OC03Ljg2NCA4LjA2OC03Ljg2NGw4LjQwNy0xNi4yMzctMjEuMTE5IDkuODk4eiIgZmlsbD0iI2M4ZGFlYSIvPjxwYXRoIGQ9Ik0yNi42OTUgMzcuMDM0bC0uNzI5IDcuNzQ2cy0uMzA1IDIuMzczIDIuMDY4IDBsNC42NDQtNC4yMDMiIGZpbGw9IiNhOWM2ZDgiLz48cGF0aCBkPSJNMjEuNzMgMzQuNzEybC03LjgwOS0yLjU0NXMtLjkzMi0uMzc4LS42MzMtMS4yMzdjLjA2Mi0uMTc3LjE4Ni0uMzI4LjU1OS0uNTg4IDEuNzMxLTEuMjA2IDMyLjAyOC0xMi4wOTYgMzIuMDI4LTEyLjA5NnMuODU2LS4yODggMS4zNjEtLjA5N2MuMjMxLjA4OC4zNzguMTg3LjUwMy41NDguMDQ1LjEzMi4wNzEuNDExLjA2OC42ODktLjAwMy4yMDEtLjAyNy4zODYtLjA0NS42NzgtLjE4NCAyLjk3OC01LjcwNiAyNS4xOTgtNS43MDYgMjUuMTk4cy0uMzMgMS4zLTEuNTE0IDEuMzQ1Yy0uNDMyLjAxNi0uOTU2LS4wNzEtMS41ODItLjYxLTIuMzIzLTEuOTk4LTEwLjM1Mi03LjM5NC0xMi4xMjYtOC41OGEuMzQuMzQgMCAwMS0uMTQ2LS4yMzljLS4wMjUtLjEyNS4xMDgtLjI4LjEwOC0uMjhzMTMuOTgtMTIuNDI3IDE0LjM1Mi0xMy43MzFjLjAyOS0uMTAxLS4wNzktLjE1MS0uMjI2LS4xMDctLjkyOS4zNDItMTcuMDI1IDEwLjUwNi0xOC44MDEgMTEuNjI5LS4xMDQuMDY2LS4zOTUuMDIzLS4zOTUuMDIzIi8+PC9nPjwvc3ltYm9sPjwvc3ZnPg==', + }, + categories: [ + { + id: 6, + name: 'Communication', + }, + ], + displayName: 'Telegram', + typeVersion: 1, + }, + { + id: 107, + icon: 'file:shopify.svg', + name: 'n8n-nodes-base.shopifyTrigger', + defaults: { + name: 'Shopify Trigger', + }, + iconData: { + type: 'file', + fileBuffer: + 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSIwIDAgNTggNjYiIGZpbGw9IiNmZmYiIGZpbGwtcnVsZT0iZXZlbm9kZCIgc3Ryb2tlPSIjMDAwIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjx1c2UgeGxpbms6aHJlZj0iI2EiIHg9Ii41IiB5PSIuNSIvPjxzeW1ib2wgaWQ9ImEiIG92ZXJmbG93PSJ2aXNpYmxlIj48ZyBzdHJva2U9Im5vbmUiIGZpbGwtcnVsZT0ibm9uemVybyI+PHBhdGggZD0iTTQ5LjI1NSAxMi40ODRhLjYzMy42MzMgMCAwMC0uNTY0LS41MjdjLS4yMjUtLjAzNy01LjE3LS4zNzYtNS4xNy0uMzc2bC0zLjc3LTMuNzdjLS4zNC0uMzc2LTEuMDkyLS4yNjYtMS4zNzYtLjE4OC0uMDM3IDAtLjc1Mi4yMjUtMS45MjIuNjA1LTEuMTM3LTMuMy0zLjE1LTYuMzA2LTYuNjk2LTYuMzA2aC0uMzAzQzI4LjQzOC42MDUgMjcuMTk0IDAgMjYuMTQ0IDBjLTguMjU2LjAzNy0xMi4yIDEwLjMzMy0xMy40MzQgMTUuNTk0bC01Ljc3IDEuNzdjLTEuNzcuNTY0LTEuODM1LjYwNS0yLjA3MyAyLjI5M0wwIDU3LjE3NSAzNi40NjggNjRsMTkuNzYzLTQuMjZjMC0uMDM3LTYuOTQtNDYuODk3LTYuOTc2LTQ3LjI1NXpNMzQuNDMxIDguODZjLS45MTcuMzAzLTEuOTYzLjYwNS0zLjEuOTQ1di0uNjhhMTUuMDMgMTUuMDMgMCAwMC0uNzUyLTQuOTk5YzEuODQ4LjI4NCAzLjEgMi4zNTcgMy44NDMgNC43MzN6bS02LjA2OC00LjI5OGMuNjAzIDEuNzc4Ljg4MyAzLjY1LjgyNiA1LjUyN3YuMzRsLTYuMzc1IDEuOTYzYzEuMjQ4LTQuNjYgMy41NS02Ljk2MiA1LjU1LTcuODN6bS0yLjQ1LTIuMjkzYTEuOTQgMS45NCAwIDAxMS4wNTUuMzM5Yy0yLjY2IDEuMjM4LTUuNDcyIDQuMzY2LTYuNjc4IDEwLjYyN2wtNS4wNDUgMS41NDZDMTYuNjY4IDEwLjAzIDE5Ljk4OCAyLjI2IDI1LjkxIDIuMjZ6IiBmaWxsPSIjOTViZjQ3Ii8+PHBhdGggZD0iTTQ4LjY5MSAxMS45NTdjLS4yMjUtLjAzNy01LjE3LS4zNzYtNS4xNy0uMzc2bC0zLjc3LTMuNzdhLjc1My43NTMgMCAwMC0uNTI3LS4yMjVMMzYuNDcyIDY0bDE5Ljc2My00LjI2LTYuOTgtNDcuMjE4YS42OC42OCAwIDAwLS41NjQtLjU2NHoiIGZpbGw9IiM1ZThlM2UiLz48cGF0aCBkPSJNMjkuNzU4IDIyLjlsLTIuNDU0IDcuMjQyYTExLjM2IDExLjM2IDAgMDAtNC43NTItMS4xMzNjLTMuODQ4IDAtNC4wMzYgMi40MTItNC4wMzYgMy4wMTggMCAzLjI5OCA4LjYzNiA0LjU2NCA4LjYzNiAxMi4zMzMgMCA2LjEtMy44ODUgMTAuMDMtOS4xIDEwLjAzLTYuMjYgMC05LjQ2Ny0zLjg4NS05LjQ2Ny0zLjg4NWwxLjY2NS01LjU1czMuMjggMi44MyA2LjA3MyAyLjgzYTIuNDcgMi40NyAwIDAwMi41NjQtMi40OWMwLTQuMzQtNy4xLTQuNTI3LTcuMS0xMS42MTggMC01Ljk2MiA0LjI5OC0xMS43NyAxMi45MzQtMTEuNzcgMy4zOTQuMDUgNS4wMTggMSA1LjAxOCAxeiIvPjwvZz48L3N5bWJvbD48L3N2Zz4=', + }, + categories: [ + { + id: 2, + name: 'Sales', + }, + ], + displayName: 'Shopify Trigger', + typeVersion: 1, + }, + { + id: 325, + icon: 'file:x.svg', + name: 'n8n-nodes-base.twitter', + defaults: { + name: 'X', + }, + iconData: { + type: 'file', + fileBuffer: + 'data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMjQgMjQiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTE4LjI0NCAyLjI1aDMuMzA4bC03LjIyNyA4LjI2IDguNTAyIDExLjI0SDE2LjE3bC01LjIxNC02LjgxN0w0Ljk5IDIxLjc1SDEuNjhsNy43My04LjgzNUwxLjI1NCAyLjI1SDguMDhsNC43MTMgNi4yMzF6bS0xLjE2MSAxNy41MmgxLjgzM0w3LjA4NCA0LjEyNkg1LjExN3oiPjwvcGF0aD48L3N2Zz4K', + }, + categories: [ + { + id: 1, + name: 'Marketing & Content', + }, + ], + displayName: 'X (Formerly Twitter)', + typeVersion: 2, + }, + ], + categories: [ + { + id: 2, + name: 'Sales', + }, + { + id: 19, + name: 'Marketing & Growth', + }, + ], + image: [ + { + id: 527, + url: 'https://n8niostorageaccount.blob.core.windows.net/n8nio-strapi-blobs-prod/assets/89a078b208fe4c6181902608b1cd1332.png', + }, + ], +} satisfies ITemplatesWorkflowFull; + +export const fullSaveEmailAttachmentsToNextCloudTemplate = { + id: 1344, + name: 'Save email attachments to Nextcloud', + totalViews: 649, + createdAt: '2021-11-29T13:59:16.771Z', + description: + 'This workflow will take all emails you put into a certain folder, upload any attachements to Nextcloud, and mark the emails as read (configurable).\n\nAttachements will be saved with automatically generated filenames:\n`2021-01-01_From-Sender-Name_Filename-of-attachement.pdf`\n\nInstructions:\n1. **Allow lodash to be used in n8n** (or rewrite the code...)\n `NODE_FUNCTION_ALLOW_EXTERNAL=lodash` (environment variable)\n2. Import workflow\n3. Set credentials for Email & Nextcloud nodes\n4. Configure to use correct folder / custom filters\n5. Activate\n\nCustom filter examples:\n- Only unread emails:\n `Custom Email Config` = `["UNSEEN"]`\n- Filter emails by \'to\' address:\n `Custom Email Config` = `[["TO", "example+invoices@posteo.de"]]`', + workflow: { + nodes: [ + { + name: 'IMAP Email', + type: 'n8n-nodes-base.emailReadImap', + position: [240, 420], + parameters: { + format: 'resolved', + mailbox: 'Invoices', + options: { customEmailConfig: '["ALL"]' }, + }, + typeVersion: 1, + }, + { + name: 'Nextcloud', + type: 'n8n-nodes-base.nextCloud', + position: [940, 420], + parameters: { + path: '=Documents/Invoices/{{$json["date"]}}_{{$json["from"]}}_{{$binary.file.fileName}}', + binaryDataUpload: true, + binaryPropertyName: 'file', + }, + typeVersion: 1, + }, + { + name: 'Map each attachment', + type: 'n8n-nodes-base.function', + position: [620, 420], + parameters: { + functionCode: + "const _ = require('lodash')\n\nconst sanitize = str => _.chain(str)\n .replace(/[^A-Za-z0-9&.-]/g, '-') // sanitise via whitelist of characters\n .replace(/-(?=-)/g, '') // remove repeated dashes - https://regexr.com/6ag8h\n .trim('-') // trim any leading/trailing dashes\n .truncate({\n length: 60,\n omission: '-' // when the string ends with '-', you'll know it was truncated\n })\n .value()\n\nconst result = _.flatMap(items.map(item => {\n //console.log({item})\n\n // Maps each attachment to a separate item\n return _.values(item.binary).map(file => {\n console.log(\"Saving attachement:\", file.fileName, 'from:', ...item.json.from.value)\n \n // sanitize filename but exclude extension\n const filename_parts = file.fileName.split('.')\n const ext = _.slice(filename_parts, filename_parts.length-1)\n const filename_main = _.join(_.dropRight(filename_parts), '.')\n file.fileName = sanitize(filename_main) + '.' + ext\n \n return {\n json: {\n from: sanitize(item.json.from.value[0].name),\n date: sanitize(new Date(item.json.date).toISOString().split(\"T\")[0]) // get date part \"2020-01-01\"\n }, \n binary: { file }\n }\n })\n}))\n\n//console.log(result)\nreturn result", + }, + typeVersion: 1, + }, + ], + connections: { + 'IMAP Email': { main: [[{ node: 'Map each attachment', type: 'main', index: 0 }]] }, + 'Map each attachment': { main: [[{ node: 'Nextcloud', type: 'main', index: 0 }]] }, + }, + }, + workflowInfo: { + nodeCount: 3, + nodeTypes: { + 'n8n-nodes-base.function': { count: 1 }, + 'n8n-nodes-base.nextCloud': { count: 1 }, + 'n8n-nodes-base.emailReadImap': { count: 1 }, + }, + }, + user: { username: 'tennox' }, + nodes: [ + { + id: 10, + icon: 'fa:inbox', + name: 'n8n-nodes-base.emailReadImap', + defaults: { name: 'Email Trigger (IMAP)', color: '#44AA22' }, + iconData: { icon: 'inbox', type: 'icon' }, + categories: [ + { id: 6, name: 'Communication' }, + { id: 9, name: 'Core Nodes' }, + ], + displayName: 'Email Trigger (IMAP)', + typeVersion: 2, + }, + { + id: 14, + icon: 'fa:code', + name: 'n8n-nodes-base.function', + defaults: { name: 'Function', color: '#FF9922' }, + iconData: { icon: 'code', type: 'icon' }, + categories: [ + { id: 5, name: 'Development' }, + { id: 9, name: 'Core Nodes' }, + ], + displayName: 'Function', + typeVersion: 1, + }, + { + id: 25, + icon: 'file:nextcloud.svg', + name: 'n8n-nodes-base.nextCloud', + defaults: { name: 'Nextcloud' }, + iconData: { + type: 'file', + fileBuffer: + 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSIwIDAgNzYgNTEiIGZpbGw9IiNmZmYiIGZpbGwtcnVsZT0iZXZlbm9kZCIgc3Ryb2tlPSIjMDAwIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjx1c2UgeGxpbms6aHJlZj0iI0EiIHg9Ii41IiB5PSIuNSIvPjxzeW1ib2wgaWQ9IkEiIG92ZXJmbG93PSJ2aXNpYmxlIj48cGF0aCBkPSJNMzcuNTMzIDBjLTcuNzcgMC0xNC4zNTUgNS4yNjgtMTYuMzk2IDEyLjM3OS0xLjc3OC0zLjgxOS01LjU5Ny02LjQ1My0xMC4wNzUtNi40NTNDNS4wMDQgNS45MjYgMCAxMC45MzEgMCAxNy4wNTRhMTEuMTYgMTEuMTYgMCAwIDAgMTEuMTI4IDExLjEyOGM0LjQxMiAwIDguMjk3LTIuNjM0IDEwLjA3NS02LjQ1M2ExNi45OSAxNi45OSAwIDAgMCAxNi4zMyAxMi4zNzljNy43MDQgMCAxNC4yODktNS4yMDIgMTYuMzk2LTEyLjI0OCAxLjc3OCAzLjY4NyA1LjU5NyA2LjI1NiA5Ljk0MyA2LjI1NkExMS4xNiAxMS4xNiAwIDAgMCA3NSAxNi45ODljMC02LjEyNC01LjAwNC0xMS4wNjItMTEuMTI4LTExLjA2Mi00LjM0NiAwLTguMTY1IDIuNTY4LTkuOTQzIDYuMjU2QzUxLjgyMiA1LjIwMiA0NS4zMDMgMCAzNy41MzMgMHptMCA2LjUxOWExMC40OCAxMC40OCAwIDAgMSAxMC41MzUgMTAuNTM2QTEwLjQ4IDEwLjQ4IDAgMCAxIDM3LjUzMyAyNy41OWExMC40OCAxMC40OCAwIDAgMS0xMC41MzYtMTAuNTM1QTEwLjQ4IDEwLjQ4IDAgMCAxIDM3LjUzMyA2LjUxOXptLTI2LjQwNSA1LjkyNmE0LjU4IDQuNTggMCAwIDEgNC42MDkgNC42MDkgNC41OCA0LjU4IDAgMCAxLTQuNjA5IDQuNjA5IDQuNTggNC41OCAwIDAgMS00LjYwOS00LjYwOSA0LjU4IDQuNTggMCAwIDEgNC42MDktNC42MDl6bTUyLjc0NCAwYTQuNTggNC41OCAwIDAgMSA0LjYwOSA0LjYwOSA0LjYwOSA0LjYwOSAwIDEgMS05LjIxOCAwYy4wNjYtMi41NjggMi4wNDEtNC42MDkgNC42MDktNC42MDl6TTE5LjE3NiA0MS45NTdjMS44MjcgMCAyLjg1IDEuMzAxIDIuODUgMy4yNTIgMCAuMTg2LS4xNTUuMzQxLS4zNDEuMzQxSDE2Ljc2Yy4wMzEgMS43MzQgMS4yMzkgMi43MjYgMi42MzMgMi43MjZhMi44OSAyLjg5IDAgMCAwIDEuNzk2LS42MTljLjE4Ni0uMTI0LjM0MS0uMDkzLjQzNC4wOTNsLjA5My4xNTVjLjA5My4xNTUuMDYyLjMxLS4wOTMuNDM0YTMuODQgMy44NCAwIDAgMS0yLjI2MS43NDNjLTIuMDEzIDAtMy41NjItMS40NTYtMy41NjItMy41NjIuMDMxLTIuMjMgMS41MTgtMy41NjIgMy4zNzYtMy41NjJ6bTEuODg5IDIuOTExYy0uMDYyLTEuNDI1LS45MjktMi4xMzctMS45Mi0yLjEzNy0xLjE0NiAwLTIuMTM3Ljc0My0yLjM1NCAyLjEzN2g0LjI3NHptMTAuMjUzLTEuOTJ2LS43NzQtMS42MTFjMC0uMjE3LjEyNC0uMzQxLjM0MS0uMzQxaC4yNDhjLjIxNyAwIC4zMS4xMjQuMzEuMzQxdjEuNjExaDEuMzk0Yy4yMTcgMCAuMzQxLjEyNC4zNDEuMzQxdi4wOTNjMCAuMjE3LS4xMjQuMzEtLjM0MS4zMWgtMS4zOTR2My40MDdjMCAxLjU4Ljk2IDEuNzY2IDEuNDg3IDEuNzk2LjI3OS4wMzEuMzcyLjA5My4zNzIuMzQxdi4xODZjMCAuMjE3LS4wOTMuMzEtLjM3Mi4zMS0xLjQ4NyAwLTIuMzg1LS44OTgtMi4zODUtMi41MDl2LTMuNXptNy4wOTMtLjk5MWMxLjE3NyAwIDEuOTIuNDk2IDIuMjYxLjc3NC4xNTUuMTI0LjE4Ni4yNzkuMDMxLjQ2NWwtLjA5My4xNTVjLS4xMjQuMTg2LS4yNzkuMTg2LS40NjUuMDYyLS4zMS0uMjE3LS44OTgtLjYxOS0xLjcwMy0uNjE5LTEuNDg3IDAtMi42NjQgMS4xMTUtMi42NjQgMi43NTcgMCAxLjYxMSAxLjE3NyAyLjcyNiAyLjY2NCAyLjcyNi45NiAwIDEuNjExLS40MzQgMS45Mi0uNzEyLjE4Ni0uMTI0LjMxLS4wOTMuNDM0LjA5M2wuMDkzLjEyNGMuMDkzLjE4Ni4wNjIuMzEtLjA5My40NjVhMy44MSAzLjgxIDAgMCAxLTIuNDE2Ljg2N2MtMi4wMTMgMC0zLjU2Mi0xLjQ1Ni0zLjU2Mi0zLjU2Mi4wMzEtMi4xMDYgMS41OC0zLjU5MyAzLjU5My0zLjU5M3ptNC4xMTktMi4xOTljMC0uMjE3LS4xMjQtLjM0MS4wOTMtLjM0MWguMjQ4Yy4yMTcgMCAuNTU4LjEyNC41NTguMzQxdjcuNDAzYzAgLjg2Ny40MDMuOTYuNzEyLjk5MS4xNTUgMCAuMjc5LjA5My4yNzkuMzF2LjIxN2MwIC4yMTctLjA5My4zNDEtLjM0MS4zNDEtLjU1NyAwLTEuNTQ5LS4xODYtMS41NDktMS42NzN2LTcuNTg5em02LjM1IDIuMTk5YzEuOTgyIDAgMy41OTMgMS41MTggMy41OTMgMy41MzEgMCAyLjA0NC0xLjYxMSAzLjU5My0zLjU5MyAzLjU5M3MtMy41OTMtMS41NDktMy41OTMtMy41OTNjMC0yLjAxMyAxLjYxMS0zLjUzMSAzLjU5My0zLjUzMXptMCA2LjMxOWMxLjQ1NiAwIDIuNjMzLTEuMTc3IDIuNjMzLTIuNzg4IDAtMS41NDktMS4xNzctMi42OTUtMi42MzMtMi42OTVhMi42NyAyLjY3IDAgMCAwLTIuNjY0IDIuNjk1Yy4wMzEgMS41OCAxLjIwOCAyLjc4OCAyLjY2NCAyLjc4OHptMTUuNDU2LTYuMzE5YTIuNDUgMi40NSAwIDAgMSAyLjIzIDEuMzYzaC4wMzFzLS4wMzEtLjIxNy0uMDMxLS41MjZ2LTMuMDY2YzAtLjIxNy0uMDkzLS4zNDEuMTI0LS4zNDFoLjI0OGMuMjE3IDAgLjU1OC4xMjQuNTU4LjM0MXY4LjgyN2MwIC4yMTctLjA5My4zNDEtLjMxLjM0MWgtLjIxN2MtLjIxNyAwLS4zNDEtLjA5My0uMzQxLS4zMXYtLjUyN2MwLS4yNDguMDYyLS40MzQuMDYyLS40MzRoLS4wMzFzLS41ODkgMS40MjUtMi4zNTQgMS40MjVjLTEuODI3IDAtMi45NzMtMS40NTYtMi45NzMtMy41NjItLjA2Mi0yLjEwNiAxLjIwOC0zLjUzMSAzLjAwNC0zLjUzMWgwem0uMDMxIDYuMzE5YzEuMTQ2IDAgMi4xOTktLjgwNSAyLjE5OS0yLjc1NyAwLTEuMzk0LS43MTItMi43MjYtMi4xNjgtMi43MjYtMS4yMDggMC0yLjE5OS45OTEtMi4xOTkgMi43MjYuMDMxIDEuNjczLjg5OCAyLjc1NyAyLjE2OCAyLjc1N3ptLTU2LjU1OC42NWguMjQ4Yy4yMTcgMCAuMzQxLS4xMjQuMzQxLS4zNDF2LTYuNjI4YzAtMS4wNTMgMS4xNDYtMS43OTYgMi40NDctMS43OTZzMi40NDcuNzQzIDIuNDQ3IDEuNzk2djYuNjU5YzAgLjIxNy4xMjQuMzQxLjM0MS4zNDFoLjI0OGMuMjE3IDAgLjMxLS4xMjQuMzEtLjM0MXYtNi43MjFjMC0xLjc2Ni0xLjc2NS0yLjYzMy0zLjM3Ni0yLjYzM2gwIDAgMCAwYy0xLjU0OSAwLTMuMzE0Ljg2Ny0zLjMxNCAyLjYzM3Y2LjY5YzAgLjIxNy4wOTMuMzQxLjMxLjM0MXptNTEuNjk1LTYuODE0aC0uMjQ4Yy0uMjE3IDAtLjM0MS4xMjQtLjM0MS4zNDF2My43NDhjMCAxLjA1My0uNjgxIDIuMDEzLTIuMDEzIDIuMDEzLTEuMzAxIDAtMi4wMTMtLjk2LTIuMDEzLTIuMDEzdi0zLjc0OGMwLS4yMTctLjEyNC0uMzQxLS4zNDEtLjM0MUg1NC4zYy0uMjE3IDAtLjMxLjEyNC0uMzEuMzQxdjMuOTk2YzAgMS43NjUgMS4zMDEgMi42MzMgMi45MTIgMi42MzNoMCAwIDAgMGMxLjYxMSAwIDIuOTExLS44NjcgMi45MTEtMi42MzN2LTMuOTk2Yy4wMzEtLjIxNy0uMDkzLS4zNDEtLjMxLS4zNDFoMHptLTMwLjY2NC0uMDMxYy0uMDYyIDAtLjE1NS4wNjItLjIxNy4xNTVsLTEuMjM5IDEuNDg3LS45MjkgMS4xMTUtMS40MjUtMS43MDQtLjc3NC0uOTI5Yy0uMDYyLS4wOTMtLjE1NS0uMTI0LS4yMTctLjEyNHMtLjE1NS4wMzEtLjI0OC4wOTNsLS4xODYuMTU1Yy0uMTU1LjEyNC0uMTU1LjI3OS0uMDMxLjQ2NWwxLjIzOSAxLjQ4NyAxLjA1MyAxLjIzOS0xLjUxOCAxLjgyN2gwbC0uNzc0LjkyOWMtLjEyNC4xNTUtLjEyNC4zNDEuMDMxLjQ5NmwuMTg2LjE1NWMuMTU1LjEyNC4zMS4wOTMuNDY1LS4wNjJsMS4yMzktMS40ODcuOTI5LTEuMTE1IDEuNDI1IDEuNzA0aDBsLjc3NC45MjljLjEyNC4xNTUuMzEuMTg2LjQ2NS4wMzFsLjE4Ni0uMTU1Yy4xNTUtLjEyNC4xNTUtLjI3OS4wMzEtLjQ2NWwtMS4yMzktMS40ODctMS4wNTMtMS4yMzkgMS41MTgtMS44MjdoMGwuNzc0LS45MjljLjEyNC0uMTU1LjEyNC0uMzQxLS4wMzEtLjQ5NWwtLjE4Ni0uMTg2Yy0uMDkzLS4wNjItLjE1NS0uMDkzLS4yNDgtLjA2MmgweiIgZmlsbD0iIzAwODJjOSIgZmlsbC1ydWxlPSJub256ZXJvIiBzdHJva2U9Im5vbmUiLz48L3N5bWJvbD48L3N2Zz4=', + }, + categories: [{ id: 3, name: 'Data & Storage' }], + displayName: 'Nextcloud', + typeVersion: 1, + }, + ], + categories: [ + { id: 2, name: 'Sales' }, + { id: 8, name: 'Finance & Accounting' }, + ], + image: [], + full: true, +} satisfies ITemplatesWorkflowFull; diff --git a/packages/editor-ui/src/views/SetupWorkflowFromTemplateView/__tests__/setupTemplate.store.test.ts b/packages/editor-ui/src/views/SetupWorkflowFromTemplateView/__tests__/setupTemplate.store.test.ts index 00cfec87aa..c12b551cc7 100644 --- a/packages/editor-ui/src/views/SetupWorkflowFromTemplateView/__tests__/setupTemplate.store.test.ts +++ b/packages/editor-ui/src/views/SetupWorkflowFromTemplateView/__tests__/setupTemplate.store.test.ts @@ -16,6 +16,7 @@ import * as testData from './setupTemplate.store.testData'; import { createTestingPinia } from '@pinia/testing'; import { useCredentialsStore } from '@/stores/credentials.store'; import { newWorkflowTemplateNode } from '@/utils/testData/templateTestData'; +import { useNodeTypesStore } from '@/stores/nodeTypes.store'; const objToMap = (obj: Record) => { return new Map(Object.entries(obj)) as Map; @@ -36,33 +37,6 @@ describe('SetupWorkflowFromTemplateView store', () => { }, typeVersion: 1, }, - Telegram: { - name: 'Telegram', - type: 'n8n-nodes-base.telegram', - position: [720, -20], - parameters: { - text: '=Hey there, my design is now on a new product!\nVisit my {{$json["vendor"]}} shop to get this cool{{$json["title"]}} (and check out more {{$json["product_type"]}})', - chatId: '123456', - additionalFields: {}, - }, - credentials: { - telegramApi: 'telegram', - }, - typeVersion: 1, - }, - shopify: { - name: 'shopify', - type: 'n8n-nodes-base.shopifyTrigger', - position: [540, -110], - webhookId: '2a7e0e50-8f09-4a2b-bf54-a849a6ac4fe0', - parameters: { - topic: 'products/create', - }, - credentials: { - shopifyApi: 'shopify', - }, - typeVersion: 1, - }, } satisfies Record; describe('groupNodeCredentialsByTypeAndName', () => { @@ -71,7 +45,14 @@ describe('SetupWorkflowFromTemplateView store', () => { }); it('returns credentials grouped by type and name', () => { - expect(groupNodeCredentialsByKey(Object.values(nodesByName))).toEqual( + expect( + groupNodeCredentialsByKey([ + { + node: nodesByName.Twitter, + requiredCredentials: testData.nodeTypeTwitterV1.credentials, + }, + ]), + ).toEqual( objToMap({ 'twitterOAuth1Api-twitter': { key: 'twitterOAuth1Api-twitter', @@ -80,20 +61,6 @@ describe('SetupWorkflowFromTemplateView store', () => { nodeTypeName: 'n8n-nodes-base.twitter', usedBy: [nodesByName.Twitter], }, - 'telegramApi-telegram': { - key: 'telegramApi-telegram', - credentialName: 'telegram', - credentialType: 'telegramApi', - nodeTypeName: 'n8n-nodes-base.telegram', - usedBy: [nodesByName.Telegram], - }, - 'shopifyApi-shopify': { - key: 'shopifyApi-shopify', - credentialName: 'shopify', - credentialType: 'shopifyApi', - nodeTypeName: 'n8n-nodes-base.shopifyTrigger', - usedBy: [nodesByName.shopify], - }, }), ); }); @@ -114,7 +81,18 @@ describe('SetupWorkflowFromTemplateView store', () => { }) as IWorkflowTemplateNodeWithCredentials, ]; - expect(groupNodeCredentialsByKey([node1, node2])).toEqual( + expect( + groupNodeCredentialsByKey([ + { + node: node1, + requiredCredentials: testData.nodeTypeTwitterV1.credentials, + }, + { + node: node2, + requiredCredentials: testData.nodeTypeTelegramV1.credentials, + }, + ]), + ).toEqual( objToMap({ 'twitterOAuth1Api-credential': { key: 'twitterOAuth1Api-credential', @@ -206,32 +184,35 @@ describe('SetupWorkflowFromTemplateView store', () => { stubActions: false, }), ); - }); - it('returns an empty object if there are no credential overrides', () => { - // Setup const credentialsStore = useCredentialsStore(); credentialsStore.setCredentialTypes([testData.credentialTypeTelegram]); const templatesStore = useTemplatesStore(); templatesStore.addWorkflows([testData.fullShopifyTelegramTwitterTemplate]); - + const nodeTypesStore = useNodeTypesStore(); + nodeTypesStore.setNodeTypes([ + testData.nodeTypeTelegramV1, + testData.nodeTypeTwitterV1, + testData.nodeTypeShopifyTriggerV1, + ]); const setupTemplateStore = useSetupTemplateStore(); setupTemplateStore.setTemplateId(testData.fullShopifyTelegramTwitterTemplate.id.toString()); + }); + + it('should return an empty object if there are no credential overrides', () => { + // Setup + const setupTemplateStore = useSetupTemplateStore(); expect(setupTemplateStore.credentialUsages.length).toBe(3); expect(setupTemplateStore.credentialOverrides).toEqual({}); }); - it('returns overrides for one node', () => { + it('should return overrides for one node', () => { // Setup const credentialsStore = useCredentialsStore(); - credentialsStore.setCredentialTypes([testData.credentialTypeTelegram]); credentialsStore.setCredentials([testData.credentialsTelegram1]); - const templatesStore = useTemplatesStore(); - templatesStore.addWorkflows([testData.fullShopifyTelegramTwitterTemplate]); const setupTemplateStore = useSetupTemplateStore(); - setupTemplateStore.setTemplateId(testData.fullShopifyTelegramTwitterTemplate.id.toString()); setupTemplateStore.setSelectedCredentialId( keyFromCredentialTypeAndName('twitterOAuth1Api', 'twitter'), testData.credentialsTelegram1.id, @@ -254,17 +235,25 @@ describe('SetupWorkflowFromTemplateView store', () => { stubActions: false, }), ); - }); - it("selects no credentials when there isn't any available", () => { - // Setup const credentialsStore = useCredentialsStore(); credentialsStore.setCredentialTypes([testData.credentialTypeTelegram]); const templatesStore = useTemplatesStore(); templatesStore.addWorkflows([testData.fullShopifyTelegramTwitterTemplate]); - + const nodeTypesStore = useNodeTypesStore(); + nodeTypesStore.setNodeTypes([ + testData.nodeTypeTelegramV1, + testData.nodeTypeTwitterV1, + testData.nodeTypeShopifyTriggerV1, + testData.nodeTypeHttpRequestV1, + ]); const setupTemplateStore = useSetupTemplateStore(); setupTemplateStore.setTemplateId(testData.fullShopifyTelegramTwitterTemplate.id.toString()); + }); + + it("should select no credentials when there isn't any available", () => { + // Setup + const setupTemplateStore = useSetupTemplateStore(); // Execute setupTemplateStore.setInitialCredentialSelection(); @@ -272,16 +261,12 @@ describe('SetupWorkflowFromTemplateView store', () => { expect(setupTemplateStore.selectedCredentialIdByKey).toEqual({}); }); - it("selects credential when there's only one", () => { + it("should select credential when there's only one", () => { // Setup const credentialsStore = useCredentialsStore(); - credentialsStore.setCredentialTypes([testData.credentialTypeTelegram]); credentialsStore.setCredentials([testData.credentialsTelegram1]); - const templatesStore = useTemplatesStore(); - templatesStore.addWorkflows([testData.fullShopifyTelegramTwitterTemplate]); const setupTemplateStore = useSetupTemplateStore(); - setupTemplateStore.setTemplateId(testData.fullShopifyTelegramTwitterTemplate.id.toString()); // Execute setupTemplateStore.setInitialCredentialSelection(); @@ -291,19 +276,15 @@ describe('SetupWorkflowFromTemplateView store', () => { }); }); - it('selects no credentials when there are more than 1 available', () => { + it('should select no credentials when there are more than 1 available', () => { // Setup const credentialsStore = useCredentialsStore(); - credentialsStore.setCredentialTypes([testData.credentialTypeTelegram]); credentialsStore.setCredentials([ testData.credentialsTelegram1, testData.credentialsTelegram2, ]); - const templatesStore = useTemplatesStore(); - templatesStore.addWorkflows([testData.fullShopifyTelegramTwitterTemplate]); const setupTemplateStore = useSetupTemplateStore(); - setupTemplateStore.setTemplateId(testData.fullShopifyTelegramTwitterTemplate.id.toString()); // Execute setupTemplateStore.setInitialCredentialSelection(); @@ -312,14 +293,14 @@ describe('SetupWorkflowFromTemplateView store', () => { }); test.each([ - ['httpBasicAuth'], - ['httpCustomAuth'], - ['httpDigestAuth'], - ['httpHeaderAuth'], - ['oAuth1Api'], - ['oAuth2Api'], - ['httpQueryAuth'], - ])('does not auto-select credentials for %s', (credentialType) => { + ['httpBasicAuth', 'basicAuth'], + ['httpCustomAuth', 'basicAuth'], + ['httpDigestAuth', 'digestAuth'], + ['httpHeaderAuth', 'headerAuth'], + ['oAuth1Api', 'oAuth1'], + ['oAuth2Api', 'oAuth2'], + ['httpQueryAuth', 'queryAuth'], + ])('should not auto-select credentials for %s', (credentialType, auth) => { // Setup const credentialsStore = useCredentialsStore(); credentialsStore.setCredentialTypes([testData.newCredentialType(credentialType)]); @@ -338,7 +319,9 @@ describe('SetupWorkflowFromTemplateView store', () => { credentials: { [credentialType]: 'Test', }, - parameters: {}, + parameters: { + authentication: auth, + }, position: [250, 300], }); templatesStore.addWorkflows([workflow]); @@ -353,4 +336,84 @@ describe('SetupWorkflowFromTemplateView store', () => { expect(setupTemplateStore.selectedCredentialIdByKey).toEqual({}); }); }); + + describe("With template that has nodes requiring credentials but workflow doesn't have them", () => { + beforeEach(() => { + setActivePinia( + createTestingPinia({ + stubActions: false, + }), + ); + + // Setup + const credentialsStore = useCredentialsStore(); + credentialsStore.setCredentialTypes([testData.credentialTypeTelegram]); + const templatesStore = useTemplatesStore(); + templatesStore.addWorkflows([testData.fullSaveEmailAttachmentsToNextCloudTemplate]); + const nodeTypesStore = useNodeTypesStore(); + nodeTypesStore.setNodeTypes([ + testData.nodeTypeReadImapV1, + testData.nodeTypeReadImapV2, + testData.nodeTypeNextCloudV1, + ]); + const setupTemplateStore = useSetupTemplateStore(); + setupTemplateStore.setTemplateId( + testData.fullSaveEmailAttachmentsToNextCloudTemplate.id.toString(), + ); + }); + + const templateImapNode = testData.fullSaveEmailAttachmentsToNextCloudTemplate.workflow.nodes[0]; + const templateNextcloudNode = + testData.fullSaveEmailAttachmentsToNextCloudTemplate.workflow.nodes[1]; + + it('should return correct credential usages', () => { + const setupTemplateStore = useSetupTemplateStore(); + expect(setupTemplateStore.credentialUsages).toEqual([ + { + credentialName: '', + credentialType: 'imap', + key: 'imap-', + nodeTypeName: 'n8n-nodes-base.emailReadImap', + usedBy: [templateImapNode], + }, + { + credentialName: '', + credentialType: 'nextCloudApi', + key: 'nextCloudApi-', + nodeTypeName: 'n8n-nodes-base.nextCloud', + usedBy: [templateNextcloudNode], + }, + ]); + }); + + it('should return correct app credentials', () => { + const setupTemplateStore = useSetupTemplateStore(); + expect(setupTemplateStore.appCredentials).toEqual([ + { + appName: 'Email (IMAP)', + credentials: [ + { + credentialName: '', + credentialType: 'imap', + key: 'imap-', + nodeTypeName: 'n8n-nodes-base.emailReadImap', + usedBy: [templateImapNode], + }, + ], + }, + { + appName: 'Nextcloud', + credentials: [ + { + credentialName: '', + credentialType: 'nextCloudApi', + key: 'nextCloudApi-', + nodeTypeName: 'n8n-nodes-base.nextCloud', + usedBy: [templateNextcloudNode], + }, + ], + }, + ]); + }); + }); }); diff --git a/packages/editor-ui/src/views/SetupWorkflowFromTemplateView/__tests__/setupTemplate.store.testData.ts b/packages/editor-ui/src/views/SetupWorkflowFromTemplateView/__tests__/setupTemplate.store.testData.ts index b036138969..aafff75971 100644 --- a/packages/editor-ui/src/views/SetupWorkflowFromTemplateView/__tests__/setupTemplate.store.testData.ts +++ b/packages/editor-ui/src/views/SetupWorkflowFromTemplateView/__tests__/setupTemplate.store.testData.ts @@ -4,7 +4,6 @@ import type { ITemplatesWorkflowFull, IWorkflowTemplateNode, } from '@/Interface'; -import type { ICredentialType } from 'n8n-workflow'; export const newFullOneNodeTemplate = (node: IWorkflowTemplateNode): ITemplatesWorkflowFull => ({ full: true, @@ -45,183 +44,6 @@ export const newFullOneNodeTemplate = (node: IWorkflowTemplateNode): ITemplatesW }, }); -export const fullShopifyTelegramTwitterTemplate: ITemplatesWorkflowFull = { - full: true, - id: 1205, - name: 'Promote new Shopify products on Twitter and Telegram', - totalViews: 485, - createdAt: '2021-08-24T10:40:50.007Z', - description: - 'This workflow automatically promotes your new Shopify products on Twitter and Telegram. This workflow is also featured in the blog post [*6 e-commerce workflows to power up your Shopify store*](https://n8n.io/blog/no-code-ecommerce-workflow-automations/#promote-your-new-products-on-social-media).\n\n## Prerequisites\n\n- A Shopify account and [credentials](https://docs.n8n.io/integrations/credentials/shopify/)\n- A Twitter account and [credentials](https://docs.n8n.io/integrations/credentials/twitter/)\n- A Telegram account and [credentials](https://docs.n8n.io/integrations/credentials/telegram/) for the channel you want to send messages to.\n\n## Nodes\n\n- [Shopify Trigger node](https://docs.n8n.io/integrations/trigger-nodes/n8n-nodes-base.shopifytrigger/) triggers the workflow when you create a new product in Shopify.\n- [Twitter node](https://docs.n8n.io/integrations/nodes/n8n-nodes-base.twitter/) posts a tweet with the text "Hey there, my design is now on a new product! Visit my {shop name} to get this cool {product title} (and check out more {product type})".\n- [Telegram node](https://docs.n8n.io/integrations/nodes/n8n-nodes-base.telegram/) posts a message with the same text as above in a Telegram channel.', - workflow: { - nodes: [ - { - name: 'Twitter', - type: 'n8n-nodes-base.twitter', - position: [720, -220], - parameters: { - text: '=Hey there, my design is now on a new product ✨\nVisit my {{$json["vendor"]}} shop to get this cool{{$json["title"]}} (and check out more {{$json["product_type"]}}) 🛍️', - additionalFields: {}, - }, - credentials: { - twitterOAuth1Api: 'twitter', - }, - typeVersion: 1, - }, - { - name: 'Telegram', - type: 'n8n-nodes-base.telegram', - position: [720, -20], - parameters: { - text: '=Hey there, my design is now on a new product!\nVisit my {{$json["vendor"]}} shop to get this cool{{$json["title"]}} (and check out more {{$json["product_type"]}})', - chatId: '123456', - additionalFields: {}, - }, - credentials: { - telegramApi: 'telegram_habot', - }, - typeVersion: 1, - }, - { - name: 'product created', - type: 'n8n-nodes-base.shopifyTrigger', - position: [540, -110], - webhookId: '2a7e0e50-8f09-4a2b-bf54-a849a6ac4fe0', - parameters: { - topic: 'products/create', - }, - credentials: { - shopifyApi: 'shopify_nodeqa', - }, - typeVersion: 1, - }, - ], - connections: { - 'product created': { - main: [ - [ - { - node: 'Twitter', - type: 'main', - index: 0, - }, - { - node: 'Telegram', - type: 'main', - index: 0, - }, - ], - ], - }, - }, - }, - workflowInfo: { - nodeCount: 3, - nodeTypes: { - 'n8n-nodes-base.twitter': { - count: 1, - }, - 'n8n-nodes-base.telegram': { - count: 1, - }, - 'n8n-nodes-base.shopifyTrigger': { - count: 1, - }, - }, - }, - user: { - username: 'lorenanda', - }, - nodes: [ - { - id: 49, - icon: 'file:telegram.svg', - name: 'n8n-nodes-base.telegram', - defaults: { - name: 'Telegram', - }, - iconData: { - type: 'file', - fileBuffer: - 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSIwIDAgNjYgNjYiIGZpbGw9IiNmZmYiIGZpbGwtcnVsZT0iZXZlbm9kZCIgc3Ryb2tlPSIjMDAwIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjx1c2UgeGxpbms6aHJlZj0iI2EiIHg9Ii41IiB5PSIuNSIvPjxzeW1ib2wgaWQ9ImEiIG92ZXJmbG93PSJ2aXNpYmxlIj48ZyBzdHJva2U9Im5vbmUiIGZpbGwtcnVsZT0ibm9uemVybyI+PHBhdGggZD0iTTAgMzJjMCAxNy42NzMgMTQuMzI3IDMyIDMyIDMyczMyLTE0LjMyNyAzMi0zMlM0OS42NzMgMCAzMiAwIDAgMTQuMzI3IDAgMzIiIGZpbGw9IiMzN2FlZTIiLz48cGF0aCBkPSJNMjEuNjYxIDM0LjMzOGwzLjc5NyAxMC41MDhzLjQ3NS45ODMuOTgzLjk4MyA4LjA2OC03Ljg2NCA4LjA2OC03Ljg2NGw4LjQwNy0xNi4yMzctMjEuMTE5IDkuODk4eiIgZmlsbD0iI2M4ZGFlYSIvPjxwYXRoIGQ9Ik0yNi42OTUgMzcuMDM0bC0uNzI5IDcuNzQ2cy0uMzA1IDIuMzczIDIuMDY4IDBsNC42NDQtNC4yMDMiIGZpbGw9IiNhOWM2ZDgiLz48cGF0aCBkPSJNMjEuNzMgMzQuNzEybC03LjgwOS0yLjU0NXMtLjkzMi0uMzc4LS42MzMtMS4yMzdjLjA2Mi0uMTc3LjE4Ni0uMzI4LjU1OS0uNTg4IDEuNzMxLTEuMjA2IDMyLjAyOC0xMi4wOTYgMzIuMDI4LTEyLjA5NnMuODU2LS4yODggMS4zNjEtLjA5N2MuMjMxLjA4OC4zNzguMTg3LjUwMy41NDguMDQ1LjEzMi4wNzEuNDExLjA2OC42ODktLjAwMy4yMDEtLjAyNy4zODYtLjA0NS42NzgtLjE4NCAyLjk3OC01LjcwNiAyNS4xOTgtNS43MDYgMjUuMTk4cy0uMzMgMS4zLTEuNTE0IDEuMzQ1Yy0uNDMyLjAxNi0uOTU2LS4wNzEtMS41ODItLjYxLTIuMzIzLTEuOTk4LTEwLjM1Mi03LjM5NC0xMi4xMjYtOC41OGEuMzQuMzQgMCAwMS0uMTQ2LS4yMzljLS4wMjUtLjEyNS4xMDgtLjI4LjEwOC0uMjhzMTMuOTgtMTIuNDI3IDE0LjM1Mi0xMy43MzFjLjAyOS0uMTAxLS4wNzktLjE1MS0uMjI2LS4xMDctLjkyOS4zNDItMTcuMDI1IDEwLjUwNi0xOC44MDEgMTEuNjI5LS4xMDQuMDY2LS4zOTUuMDIzLS4zOTUuMDIzIi8+PC9nPjwvc3ltYm9sPjwvc3ZnPg==', - }, - categories: [ - { - id: 6, - name: 'Communication', - }, - ], - displayName: 'Telegram', - typeVersion: 1, - }, - { - id: 107, - icon: 'file:shopify.svg', - name: 'n8n-nodes-base.shopifyTrigger', - defaults: { - name: 'Shopify Trigger', - }, - iconData: { - type: 'file', - fileBuffer: - 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSIwIDAgNTggNjYiIGZpbGw9IiNmZmYiIGZpbGwtcnVsZT0iZXZlbm9kZCIgc3Ryb2tlPSIjMDAwIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjx1c2UgeGxpbms6aHJlZj0iI2EiIHg9Ii41IiB5PSIuNSIvPjxzeW1ib2wgaWQ9ImEiIG92ZXJmbG93PSJ2aXNpYmxlIj48ZyBzdHJva2U9Im5vbmUiIGZpbGwtcnVsZT0ibm9uemVybyI+PHBhdGggZD0iTTQ5LjI1NSAxMi40ODRhLjYzMy42MzMgMCAwMC0uNTY0LS41MjdjLS4yMjUtLjAzNy01LjE3LS4zNzYtNS4xNy0uMzc2bC0zLjc3LTMuNzdjLS4zNC0uMzc2LTEuMDkyLS4yNjYtMS4zNzYtLjE4OC0uMDM3IDAtLjc1Mi4yMjUtMS45MjIuNjA1LTEuMTM3LTMuMy0zLjE1LTYuMzA2LTYuNjk2LTYuMzA2aC0uMzAzQzI4LjQzOC42MDUgMjcuMTk0IDAgMjYuMTQ0IDBjLTguMjU2LjAzNy0xMi4yIDEwLjMzMy0xMy40MzQgMTUuNTk0bC01Ljc3IDEuNzdjLTEuNzcuNTY0LTEuODM1LjYwNS0yLjA3MyAyLjI5M0wwIDU3LjE3NSAzNi40NjggNjRsMTkuNzYzLTQuMjZjMC0uMDM3LTYuOTQtNDYuODk3LTYuOTc2LTQ3LjI1NXpNMzQuNDMxIDguODZjLS45MTcuMzAzLTEuOTYzLjYwNS0zLjEuOTQ1di0uNjhhMTUuMDMgMTUuMDMgMCAwMC0uNzUyLTQuOTk5YzEuODQ4LjI4NCAzLjEgMi4zNTcgMy44NDMgNC43MzN6bS02LjA2OC00LjI5OGMuNjAzIDEuNzc4Ljg4MyAzLjY1LjgyNiA1LjUyN3YuMzRsLTYuMzc1IDEuOTYzYzEuMjQ4LTQuNjYgMy41NS02Ljk2MiA1LjU1LTcuODN6bS0yLjQ1LTIuMjkzYTEuOTQgMS45NCAwIDAxMS4wNTUuMzM5Yy0yLjY2IDEuMjM4LTUuNDcyIDQuMzY2LTYuNjc4IDEwLjYyN2wtNS4wNDUgMS41NDZDMTYuNjY4IDEwLjAzIDE5Ljk4OCAyLjI2IDI1LjkxIDIuMjZ6IiBmaWxsPSIjOTViZjQ3Ii8+PHBhdGggZD0iTTQ4LjY5MSAxMS45NTdjLS4yMjUtLjAzNy01LjE3LS4zNzYtNS4xNy0uMzc2bC0zLjc3LTMuNzdhLjc1My43NTMgMCAwMC0uNTI3LS4yMjVMMzYuNDcyIDY0bDE5Ljc2My00LjI2LTYuOTgtNDcuMjE4YS42OC42OCAwIDAwLS41NjQtLjU2NHoiIGZpbGw9IiM1ZThlM2UiLz48cGF0aCBkPSJNMjkuNzU4IDIyLjlsLTIuNDU0IDcuMjQyYTExLjM2IDExLjM2IDAgMDAtNC43NTItMS4xMzNjLTMuODQ4IDAtNC4wMzYgMi40MTItNC4wMzYgMy4wMTggMCAzLjI5OCA4LjYzNiA0LjU2NCA4LjYzNiAxMi4zMzMgMCA2LjEtMy44ODUgMTAuMDMtOS4xIDEwLjAzLTYuMjYgMC05LjQ2Ny0zLjg4NS05LjQ2Ny0zLjg4NWwxLjY2NS01LjU1czMuMjggMi44MyA2LjA3MyAyLjgzYTIuNDcgMi40NyAwIDAwMi41NjQtMi40OWMwLTQuMzQtNy4xLTQuNTI3LTcuMS0xMS42MTggMC01Ljk2MiA0LjI5OC0xMS43NyAxMi45MzQtMTEuNzcgMy4zOTQuMDUgNS4wMTggMSA1LjAxOCAxeiIvPjwvZz48L3N5bWJvbD48L3N2Zz4=', - }, - categories: [ - { - id: 2, - name: 'Sales', - }, - ], - displayName: 'Shopify Trigger', - typeVersion: 1, - }, - { - id: 325, - icon: 'file:x.svg', - name: 'n8n-nodes-base.twitter', - defaults: { - name: 'X', - }, - iconData: { - type: 'file', - fileBuffer: - 'data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMjQgMjQiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTE4LjI0NCAyLjI1aDMuMzA4bC03LjIyNyA4LjI2IDguNTAyIDExLjI0SDE2LjE3bC01LjIxNC02LjgxN0w0Ljk5IDIxLjc1SDEuNjhsNy43My04LjgzNUwxLjI1NCAyLjI1SDguMDhsNC43MTMgNi4yMzF6bS0xLjE2MSAxNy41MmgxLjgzM0w3LjA4NCA0LjEyNkg1LjExN3oiPjwvcGF0aD48L3N2Zz4K', - }, - categories: [ - { - id: 1, - name: 'Marketing & Content', - }, - ], - displayName: 'X (Formerly Twitter)', - typeVersion: 2, - }, - ], - categories: [ - { - id: 2, - name: 'Sales', - }, - { - id: 19, - name: 'Marketing & Growth', - }, - ], - image: [ - { - id: 527, - url: 'https://n8niostorageaccount.blob.core.windows.net/n8nio-strapi-blobs-prod/assets/89a078b208fe4c6181902608b1cd1332.png', - }, - ], -}; - -export const newCredentialType = (name: string): ICredentialType => ({ - name, - displayName: name, - documentationUrl: name, - properties: [], -}); - export const newCredential = ( opts: Pick & Partial, ): ICredentialsResponse => ({ @@ -233,31 +55,6 @@ export const newCredential = ( ...opts, }); -export const credentialTypeTelegram: ICredentialType = { - name: 'telegramApi', - displayName: 'Telegram API', - documentationUrl: 'telegram', - properties: [ - { - displayName: 'Access Token', - name: 'accessToken', - type: 'string', - typeOptions: { - password: true, - }, - default: '', - description: - 'Chat with the bot father to obtain the access token', - }, - ], - test: { - request: { - baseURL: '=https://api.telegram.org/bot{{$credentials.accessToken}}', - url: '/getMe', - }, - }, -}; - export const credentialsTelegram1: ICredentialsResponse = { createdAt: '2023-11-23T14:26:07.969Z', updatedAt: '2023-11-23T14:26:07.964Z', @@ -307,3 +104,22 @@ export const credentialsTelegram2: ICredentialsResponse = { }, sharedWith: [], }; + +export { + fullSaveEmailAttachmentsToNextCloudTemplate, + fullShopifyTelegramTwitterTemplate, +} from '@/utils/testData/templateTestData'; + +export { credentialTypeTelegram, newCredentialType } from '@/utils/testData/credentialTypeTestData'; + +export { + nodeTypeHttpRequestV1, + nodeTypeNextCloudV1, + nodeTypeReadImapV1, + nodeTypeReadImapV2, + nodeTypeShopifyTriggerV1, + nodeTypeTelegramV1, + nodeTypeTelegramV1_1, + nodeTypeTwitterV1, + nodeTypeTwitterV2, +} from '@/utils/testData/nodeTypeTestData'; diff --git a/packages/editor-ui/src/views/SetupWorkflowFromTemplateView/setupTemplate.store.ts b/packages/editor-ui/src/views/SetupWorkflowFromTemplateView/setupTemplate.store.ts index 0f2407da43..634f0ff3d6 100644 --- a/packages/editor-ui/src/views/SetupWorkflowFromTemplateView/setupTemplate.store.ts +++ b/packages/editor-ui/src/views/SetupWorkflowFromTemplateView/setupTemplate.store.ts @@ -8,7 +8,11 @@ import { useRootStore } from '@/stores/n8nRoot.store'; import { useTemplatesStore } from '@/stores/templates.store'; import { useWorkflowsStore } from '@/stores/workflows.store'; import { getAppNameFromNodeName } from '@/utils/nodeTypesUtils'; -import type { INodeCredentialsDetails, INodeTypeDescription } from 'n8n-workflow'; +import type { + INodeCredentialDescription, + INodeCredentialsDetails, + INodeTypeDescription, +} from 'n8n-workflow'; import type { ICredentialsResponse, INodeUi, @@ -17,17 +21,15 @@ import type { } from '@/Interface'; import { VIEWS } from '@/constants'; import { createWorkflowFromTemplate } from '@/utils/templates/templateActions'; -import type { - TemplateCredentialKey, - IWorkflowTemplateNodeWithCredentials, -} from '@/utils/templates/templateTransforms'; +import type { TemplateCredentialKey } from '@/utils/templates/templateTransforms'; import { - hasNodeCredentials, keyFromCredentialTypeAndName, normalizeTemplateNodeCredentials, } from '@/utils/templates/templateTransforms'; import { useExternalHooks } from '@/composables/useExternalHooks'; import { useTelemetry } from '@/composables/useTelemetry'; +import { getNodeTypeDisplayableCredentials } from '@/utils/nodes/nodeTransforms'; +import type { NodeTypeProvider } from '@/utils/nodeTypes/nodeTypeTransforms'; export type NodeAndType = { node: INodeUi; @@ -62,33 +64,56 @@ export type AppCredentialCount = { count: number; }; +export type TemplateNodeWithRequiredCredential = { + node: IWorkflowTemplateNode; + requiredCredentials: INodeCredentialDescription[]; +}; + //#region Getter functions +/** + * Returns the nodes in the template that require credentials + * and the required credentials for each node. + */ export const getNodesRequiringCredentials = ( + nodeTypeProvider: NodeTypeProvider, template: ITemplatesWorkflowFull, -): IWorkflowTemplateNodeWithCredentials[] => { +): TemplateNodeWithRequiredCredential[] => { if (!template) { return []; } - return template.workflow.nodes.filter(hasNodeCredentials); + const nodesWithCredentials: TemplateNodeWithRequiredCredential[] = template.workflow.nodes + .map((node) => ({ + node, + requiredCredentials: getNodeTypeDisplayableCredentials(nodeTypeProvider, node), + })) + .filter(({ requiredCredentials }) => requiredCredentials.length > 0); + + return nodesWithCredentials; }; -export const groupNodeCredentialsByKey = (nodes: IWorkflowTemplateNodeWithCredentials[]) => { +export const groupNodeCredentialsByKey = ( + nodeWithRequiredCredentials: TemplateNodeWithRequiredCredential[], +) => { const credentialsByTypeName = new Map(); - for (const node of nodes) { - const normalizedCreds = normalizeTemplateNodeCredentials(node.credentials); - for (const credentialType in normalizedCreds) { - const credentialName = normalizedCreds[credentialType]; - const key = keyFromCredentialTypeAndName(credentialType, credentialName); + for (const { node, requiredCredentials } of nodeWithRequiredCredentials) { + const normalizedNodeCreds = node.credentials + ? normalizeTemplateNodeCredentials(node.credentials) + : {}; + + for (const credentialDescription of requiredCredentials) { + const credentialType = credentialDescription.name; + const nodeCredentialName = normalizedNodeCreds[credentialDescription.name] ?? ''; + const key = keyFromCredentialTypeAndName(credentialType, nodeCredentialName); let credentialUsages = credentialsByTypeName.get(key); if (!credentialUsages) { credentialUsages = { key, nodeTypeName: node.type, - credentialName, + credentialName: nodeCredentialName, credentialType, usedBy: [], }; @@ -184,10 +209,12 @@ export const useSetupTemplateStore = defineStore('setupTemplate', () => { }); const nodesRequiringCredentialsSorted = computed(() => { - const credentials = template.value ? getNodesRequiringCredentials(template.value) : []; + const nodesWithCredentials = template.value + ? getNodesRequiringCredentials(nodeTypesStore, template.value) + : []; // Order by the X coordinate of the node - return sortBy(credentials, ({ position }) => position[0]); + return sortBy(nodesWithCredentials, ({ node }) => node.position[0]); }); const appNameByNodeType = (nodeTypeName: string, version?: number) => { @@ -339,12 +366,13 @@ export const useSetupTemplateStore = defineStore('setupTemplate', () => { try { isSaving.value = true; - const createdWorkflow = await createWorkflowFromTemplate( - template.value, - credentialOverrides.value, + const createdWorkflow = await createWorkflowFromTemplate({ + template: template.value, + credentialOverrides: credentialOverrides.value, rootStore, workflowsStore, - ); + nodeTypeProvider: nodeTypesStore, + }); telemetry.track('User closed cred setup', { completed: true, diff --git a/packages/workflow/src/NodeHelpers.ts b/packages/workflow/src/NodeHelpers.ts index eb5af85414..50474011cf 100644 --- a/packages/workflow/src/NodeHelpers.ts +++ b/packages/workflow/src/NodeHelpers.ts @@ -293,7 +293,7 @@ export function applySpecialNodeParameters(nodeType: INodeType): void { export function displayParameter( nodeValues: INodeParameters, parameter: INodeProperties | INodeCredentialDescription, - node: INode | null, // Allow null as it does also get used by credentials and they do not have versioning yet + node: Pick | null, // Allow null as it does also get used by credentials and they do not have versioning yet nodeValuesRoot?: INodeParameters, ) { if (!parameter.displayOptions) { @@ -391,7 +391,7 @@ export function displayParameterPath( nodeValues: INodeParameters, parameter: INodeProperties | INodeCredentialDescription, path: string, - node: INode | null, + node: Pick | null, ) { let resolvedNodeValues = nodeValues; if (path !== '') { @@ -567,7 +567,7 @@ export function getNodeParameters( nodeValues: INodeParameters | null, returnDefaults: boolean, returnNoneDisplayed: boolean, - node: INode | null, + node: Pick | null, onlySimpleTypes = false, dataIsResolved = false, nodeValuesRoot?: INodeParameters,