From d40b4e2aeba586bf31ef9391df0cead0f104a053 Mon Sep 17 00:00:00 2001 From: Tomi Turtiainen <10324676+tomi@users.noreply.github.com> Date: Thu, 4 Jan 2024 20:17:56 +0200 Subject: [PATCH] fix: fix use template telemetry events --- .../editor-ui/src/stores/templates.store.ts | 2 + .../__tests__/templateActions.test.ts | 158 ++ .../src/utils/templates/templateActions.ts | 134 +- .../src/utils/templates/templateTransforms.ts | 39 +- .../src/utils/testData/nodeTypeTestData.ts | 1872 +++++++++++++++++ .../src/utils/testData/templateTestData.ts | 145 ++ .../setupTemplate.store.ts | 58 +- .../src/views/TemplatesCollectionView.vue | 23 +- .../src/views/TemplatesWorkflowView.vue | 23 +- 9 files changed, 2361 insertions(+), 93 deletions(-) create mode 100644 packages/editor-ui/src/utils/templates/__tests__/templateActions.test.ts diff --git a/packages/editor-ui/src/stores/templates.store.ts b/packages/editor-ui/src/stores/templates.store.ts index e4e827584e175..6428d6a5f4919 100644 --- a/packages/editor-ui/src/stores/templates.store.ts +++ b/packages/editor-ui/src/stores/templates.store.ts @@ -28,6 +28,8 @@ function getSearchKey(query: ITemplatesQuery): string { return JSON.stringify([query.search || '', [...query.categories].sort()]); } +export type TemplatesStore = ReturnType; + export const useTemplatesStore = defineStore(STORES.TEMPLATES, { state: (): ITemplateState => ({ categories: {}, diff --git a/packages/editor-ui/src/utils/templates/__tests__/templateActions.test.ts b/packages/editor-ui/src/utils/templates/__tests__/templateActions.test.ts new file mode 100644 index 0000000000000..1f7c537c9a1ea --- /dev/null +++ b/packages/editor-ui/src/utils/templates/__tests__/templateActions.test.ts @@ -0,0 +1,158 @@ +import { VIEWS } from '@/constants'; +import { Telemetry } from '@/plugins/telemetry'; +import type { NodeTypesStore } from '@/stores/nodeTypes.store'; +import { useNodeTypesStore } from '@/stores/nodeTypes.store'; +import type { PosthogStore } from '@/stores/posthog.store'; +import { usePostHog } from '@/stores/posthog.store'; +import type { TemplatesStore } from '@/stores/templates.store'; +import { useTemplatesStore } from '@/stores/templates.store'; +import { useTemplateWorkflow } from '@/utils/templates/templateActions'; +import { + nodeTypeRespondToWebhookV1, + nodeTypeShopifyTriggerV1, + nodeTypeTelegramV1, + nodeTypeTwitterV1, + nodeTypeWebhookV1, + nodeTypeWebhookV1_1, + nodeTypesSet, +} from '@/utils/testData/nodeTypeTestData'; +import { + fullCreateApiEndpointTemplate, + fullShopifyTelegramTwitterTemplate, +} from '@/utils/testData/templateTestData'; +import { createTestingPinia } from '@pinia/testing'; +import { setActivePinia } from 'pinia'; +import { vi } from 'vitest'; +import type { Router } from 'vue-router'; + +describe('templateActions', () => { + describe('useTemplateWorkflow', () => { + const telemetry = new Telemetry(); + const externalHooks = { + run: vi.fn(), + }; + const router: Router = { + push: vi.fn(), + resolve: vi.fn(), + } as unknown as Router; + let nodeTypesStore: NodeTypesStore; + let posthogStore: PosthogStore; + let templatesStore: TemplatesStore; + + beforeEach(() => { + vi.resetAllMocks(); + setActivePinia( + createTestingPinia({ + stubActions: false, + }), + ); + + vi.spyOn(telemetry, 'track').mockImplementation(() => {}); + nodeTypesStore = useNodeTypesStore(); + posthogStore = usePostHog(); + templatesStore = useTemplatesStore(); + }); + + describe('When feature flag is disabled', () => { + const templateId = '1'; + + beforeEach(async () => { + posthogStore.isFeatureEnabled = vi.fn().mockReturnValue(false); + + await useTemplateWorkflow({ + externalHooks, + posthogStore, + nodeTypesStore, + telemetry, + templateId, + templatesStore, + router, + }); + }); + + it('should navigate to correct url', async () => { + expect(router.push).toHaveBeenCalledWith({ + name: VIEWS.TEMPLATE_IMPORT, + params: { id: templateId }, + }); + }); + + it("should track 'User inserted workflow template'", async () => { + expect(telemetry.track).toHaveBeenCalledWith( + 'User inserted workflow template', + { + source: 'workflow', + template_id: templateId, + wf_template_repo_session_id: '', + }, + { withPostHog: true }, + ); + }); + }); + + describe('When feature flag is enabled and template has nodes requiring credentials', () => { + const templateId = fullShopifyTelegramTwitterTemplate.id.toString(); + + beforeEach(async () => { + posthogStore.isFeatureEnabled = vi.fn().mockReturnValue(true); + templatesStore.addWorkflows([fullShopifyTelegramTwitterTemplate]); + nodeTypesStore.setNodeTypes([ + nodeTypeTelegramV1, + nodeTypeTwitterV1, + nodeTypeShopifyTriggerV1, + ]); + vi.spyOn(nodeTypesStore, 'loadNodeTypesIfNotLoaded').mockResolvedValue(); + + await useTemplateWorkflow({ + externalHooks, + posthogStore, + nodeTypesStore, + telemetry, + templateId, + templatesStore, + router, + }); + }); + + it('should navigate to correct url', async () => { + expect(router.push).toHaveBeenCalledWith({ + name: VIEWS.TEMPLATE_SETUP, + params: { id: templateId }, + }); + }); + }); + + describe("When feature flag is enabled and template doesn't have nodes requiring credentials", () => { + const templateId = fullCreateApiEndpointTemplate.id.toString(); + + beforeEach(async () => { + posthogStore.isFeatureEnabled = vi.fn().mockReturnValue(true); + templatesStore.addWorkflows([fullCreateApiEndpointTemplate]); + nodeTypesStore.setNodeTypes([ + nodeTypeWebhookV1, + nodeTypeWebhookV1_1, + nodeTypeRespondToWebhookV1, + ...Object.values(nodeTypesSet), + ]); + vi.spyOn(nodeTypesStore, 'loadNodeTypesIfNotLoaded').mockResolvedValue(); + + await useTemplateWorkflow({ + externalHooks, + posthogStore, + nodeTypesStore, + telemetry, + templateId, + templatesStore, + router, + }); + }); + + it('should navigate to correct url', async () => { + expect(router.push).toHaveBeenCalledWith({ + name: VIEWS.TEMPLATE_IMPORT, + params: { id: templateId }, + }); + }); + }); + }); +}); diff --git a/packages/editor-ui/src/utils/templates/templateActions.ts b/packages/editor-ui/src/utils/templates/templateActions.ts index 1d380b6015084..210ec3f10bdc2 100644 --- a/packages/editor-ui/src/utils/templates/templateActions.ts +++ b/packages/editor-ui/src/utils/templates/templateActions.ts @@ -1,4 +1,9 @@ -import type { INodeUi, IWorkflowData, IWorkflowTemplate } from '@/Interface'; +import type { + INodeUi, + ITemplatesWorkflowFull, + IWorkflowData, + IWorkflowTemplate, +} from '@/Interface'; import { getNewWorkflow } from '@/api/workflows'; import { TEMPLATE_CREDENTIAL_SETUP_EXPERIMENT, VIEWS } from '@/constants'; import type { useRootStore } from '@/stores/n8nRoot.store'; @@ -7,9 +12,19 @@ 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 { + getNodesRequiringCredentials, + replaceAllTemplateNodeCredentials, +} from '@/utils/templates/templateTransforms'; import type { INodeCredentialsDetails } from 'n8n-workflow'; import type { RouteLocationRaw, Router } from 'vue-router'; +import type { TemplatesStore } from '@/stores/templates.store'; +import type { NodeTypesStore } from '@/stores/nodeTypes.store'; +import type { Telemetry } from '@/plugins/telemetry'; +import type { useExternalHooks } from '@/composables/useExternalHooks'; +import { assert } from '@/utils/assert'; + +type ExternalHooks = ReturnType; /** * Creates a new workflow from a template @@ -49,28 +64,55 @@ export async function createWorkflowFromTemplate(opts: { } /** - * Opens the template credential setup view (or workflow view - * if the feature flag is disabled) + * Opens the template credential setup view */ -export async function openTemplateCredentialSetup(opts: { - posthogStore: PosthogStore; +async function openTemplateCredentialSetup(opts: { + templateId: string; + router: Router; + inNewBrowserTab?: boolean; +}) { + const { router, templateId, inNewBrowserTab = false } = opts; + + const routeLocation: RouteLocationRaw = { + name: VIEWS.TEMPLATE_SETUP, + params: { id: templateId }, + }; + + if (inNewBrowserTab) { + const route = router.resolve(routeLocation); + window.open(route.href, '_blank'); + } else { + await router.push(routeLocation); + } +} + +/** + * Opens the given template's workflow on NodeView. Fires necessary + * telemetry events. + */ +async function openTemplateWorkflowOnNodeView(opts: { + externalHooks: ExternalHooks; templateId: string; + templatesStore: TemplatesStore; router: Router; inNewBrowserTab?: boolean; + telemetry: Telemetry; }) { - const { router, templateId, inNewBrowserTab = false, posthogStore } = opts; - - const routeLocation: RouteLocationRaw = posthogStore.isFeatureEnabled( - TEMPLATE_CREDENTIAL_SETUP_EXPERIMENT, - ) - ? { - name: VIEWS.TEMPLATE_SETUP, - params: { id: templateId }, - } - : { - name: VIEWS.TEMPLATE_IMPORT, - params: { id: templateId }, - }; + const { externalHooks, templateId, templatesStore, telemetry, inNewBrowserTab, router } = opts; + const routeLocation: RouteLocationRaw = { + name: VIEWS.TEMPLATE_IMPORT, + params: { id: templateId }, + }; + const telemetryPayload = { + source: 'workflow', + template_id: templateId, + wf_template_repo_session_id: templatesStore.currentSessionId, + }; + + telemetry.track('User inserted workflow template', telemetryPayload, { + withPostHog: true, + }); + await externalHooks.run('templatesWorkflowView.openWorkflow', telemetryPayload); if (inNewBrowserTab) { const route = router.resolve(routeLocation); @@ -79,3 +121,57 @@ export async function openTemplateCredentialSetup(opts: { await router.push(routeLocation); } } + +function hasTemplateCredentials( + nodeTypeProvider: NodeTypeProvider, + template: ITemplatesWorkflowFull, +) { + const nodesRequiringCreds = getNodesRequiringCredentials(nodeTypeProvider, template); + + return nodesRequiringCreds.length > 0; +} + +async function getFullTemplate(templatesStore: TemplatesStore, templateId: string) { + const template = templatesStore.getFullTemplateById(templateId); + if (template) { + return template; + } + + await templatesStore.fetchTemplateById(templateId); + return templatesStore.getFullTemplateById(templateId); +} + +/** + * Uses the given template by opening the template workflow on NodeView + * or the template credential setup view. Fires necessary telemetry events. + */ +export async function useTemplateWorkflow(opts: { + externalHooks: ExternalHooks; + nodeTypesStore: NodeTypesStore; + posthogStore: PosthogStore; + templateId: string; + templatesStore: TemplatesStore; + router: Router; + inNewBrowserTab?: boolean; + telemetry: Telemetry; +}) { + const { nodeTypesStore, posthogStore, templateId, templatesStore } = opts; + + const openCredentialSetup = posthogStore.isFeatureEnabled(TEMPLATE_CREDENTIAL_SETUP_EXPERIMENT); + if (!openCredentialSetup) { + await openTemplateWorkflowOnNodeView(opts); + return; + } + + const [template] = await Promise.all([ + getFullTemplate(templatesStore, templateId), + nodeTypesStore.loadNodeTypesIfNotLoaded(), + ]); + assert(template); + + if (hasTemplateCredentials(nodeTypesStore, template)) { + await openTemplateCredentialSetup(opts); + } else { + await openTemplateWorkflowOnNodeView(opts); + } +} diff --git a/packages/editor-ui/src/utils/templates/templateTransforms.ts b/packages/editor-ui/src/utils/templates/templateTransforms.ts index 8be851865ac1b..e92b7a23476bc 100644 --- a/packages/editor-ui/src/utils/templates/templateTransforms.ts +++ b/packages/editor-ui/src/utils/templates/templateTransforms.ts @@ -1,8 +1,16 @@ -import type { IWorkflowTemplateNode, IWorkflowTemplateNodeCredentials } from '@/Interface'; +import type { + ITemplatesWorkflowFull, + 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'; +import type { + INodeCredentialDescription, + INodeCredentials, + INodeCredentialsDetails, +} from 'n8n-workflow'; export type IWorkflowTemplateNodeWithCredentials = IWorkflowTemplateNode & Required>; @@ -17,6 +25,11 @@ const credentialKeySymbol = Symbol('credentialKey'); */ export type TemplateCredentialKey = string & { [credentialKeySymbol]: never }; +export type TemplateNodeWithRequiredCredential = { + node: IWorkflowTemplateNode; + requiredCredentials: INodeCredentialDescription[]; +}; + /** * Forms a key from credential type name and credential name */ @@ -120,3 +133,25 @@ export const replaceAllTemplateNodeCredentials = ( }; }); }; + +/** + * Returns the nodes in the template that require credentials + * and the required credentials for each node. + */ +export const getNodesRequiringCredentials = ( + nodeTypeProvider: NodeTypeProvider, + template: ITemplatesWorkflowFull, +): TemplateNodeWithRequiredCredential[] => { + if (!template) { + return []; + } + + const nodesWithCredentials: TemplateNodeWithRequiredCredential[] = template.workflow.nodes + .map((node) => ({ + node, + requiredCredentials: getNodeTypeDisplayableCredentials(nodeTypeProvider, node), + })) + .filter(({ requiredCredentials }) => requiredCredentials.length > 0); + + return nodesWithCredentials; +}; diff --git a/packages/editor-ui/src/utils/testData/nodeTypeTestData.ts b/packages/editor-ui/src/utils/testData/nodeTypeTestData.ts index e623b05460fad..bd0b4ae440f88 100644 --- a/packages/editor-ui/src/utils/testData/nodeTypeTestData.ts +++ b/packages/editor-ui/src/utils/testData/nodeTypeTestData.ts @@ -4718,3 +4718,1875 @@ export const nodeTypeHttpRequestV1 = { alias: ['API', 'Request', 'URL', 'Build', 'cURL'], }, } satisfies INodeTypeDescription; + +export const nodeTypeRespondToWebhookV1 = { + displayName: 'Respond to Webhook', + name: 'n8n-nodes-base.respondToWebhook', + group: ['transform'], + version: 1, + description: 'Returns data for Webhook', + defaults: { name: 'Respond to Webhook' }, + inputs: ['main'], + outputs: ['main'], + credentials: [], + properties: [ + { + displayName: + 'Verify that the "Webhook" node\'s "Respond" parameter is set to "Using Respond to Webhook Node". More details', + name: 'generalNotice', + type: 'notice', + default: '', + }, + { + displayName: 'Respond With', + name: 'respondWith', + type: 'options', + options: [ + { + name: 'All Incoming Items', + value: 'allIncomingItems', + description: 'Respond with all input JSON items', + }, + { + name: 'Binary File', + value: 'binary', + description: 'Respond with incoming file binary data', + }, + { + name: 'First Incoming Item', + value: 'firstIncomingItem', + description: 'Respond with the first input JSON item', + }, + { name: 'JSON', value: 'json', description: 'Respond with a custom JSON body' }, + { name: 'No Data', value: 'noData', description: 'Respond with an empty body' }, + { + name: 'Redirect', + value: 'redirect', + description: 'Respond with a redirect to a given URL', + }, + { name: 'Text', value: 'text', description: 'Respond with a simple text message body' }, + ], + default: 'firstIncomingItem', + description: 'The data that should be returned', + }, + { + displayName: + 'When using expressions, note that this node will only run for the first item in the input data', + name: 'webhookNotice', + type: 'notice', + displayOptions: { show: { respondWith: ['json', 'text'] } }, + default: '', + }, + { + displayName: 'Redirect URL', + name: 'redirectURL', + type: 'string', + required: true, + displayOptions: { show: { respondWith: ['redirect'] } }, + default: '', + placeholder: 'e.g. http://www.n8n.io', + description: 'The URL to redirect to', + validateType: 'url', + }, + { + displayName: 'Response Body', + name: 'responseBody', + type: 'json', + displayOptions: { show: { respondWith: ['json'] } }, + default: '{\n "myField": "value"\n}', + typeOptions: { rows: 4 }, + description: 'The HTTP response JSON data', + }, + { + displayName: 'Response Body', + name: 'responseBody', + type: 'string', + displayOptions: { show: { respondWith: ['text'] } }, + typeOptions: { rows: 2 }, + default: '', + placeholder: 'e.g. Workflow completed', + description: 'The HTTP response text data', + }, + { + displayName: 'Response Data Source', + name: 'responseDataSource', + type: 'options', + displayOptions: { show: { respondWith: ['binary'] } }, + options: [ + { + name: 'Choose Automatically From Input', + value: 'automatically', + description: 'Use if input data will contain a single piece of binary data', + }, + { + name: 'Specify Myself', + value: 'set', + description: 'Enter the name of the input field the binary data will be in', + }, + ], + default: 'automatically', + }, + { + displayName: 'Input Field Name', + name: 'inputFieldName', + type: 'string', + required: true, + default: 'data', + displayOptions: { show: { respondWith: ['binary'], responseDataSource: ['set'] } }, + description: 'The name of the node input field with the binary data', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + options: [ + { + displayName: 'Response Code', + name: 'responseCode', + type: 'number', + typeOptions: { minValue: 100, maxValue: 599 }, + default: 200, + description: 'The HTTP response code to return. Defaults to 200.', + }, + { + displayName: 'Response Headers', + name: 'responseHeaders', + placeholder: 'Add Response Header', + description: 'Add headers to the webhook response', + type: 'fixedCollection', + typeOptions: { multipleValues: true }, + default: {}, + options: [ + { + name: 'entries', + displayName: 'Entries', + values: [ + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + description: 'Name of the header', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + description: 'Value of the header', + }, + ], + }, + ], + }, + { + displayName: 'Put Response in Field', + name: 'responseKey', + type: 'string', + displayOptions: { show: { '/respondWith': ['allIncomingItems', 'firstIncomingItem'] } }, + default: '', + description: 'The name of the response field to put all items in', + placeholder: 'e.g. data', + }, + ], + }, + ], + codex: { + categories: ['Core Nodes', 'Utility'], + subcategories: { 'Core Nodes': ['Helpers'] }, + resources: { + primaryDocumentation: [ + { + url: 'https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.respondtowebhook/', + }, + ], + }, + }, + iconUrl: 'icons/n8n-nodes-base/dist/nodes/RespondToWebhook/webhook.svg', +} satisfies INodeTypeDescription; + +export const nodeTypeWebhookV1 = { + displayName: 'Webhook', + name: 'n8n-nodes-base.webhook', + group: ['trigger'], + version: [1, 1.1], + description: 'Starts the workflow when a webhook is called', + eventTriggerDescription: 'Waiting for you to call the Test URL', + activationMessage: 'You can now make calls to your production webhook URL.', + defaults: { name: 'Webhook' }, + supportsCORS: true, + triggerPanel: { + header: '', + executionsHelp: { + inactive: + 'Webhooks have two modes: test and production.

Use test mode while you build your workflow. Click the \'listen\' button, then make a request to the test URL. The executions will show up in the editor.

Use production mode to run your workflow automatically.
Activate the workflow, then make requests to the production URL. These executions will show up in the executions list, but not in the editor.', + active: + 'Webhooks have two modes: test and production.

Use test mode while you build your workflow. Click the \'listen\' button, then make a request to the test URL. The executions will show up in the editor.

Use production mode to run your workflow automatically. Since the workflow is activated, you can make requests to the production URL. These executions will show up in the executions list, but not in the editor.', + }, + activationHint: + 'Once you’ve finished building your workflow, run it without having to click this button by using the production webhook URL.', + }, + inputs: [], + outputs: ['main'], + credentials: [ + { + name: 'httpBasicAuth', + required: true, + displayOptions: { show: { authentication: ['basicAuth'] } }, + }, + { + name: 'httpHeaderAuth', + required: true, + displayOptions: { show: { authentication: ['headerAuth'] } }, + }, + ], + webhooks: [ + { + name: 'default', + httpMethod: '={{$parameter["httpMethod"] || "GET"}}', + isFullPath: true, + responseCode: '={{$parameter["responseCode"]}}', + responseMode: '={{$parameter["responseMode"]}}', + responseData: + '={{$parameter["responseData"] || ($parameter.options.noResponseBody ? "noData" : undefined) }}', + responseBinaryPropertyName: '={{$parameter["responseBinaryPropertyName"]}}', + responseContentType: '={{$parameter["options"]["responseContentType"]}}', + responsePropertyName: '={{$parameter["options"]["responsePropertyName"]}}', + responseHeaders: '={{$parameter["options"]["responseHeaders"]}}', + path: '={{$parameter["path"]}}', + }, + ], + properties: [ + { + displayName: 'Authentication', + name: 'authentication', + type: 'options', + options: [ + { name: 'Basic Auth', value: 'basicAuth' }, + { name: 'Header Auth', value: 'headerAuth' }, + { name: 'None', value: 'none' }, + ], + default: 'none', + description: 'The way to authenticate', + }, + { + displayName: 'HTTP Method', + name: 'httpMethod', + type: 'options', + options: [ + { name: 'DELETE', value: 'DELETE' }, + { name: 'GET', value: 'GET' }, + { name: 'HEAD', value: 'HEAD' }, + { name: 'PATCH', value: 'PATCH' }, + { name: 'POST', value: 'POST' }, + { name: 'PUT', value: 'PUT' }, + ], + default: 'GET', + description: 'The HTTP method to listen to', + }, + { + displayName: 'Path', + name: 'path', + type: 'string', + default: '', + placeholder: 'webhook', + required: true, + description: 'The path to listen to', + }, + { + displayName: 'Respond', + name: 'responseMode', + type: 'options', + options: [ + { + name: 'Immediately', + value: 'onReceived', + description: 'As soon as this node executes', + }, + { + name: 'When Last Node Finishes', + value: 'lastNode', + description: 'Returns data of the last-executed node', + }, + { + name: "Using 'Respond to Webhook' Node", + value: 'responseNode', + description: 'Response defined in that node', + }, + ], + default: 'onReceived', + description: 'When and how to respond to the webhook', + }, + { + displayName: + 'Insert a \'Respond to Webhook\' node to control when and how you respond. More details', + name: 'webhookNotice', + type: 'notice', + displayOptions: { show: { responseMode: ['responseNode'] } }, + default: '', + }, + { + displayName: 'Response Code', + name: 'responseCode', + type: 'number', + displayOptions: { hide: { responseMode: ['responseNode'] } }, + typeOptions: { minValue: 100, maxValue: 599 }, + default: 200, + description: 'The HTTP Response code to return', + }, + { + displayName: 'Response Data', + name: 'responseData', + type: 'options', + displayOptions: { show: { responseMode: ['lastNode'] } }, + options: [ + { + name: 'All Entries', + value: 'allEntries', + description: 'Returns all the entries of the last node. Always returns an array.', + }, + { + name: 'First Entry JSON', + value: 'firstEntryJson', + description: + 'Returns the JSON data of the first entry of the last node. Always returns a JSON object.', + }, + { + name: 'First Entry Binary', + value: 'firstEntryBinary', + description: + 'Returns the binary data of the first entry of the last node. Always returns a binary file.', + }, + { name: 'No Response Body', value: 'noData', description: 'Returns without a body' }, + ], + default: 'firstEntryJson', + description: + 'What data should be returned. If it should return all items as an array or only the first item as object.', + }, + { + displayName: 'Property Name', + name: 'responseBinaryPropertyName', + type: 'string', + required: true, + default: 'data', + displayOptions: { show: { responseData: ['firstEntryBinary'] } }, + description: 'Name of the binary property to return', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + options: [ + { + displayName: 'Binary Data', + name: 'binaryData', + type: 'boolean', + displayOptions: { show: { '/httpMethod': ['PATCH', 'PUT', 'POST'], '@version': [1] } }, + default: false, + description: 'Whether the webhook will receive binary data', + }, + { + displayName: 'Binary Property', + name: 'binaryPropertyName', + type: 'string', + default: 'data', + displayOptions: { show: { binaryData: [true], '@version': [1] } }, + description: + 'Name of the binary property to write the data of the received file to. If the data gets received via "Form-Data Multipart" it will be the prefix and a number starting with 0 will be attached to it.', + }, + { + displayName: 'Binary Property', + name: 'binaryPropertyName', + type: 'string', + default: 'data', + displayOptions: { hide: { '@version': [1] } }, + description: + 'Name of the binary property to write the data of the received file to, only relevant if binary data is received', + }, + { + displayName: 'Ignore Bots', + name: 'ignoreBots', + type: 'boolean', + default: false, + description: 'Whether to ignore requests from bots like link previewers and web crawlers', + }, + { + displayName: 'No Response Body', + name: 'noResponseBody', + type: 'boolean', + default: false, + description: 'Whether to send any body in the response', + displayOptions: { + hide: { rawBody: [true] }, + show: { '/responseMode': ['onReceived'] }, + }, + }, + { + displayName: 'Raw Body', + name: 'rawBody', + type: 'boolean', + displayOptions: { + show: { '@version': [1] }, + hide: { binaryData: [true], noResponseBody: [true] }, + }, + default: false, + description: 'Raw body (binary)', + }, + { + displayName: 'Raw Body', + name: 'rawBody', + type: 'boolean', + displayOptions: { hide: { noResponseBody: [true], '@version': [1] } }, + default: false, + description: 'Whether to return the raw body', + }, + { + displayName: 'Response Data', + name: 'responseData', + type: 'string', + displayOptions: { + show: { '/responseMode': ['onReceived'] }, + hide: { noResponseBody: [true] }, + }, + default: '', + placeholder: 'success', + description: 'Custom response data to send', + }, + { + displayName: 'Response Content-Type', + name: 'responseContentType', + type: 'string', + displayOptions: { + show: { '/responseData': ['firstEntryJson'], '/responseMode': ['lastNode'] }, + }, + default: '', + placeholder: 'application/xml', + description: + 'Set a custom content-type to return if another one as the "application/json" should be returned', + }, + { + displayName: 'Response Headers', + name: 'responseHeaders', + placeholder: 'Add Response Header', + description: 'Add headers to the webhook response', + type: 'fixedCollection', + typeOptions: { multipleValues: true }, + default: {}, + options: [ + { + name: 'entries', + displayName: 'Entries', + values: [ + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + description: 'Name of the header', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + description: 'Value of the header', + }, + ], + }, + ], + }, + { + displayName: 'Property Name', + name: 'responsePropertyName', + type: 'string', + displayOptions: { + show: { '/responseData': ['firstEntryJson'], '/responseMode': ['lastNode'] }, + }, + default: 'data', + description: 'Name of the property to return the data of instead of the whole JSON', + }, + { + displayName: 'Allowed Origins (CORS)', + name: 'allowedOrigins', + type: 'string', + default: '*', + description: + 'The origin(s) to allow cross-origin non-preflight requests from in a browser', + }, + ], + }, + ], + codex: { + categories: ['Development', 'Core Nodes'], + subcategories: { 'Core Nodes': ['Helpers'] }, + resources: { + primaryDocumentation: [ + { url: 'https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.webhook/' }, + ], + }, + alias: ['HTTP', 'API', 'Build', 'WH'], + }, + iconUrl: 'icons/n8n-nodes-base/dist/nodes/Webhook/webhook.svg', +} satisfies INodeTypeDescription; + +export const nodeTypeWebhookV1_1 = { + displayName: 'Webhook', + name: 'n8n-nodes-base.webhook', + group: ['trigger'], + version: [1, 1.1], + description: 'Starts the workflow when a webhook is called', + eventTriggerDescription: 'Waiting for you to call the Test URL', + activationMessage: 'You can now make calls to your production webhook URL.', + defaults: { name: 'Webhook' }, + supportsCORS: true, + triggerPanel: { + header: '', + executionsHelp: { + inactive: + 'Webhooks have two modes: test and production.

Use test mode while you build your workflow. Click the \'listen\' button, then make a request to the test URL. The executions will show up in the editor.

Use production mode to run your workflow automatically. Activate the workflow, then make requests to the production URL. These executions will show up in the executions list, but not in the editor.', + active: + 'Webhooks have two modes: test and production.

Use test mode while you build your workflow. Click the \'listen\' button, then make a request to the test URL. The executions will show up in the editor.

Use production mode to run your workflow automatically. Since the workflow is activated, you can make requests to the production URL. These executions will show up in the executions list, but not in the editor.', + }, + activationHint: + 'Once you’ve finished building your workflow, run it without having to click this button by using the production webhook URL.', + }, + inputs: [], + outputs: ['main'], + credentials: [ + { + name: 'httpBasicAuth', + required: true, + displayOptions: { show: { authentication: ['basicAuth'] } }, + }, + { + name: 'httpHeaderAuth', + required: true, + displayOptions: { show: { authentication: ['headerAuth'] } }, + }, + ], + webhooks: [ + { + name: 'default', + httpMethod: '={{$parameter["httpMethod"] || "GET"}}', + isFullPath: true, + responseCode: '={{$parameter["responseCode"]}}', + responseMode: '={{$parameter["responseMode"]}}', + responseData: + '={{$parameter["responseData"] || ($parameter.options.noResponseBody ? "noData" : undefined) }}', + responseBinaryPropertyName: '={{$parameter["responseBinaryPropertyName"]}}', + responseContentType: '={{$parameter["options"]["responseContentType"]}}', + responsePropertyName: '={{$parameter["options"]["responsePropertyName"]}}', + responseHeaders: '={{$parameter["options"]["responseHeaders"]}}', + path: '={{$parameter["path"]}}', + }, + ], + properties: [ + { + displayName: 'Authentication', + name: 'authentication', + type: 'options', + options: [ + { name: 'Basic Auth', value: 'basicAuth' }, + { name: 'Header Auth', value: 'headerAuth' }, + { name: 'None', value: 'none' }, + ], + default: 'none', + description: 'The way to authenticate', + }, + { + displayName: 'HTTP Method', + name: 'httpMethod', + type: 'options', + options: [ + { name: 'DELETE', value: 'DELETE' }, + { name: 'GET', value: 'GET' }, + { name: 'HEAD', value: 'HEAD' }, + { name: 'PATCH', value: 'PATCH' }, + { name: 'POST', value: 'POST' }, + { name: 'PUT', value: 'PUT' }, + ], + default: 'GET', + description: 'The HTTP method to listen to', + }, + { + displayName: 'Path', + name: 'path', + type: 'string', + default: '', + placeholder: 'webhook', + required: true, + description: 'The path to listen to', + }, + { + displayName: 'Respond', + name: 'responseMode', + type: 'options', + options: [ + { + name: 'Immediately', + value: 'onReceived', + description: 'As soon as this node executes', + }, + { + name: 'When Last Node Finishes', + value: 'lastNode', + description: 'Returns data of the last-executed node', + }, + { + name: "Using 'Respond to Webhook' Node", + value: 'responseNode', + description: 'Response defined in that node', + }, + ], + default: 'onReceived', + description: 'When and how to respond to the webhook', + }, + { + displayName: + 'Insert a \'Respond to Webhook\' node to control when and how you respond. More details', + name: 'webhookNotice', + type: 'notice', + displayOptions: { show: { responseMode: ['responseNode'] } }, + default: '', + }, + { + displayName: 'Response Code', + name: 'responseCode', + type: 'number', + displayOptions: { hide: { responseMode: ['responseNode'] } }, + typeOptions: { minValue: 100, maxValue: 599 }, + default: 200, + description: 'The HTTP Response code to return', + }, + { + displayName: 'Response Data', + name: 'responseData', + type: 'options', + displayOptions: { show: { responseMode: ['lastNode'] } }, + options: [ + { + name: 'All Entries', + value: 'allEntries', + description: 'Returns all the entries of the last node. Always returns an array.', + }, + { + name: 'First Entry JSON', + value: 'firstEntryJson', + description: + 'Returns the JSON data of the first entry of the last node. Always returns a JSON object.', + }, + { + name: 'First Entry Binary', + value: 'firstEntryBinary', + description: + 'Returns the binary data of the first entry of the last node. Always returns a binary file.', + }, + { name: 'No Response Body', value: 'noData', description: 'Returns without a body' }, + ], + default: 'firstEntryJson', + description: + 'What data should be returned. If it should return all items as an array or only the first item as object.', + }, + { + displayName: 'Property Name', + name: 'responseBinaryPropertyName', + type: 'string', + required: true, + default: 'data', + displayOptions: { show: { responseData: ['firstEntryBinary'] } }, + description: 'Name of the binary property to return', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + options: [ + { + displayName: 'Binary Data', + name: 'binaryData', + type: 'boolean', + displayOptions: { show: { '/httpMethod': ['PATCH', 'PUT', 'POST'], '@version': [1] } }, + default: false, + description: 'Whether the webhook will receive binary data', + }, + { + displayName: 'Binary Property', + name: 'binaryPropertyName', + type: 'string', + default: 'data', + displayOptions: { show: { binaryData: [true], '@version': [1] } }, + description: + 'Name of the binary property to write the data of the received file to. If the data gets received via "Form-Data Multipart" it will be the prefix and a number starting with 0 will be attached to it.', + }, + { + displayName: 'Binary Property', + name: 'binaryPropertyName', + type: 'string', + default: 'data', + displayOptions: { hide: { '@version': [1] } }, + description: + 'Name of the binary property to write the data of the received file to, only relevant if binary data is received', + }, + { + displayName: 'Ignore Bots', + name: 'ignoreBots', + type: 'boolean', + default: false, + description: 'Whether to ignore requests from bots like link previewers and web crawlers', + }, + { + displayName: 'No Response Body', + name: 'noResponseBody', + type: 'boolean', + default: false, + description: 'Whether to send any body in the response', + displayOptions: { + hide: { rawBody: [true] }, + show: { '/responseMode': ['onReceived'] }, + }, + }, + { + displayName: 'Raw Body', + name: 'rawBody', + type: 'boolean', + displayOptions: { + show: { '@version': [1] }, + hide: { binaryData: [true], noResponseBody: [true] }, + }, + default: false, + description: 'Raw body (binary)', + }, + { + displayName: 'Raw Body', + name: 'rawBody', + type: 'boolean', + displayOptions: { hide: { noResponseBody: [true], '@version': [1] } }, + default: false, + description: 'Whether to return the raw body', + }, + { + displayName: 'Response Data', + name: 'responseData', + type: 'string', + displayOptions: { + show: { '/responseMode': ['onReceived'] }, + hide: { noResponseBody: [true] }, + }, + default: '', + placeholder: 'success', + description: 'Custom response data to send', + }, + { + displayName: 'Response Content-Type', + name: 'responseContentType', + type: 'string', + displayOptions: { + show: { '/responseData': ['firstEntryJson'], '/responseMode': ['lastNode'] }, + }, + default: '', + placeholder: 'application/xml', + description: + 'Set a custom content-type to return if another one as the "application/json" should be returned', + }, + { + displayName: 'Response Headers', + name: 'responseHeaders', + placeholder: 'Add Response Header', + description: 'Add headers to the webhook response', + type: 'fixedCollection', + typeOptions: { multipleValues: true }, + default: {}, + options: [ + { + name: 'entries', + displayName: 'Entries', + values: [ + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + description: 'Name of the header', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + description: 'Value of the header', + }, + ], + }, + ], + }, + { + displayName: 'Property Name', + name: 'responsePropertyName', + type: 'string', + displayOptions: { + show: { '/responseData': ['firstEntryJson'], '/responseMode': ['lastNode'] }, + }, + default: 'data', + description: 'Name of the property to return the data of instead of the whole JSON', + }, + { + displayName: 'Allowed Origins (CORS)', + name: 'allowedOrigins', + type: 'string', + default: '*', + description: + 'The origin(s) to allow cross-origin non-preflight requests from in a browser', + }, + ], + }, + ], + codex: { + categories: ['Development', 'Core Nodes'], + subcategories: { 'Core Nodes': ['Helpers'] }, + resources: { + primaryDocumentation: [ + { url: 'https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.webhook/' }, + ], + }, + alias: ['HTTP', 'API', 'Build', 'WH'], + }, + iconUrl: 'icons/n8n-nodes-base/dist/nodes/Webhook/webhook.svg', +} satisfies INodeTypeDescription; + +export const nodeTypesSet = { + '1': { + displayName: 'Set', + name: 'n8n-nodes-base.set', + icon: 'fa:pen', + group: ['input'], + description: 'Sets values on items and optionally remove other values', + defaultVersion: 3.2, + version: [1, 2], + defaults: { name: 'Set', color: '#0000FF' }, + inputs: ['main'], + outputs: ['main'], + properties: [ + { + displayName: 'Keep Only Set', + name: 'keepOnlySet', + type: 'boolean', + default: false, + description: + 'Whether only the values set on this node should be kept and all others removed', + }, + { + displayName: 'Values to Set', + name: 'values', + placeholder: 'Add Value', + type: 'fixedCollection', + typeOptions: { multipleValues: true, sortable: true }, + description: 'The value to set', + default: {}, + options: [ + { + name: 'boolean', + displayName: 'Boolean', + values: [ + { + displayName: 'Name', + name: 'name', + type: 'string', + requiresDataPath: 'single', + default: 'propertyName', + description: + 'Name of the property to write data to. Supports dot-notation. Example: "data.person[0].name"', + }, + { + displayName: 'Value', + name: 'value', + type: 'boolean', + default: false, + description: 'The boolean value to write in the property', + }, + ], + }, + { + name: 'number', + displayName: 'Number', + values: [ + { + displayName: 'Name', + name: 'name', + type: 'string', + default: 'propertyName', + requiresDataPath: 'single', + description: + 'Name of the property to write data to. Supports dot-notation. Example: "data.person[0].name"', + }, + { + displayName: 'Value', + name: 'value', + type: 'number', + default: 0, + description: 'The number value to write in the property', + }, + ], + }, + { + name: 'string', + displayName: 'String', + values: [ + { + displayName: 'Name', + name: 'name', + type: 'string', + default: 'propertyName', + requiresDataPath: 'single', + description: + 'Name of the property to write data to. Supports dot-notation. Example: "data.person[0].name"', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + description: 'The string value to write in the property', + }, + ], + }, + ], + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + options: [ + { + displayName: 'Dot Notation', + name: 'dotNotation', + type: 'boolean', + default: true, + description: + '

By default, dot-notation is used in property names. This means that "a.b" will set the property "b" underneath "a" so { "a": { "b": value} }.

If that is not intended this can be deactivated, it will then set { "a.b": value } instead.

.', + }, + ], + }, + ], + codex: { + categories: ['Core Nodes'], + subcategories: { 'Core Nodes': ['Data Transformation'] }, + resources: { + primaryDocumentation: [ + { url: 'https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.set/' }, + ], + }, + alias: ['Set', 'JSON', 'Filter', 'Transform', 'Map'], + }, + }, + '2': { + displayName: 'Set', + name: 'n8n-nodes-base.set', + icon: 'fa:pen', + group: ['input'], + description: 'Sets values on items and optionally remove other values', + defaultVersion: 3.2, + version: [1, 2], + defaults: { name: 'Set', color: '#0000FF' }, + inputs: ['main'], + outputs: ['main'], + properties: [ + { + displayName: 'Keep Only Set', + name: 'keepOnlySet', + type: 'boolean', + default: false, + description: + 'Whether only the values set on this node should be kept and all others removed', + }, + { + displayName: 'Values to Set', + name: 'values', + placeholder: 'Add Value', + type: 'fixedCollection', + typeOptions: { multipleValues: true, sortable: true }, + description: 'The value to set', + default: {}, + options: [ + { + name: 'boolean', + displayName: 'Boolean', + values: [ + { + displayName: 'Name', + name: 'name', + type: 'string', + requiresDataPath: 'single', + default: 'propertyName', + description: + 'Name of the property to write data to. Supports dot-notation. Example: "data.person[0].name"', + }, + { + displayName: 'Value', + name: 'value', + type: 'boolean', + default: false, + description: 'The boolean value to write in the property', + }, + ], + }, + { + name: 'number', + displayName: 'Number', + values: [ + { + displayName: 'Name', + name: 'name', + type: 'string', + default: 'propertyName', + requiresDataPath: 'single', + description: + 'Name of the property to write data to. Supports dot-notation. Example: "data.person[0].name"', + }, + { + displayName: 'Value', + name: 'value', + type: 'number', + default: 0, + description: 'The number value to write in the property', + }, + ], + }, + { + name: 'string', + displayName: 'String', + values: [ + { + displayName: 'Name', + name: 'name', + type: 'string', + default: 'propertyName', + requiresDataPath: 'single', + description: + 'Name of the property to write data to. Supports dot-notation. Example: "data.person[0].name"', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + description: 'The string value to write in the property', + }, + ], + }, + ], + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + options: [ + { + displayName: 'Dot Notation', + name: 'dotNotation', + type: 'boolean', + default: true, + description: + '

By default, dot-notation is used in property names. This means that "a.b" will set the property "b" underneath "a" so { "a": { "b": value} }.

If that is not intended this can be deactivated, it will then set { "a.b": value } instead.

.', + }, + ], + }, + ], + codex: { + categories: ['Core Nodes'], + subcategories: { 'Core Nodes': ['Data Transformation'] }, + resources: { + primaryDocumentation: [ + { url: 'https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.set/' }, + ], + }, + alias: ['Set', 'JSON', 'Filter', 'Transform', 'Map'], + }, + }, + '3': { + displayName: 'Edit Fields (Set)', + name: 'n8n-nodes-base.set', + icon: 'fa:pen', + group: ['input'], + description: 'Modify, add, or remove item fields', + defaultVersion: 3.2, + version: [3, 3.1, 3.2], + subtitle: '={{$parameter["mode"]}}', + defaults: { name: 'Edit Fields', color: '#0000FF' }, + inputs: ['main'], + outputs: ['main'], + properties: [ + { + displayName: 'Mode', + name: 'mode', + type: 'options', + noDataExpression: true, + options: [ + { + name: 'Manual Mapping', + value: 'manual', + description: 'Edit item fields one by one', + action: 'Edit item fields one by one', + }, + { + name: 'JSON Output', + value: 'raw', + description: 'Customize item output with JSON', + action: 'Customize item output with JSON', + }, + ], + default: 'manual', + }, + { + displayName: 'Duplicate Item', + name: 'duplicateItem', + type: 'boolean', + default: false, + isNodeSetting: true, + }, + { + displayName: 'Duplicate Item Count', + name: 'duplicateCount', + type: 'number', + default: 0, + typeOptions: { minValue: 0 }, + description: + 'How many times the item should be duplicated, mainly used for testing and debugging', + isNodeSetting: true, + displayOptions: { show: { duplicateItem: [true] } }, + }, + { + displayName: + 'Item duplication is set in the node settings. This option will be ignored when the workflow runs automatically.', + name: 'duplicateWarning', + type: 'notice', + default: '', + displayOptions: { show: { duplicateItem: [true] } }, + }, + { + displayName: 'JSON Output', + name: 'jsonOutput', + type: 'json', + typeOptions: { rows: 5 }, + default: '{\n "my_field_1": "value",\n "my_field_2": 1\n}', + validateType: 'object', + ignoreValidationDuringExecution: true, + displayOptions: { show: { mode: ['raw'] } }, + }, + { + displayName: 'Fields to Set', + name: 'fields', + placeholder: 'Add Field', + type: 'fixedCollection', + description: 'Edit existing fields or add new ones to modify the output data', + typeOptions: { multipleValues: true, sortable: true }, + default: {}, + options: [ + { + name: 'values', + displayName: 'Values', + values: [ + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + placeholder: 'e.g. fieldName', + description: + 'Name of the field to set the value of. Supports dot-notation. Example: data.person[0].name.', + requiresDataPath: 'single', + }, + { + displayName: 'Type', + name: 'type', + type: 'options', + description: 'The field value type', + options: [ + { name: 'String', value: 'stringValue' }, + { name: 'Number', value: 'numberValue' }, + { name: 'Boolean', value: 'booleanValue' }, + { name: 'Array', value: 'arrayValue' }, + { name: 'Object', value: 'objectValue' }, + ], + default: 'stringValue', + }, + { + displayName: 'Value', + name: 'stringValue', + type: 'string', + default: '', + displayOptions: { show: { type: ['stringValue'] } }, + validateType: 'string', + ignoreValidationDuringExecution: true, + }, + { + displayName: 'Value', + name: 'numberValue', + type: 'string', + default: '', + displayOptions: { show: { type: ['numberValue'] } }, + validateType: 'number', + ignoreValidationDuringExecution: true, + }, + { + displayName: 'Value', + name: 'booleanValue', + type: 'options', + default: 'true', + options: [ + { name: 'True', value: 'true' }, + { name: 'False', value: 'false' }, + ], + displayOptions: { show: { type: ['booleanValue'] } }, + validateType: 'boolean', + ignoreValidationDuringExecution: true, + }, + { + displayName: 'Value', + name: 'arrayValue', + type: 'string', + default: '', + placeholder: 'e.g. [ arrayItem1, arrayItem2, arrayItem3 ]', + displayOptions: { show: { type: ['arrayValue'] } }, + validateType: 'array', + ignoreValidationDuringExecution: true, + }, + { + displayName: 'Value', + name: 'objectValue', + type: 'json', + default: '={}', + typeOptions: { rows: 2 }, + displayOptions: { show: { type: ['objectValue'] } }, + validateType: 'object', + ignoreValidationDuringExecution: true, + }, + ], + }, + ], + displayOptions: { show: { mode: ['manual'] } }, + }, + { + displayName: 'Include in Output', + name: 'include', + type: 'options', + description: 'How to select the fields you want to include in your output items', + default: 'all', + options: [ + { + name: 'All Input Fields', + value: 'all', + description: 'Also include all unchanged fields from the input', + }, + { + name: 'No Input Fields', + value: 'none', + description: 'Include only the fields specified above', + }, + { + name: 'Selected Input Fields', + value: 'selected', + description: 'Also include the fields listed in the parameter “Fields to Include”', + }, + { + name: 'All Input Fields Except', + value: 'except', + description: 'Exclude the fields listed in the parameter “Fields to Exclude”', + }, + ], + }, + { + displayName: 'Fields to Include', + name: 'includeFields', + type: 'string', + default: '', + placeholder: 'e.g. fieldToInclude1,fieldToInclude2', + description: + 'Comma-separated list of the field names you want to include in the output. You can drag the selected fields from the input panel.', + requiresDataPath: 'multiple', + displayOptions: { show: { include: ['selected'] } }, + }, + { + displayName: 'Fields to Exclude', + name: 'excludeFields', + type: 'string', + default: '', + placeholder: 'e.g. fieldToExclude1,fieldToExclude2', + description: + 'Comma-separated list of the field names you want to exclude from the output. You can drag the selected fields from the input panel.', + requiresDataPath: 'multiple', + displayOptions: { show: { include: ['except'] } }, + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + options: [ + { + displayName: 'Include Binary Data', + name: 'includeBinary', + type: 'boolean', + default: true, + description: 'Whether binary data should be included if present in the input item', + }, + { + displayName: 'Ignore Type Conversion Errors', + name: 'ignoreConversionErrors', + type: 'boolean', + default: false, + description: + 'Whether to ignore field type errors and apply a less strict type conversion', + displayOptions: { show: { '/mode': ['manual'] } }, + }, + { + displayName: 'Support Dot Notation', + name: 'dotNotation', + type: 'boolean', + default: true, + description: + 'By default, dot-notation is used in property names. This means that "a.b" will set the property "b" underneath "a" so { "a": { "b": value} }. If that is not intended this can be deactivated, it will then set { "a.b": value } instead.', + }, + ], + }, + ], + codex: { + categories: ['Core Nodes'], + subcategories: { 'Core Nodes': ['Data Transformation'] }, + resources: { + primaryDocumentation: [ + { url: 'https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.set/' }, + ], + }, + alias: ['Set', 'JSON', 'Filter', 'Transform', 'Map'], + }, + }, + '3.1': { + displayName: 'Edit Fields (Set)', + name: 'n8n-nodes-base.set', + icon: 'fa:pen', + group: ['input'], + description: 'Modify, add, or remove item fields', + defaultVersion: 3.2, + version: [3, 3.1, 3.2], + subtitle: '={{$parameter["mode"]}}', + defaults: { name: 'Edit Fields', color: '#0000FF' }, + inputs: ['main'], + outputs: ['main'], + properties: [ + { + displayName: 'Mode', + name: 'mode', + type: 'options', + noDataExpression: true, + options: [ + { + name: 'Manual Mapping', + value: 'manual', + description: 'Edit item fields one by one', + action: 'Edit item fields one by one', + }, + { + name: 'JSON Output', + value: 'raw', + description: 'Customize item output with JSON', + action: 'Customize item output with JSON', + }, + ], + default: 'manual', + }, + { + displayName: 'Duplicate Item', + name: 'duplicateItem', + type: 'boolean', + default: false, + isNodeSetting: true, + }, + { + displayName: 'Duplicate Item Count', + name: 'duplicateCount', + type: 'number', + default: 0, + typeOptions: { minValue: 0 }, + description: + 'How many times the item should be duplicated, mainly used for testing and debugging', + isNodeSetting: true, + displayOptions: { show: { duplicateItem: [true] } }, + }, + { + displayName: + 'Item duplication is set in the node settings. This option will be ignored when the workflow runs automatically.', + name: 'duplicateWarning', + type: 'notice', + default: '', + displayOptions: { show: { duplicateItem: [true] } }, + }, + { + displayName: 'JSON Output', + name: 'jsonOutput', + type: 'json', + typeOptions: { rows: 5 }, + default: '{\n "my_field_1": "value",\n "my_field_2": 1\n}', + validateType: 'object', + ignoreValidationDuringExecution: true, + displayOptions: { show: { mode: ['raw'] } }, + }, + { + displayName: 'Fields to Set', + name: 'fields', + placeholder: 'Add Field', + type: 'fixedCollection', + description: 'Edit existing fields or add new ones to modify the output data', + typeOptions: { multipleValues: true, sortable: true }, + default: {}, + options: [ + { + name: 'values', + displayName: 'Values', + values: [ + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + placeholder: 'e.g. fieldName', + description: + 'Name of the field to set the value of. Supports dot-notation. Example: data.person[0].name.', + requiresDataPath: 'single', + }, + { + displayName: 'Type', + name: 'type', + type: 'options', + description: 'The field value type', + options: [ + { name: 'String', value: 'stringValue' }, + { name: 'Number', value: 'numberValue' }, + { name: 'Boolean', value: 'booleanValue' }, + { name: 'Array', value: 'arrayValue' }, + { name: 'Object', value: 'objectValue' }, + ], + default: 'stringValue', + }, + { + displayName: 'Value', + name: 'stringValue', + type: 'string', + default: '', + displayOptions: { show: { type: ['stringValue'] } }, + validateType: 'string', + ignoreValidationDuringExecution: true, + }, + { + displayName: 'Value', + name: 'numberValue', + type: 'string', + default: '', + displayOptions: { show: { type: ['numberValue'] } }, + validateType: 'number', + ignoreValidationDuringExecution: true, + }, + { + displayName: 'Value', + name: 'booleanValue', + type: 'options', + default: 'true', + options: [ + { name: 'True', value: 'true' }, + { name: 'False', value: 'false' }, + ], + displayOptions: { show: { type: ['booleanValue'] } }, + validateType: 'boolean', + ignoreValidationDuringExecution: true, + }, + { + displayName: 'Value', + name: 'arrayValue', + type: 'string', + default: '', + placeholder: 'e.g. [ arrayItem1, arrayItem2, arrayItem3 ]', + displayOptions: { show: { type: ['arrayValue'] } }, + validateType: 'array', + ignoreValidationDuringExecution: true, + }, + { + displayName: 'Value', + name: 'objectValue', + type: 'json', + default: '={}', + typeOptions: { rows: 2 }, + displayOptions: { show: { type: ['objectValue'] } }, + validateType: 'object', + ignoreValidationDuringExecution: true, + }, + ], + }, + ], + displayOptions: { show: { mode: ['manual'] } }, + }, + { + displayName: 'Include in Output', + name: 'include', + type: 'options', + description: 'How to select the fields you want to include in your output items', + default: 'all', + options: [ + { + name: 'All Input Fields', + value: 'all', + description: 'Also include all unchanged fields from the input', + }, + { + name: 'No Input Fields', + value: 'none', + description: 'Include only the fields specified above', + }, + { + name: 'Selected Input Fields', + value: 'selected', + description: 'Also include the fields listed in the parameter “Fields to Include”', + }, + { + name: 'All Input Fields Except', + value: 'except', + description: 'Exclude the fields listed in the parameter “Fields to Exclude”', + }, + ], + }, + { + displayName: 'Fields to Include', + name: 'includeFields', + type: 'string', + default: '', + placeholder: 'e.g. fieldToInclude1,fieldToInclude2', + description: + 'Comma-separated list of the field names you want to include in the output. You can drag the selected fields from the input panel.', + requiresDataPath: 'multiple', + displayOptions: { show: { include: ['selected'] } }, + }, + { + displayName: 'Fields to Exclude', + name: 'excludeFields', + type: 'string', + default: '', + placeholder: 'e.g. fieldToExclude1,fieldToExclude2', + description: + 'Comma-separated list of the field names you want to exclude from the output. You can drag the selected fields from the input panel.', + requiresDataPath: 'multiple', + displayOptions: { show: { include: ['except'] } }, + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + options: [ + { + displayName: 'Include Binary Data', + name: 'includeBinary', + type: 'boolean', + default: true, + description: 'Whether binary data should be included if present in the input item', + }, + { + displayName: 'Ignore Type Conversion Errors', + name: 'ignoreConversionErrors', + type: 'boolean', + default: false, + description: + 'Whether to ignore field type errors and apply a less strict type conversion', + displayOptions: { show: { '/mode': ['manual'] } }, + }, + { + displayName: 'Support Dot Notation', + name: 'dotNotation', + type: 'boolean', + default: true, + description: + 'By default, dot-notation is used in property names. This means that "a.b" will set the property "b" underneath "a" so { "a": { "b": value} }. If that is not intended this can be deactivated, it will then set { "a.b": value } instead.', + }, + ], + }, + ], + codex: { + categories: ['Core Nodes'], + subcategories: { 'Core Nodes': ['Data Transformation'] }, + resources: { + primaryDocumentation: [ + { url: 'https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.set/' }, + ], + }, + alias: ['Set', 'JSON', 'Filter', 'Transform', 'Map'], + }, + }, + '3.2': { + displayName: 'Edit Fields (Set)', + name: 'n8n-nodes-base.set', + icon: 'fa:pen', + group: ['input'], + description: 'Modify, add, or remove item fields', + defaultVersion: 3.2, + version: [3, 3.1, 3.2], + subtitle: '={{$parameter["mode"]}}', + defaults: { name: 'Edit Fields', color: '#0000FF' }, + inputs: ['main'], + outputs: ['main'], + properties: [ + { + displayName: 'Mode', + name: 'mode', + type: 'options', + noDataExpression: true, + options: [ + { + name: 'Manual Mapping', + value: 'manual', + description: 'Edit item fields one by one', + action: 'Edit item fields one by one', + }, + { + name: 'JSON Output', + value: 'raw', + description: 'Customize item output with JSON', + action: 'Customize item output with JSON', + }, + ], + default: 'manual', + }, + { + displayName: 'Duplicate Item', + name: 'duplicateItem', + type: 'boolean', + default: false, + isNodeSetting: true, + }, + { + displayName: 'Duplicate Item Count', + name: 'duplicateCount', + type: 'number', + default: 0, + typeOptions: { minValue: 0 }, + description: + 'How many times the item should be duplicated, mainly used for testing and debugging', + isNodeSetting: true, + displayOptions: { show: { duplicateItem: [true] } }, + }, + { + displayName: + 'Item duplication is set in the node settings. This option will be ignored when the workflow runs automatically.', + name: 'duplicateWarning', + type: 'notice', + default: '', + displayOptions: { show: { duplicateItem: [true] } }, + }, + { + displayName: 'JSON Output', + name: 'jsonOutput', + type: 'json', + typeOptions: { rows: 5 }, + default: '{\n "my_field_1": "value",\n "my_field_2": 1\n}', + validateType: 'object', + ignoreValidationDuringExecution: true, + displayOptions: { show: { mode: ['raw'] } }, + }, + { + displayName: 'Fields to Set', + name: 'fields', + placeholder: 'Add Field', + type: 'fixedCollection', + description: 'Edit existing fields or add new ones to modify the output data', + typeOptions: { multipleValues: true, sortable: true }, + default: {}, + options: [ + { + name: 'values', + displayName: 'Values', + values: [ + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + placeholder: 'e.g. fieldName', + description: + 'Name of the field to set the value of. Supports dot-notation. Example: data.person[0].name.', + requiresDataPath: 'single', + }, + { + displayName: 'Type', + name: 'type', + type: 'options', + description: 'The field value type', + options: [ + { name: 'String', value: 'stringValue' }, + { name: 'Number', value: 'numberValue' }, + { name: 'Boolean', value: 'booleanValue' }, + { name: 'Array', value: 'arrayValue' }, + { name: 'Object', value: 'objectValue' }, + ], + default: 'stringValue', + }, + { + displayName: 'Value', + name: 'stringValue', + type: 'string', + default: '', + displayOptions: { show: { type: ['stringValue'] } }, + validateType: 'string', + ignoreValidationDuringExecution: true, + }, + { + displayName: 'Value', + name: 'numberValue', + type: 'string', + default: '', + displayOptions: { show: { type: ['numberValue'] } }, + validateType: 'number', + ignoreValidationDuringExecution: true, + }, + { + displayName: 'Value', + name: 'booleanValue', + type: 'options', + default: 'true', + options: [ + { name: 'True', value: 'true' }, + { name: 'False', value: 'false' }, + ], + displayOptions: { show: { type: ['booleanValue'] } }, + validateType: 'boolean', + ignoreValidationDuringExecution: true, + }, + { + displayName: 'Value', + name: 'arrayValue', + type: 'string', + default: '', + placeholder: 'e.g. [ arrayItem1, arrayItem2, arrayItem3 ]', + displayOptions: { show: { type: ['arrayValue'] } }, + validateType: 'array', + ignoreValidationDuringExecution: true, + }, + { + displayName: 'Value', + name: 'objectValue', + type: 'json', + default: '={}', + typeOptions: { rows: 2 }, + displayOptions: { show: { type: ['objectValue'] } }, + validateType: 'object', + ignoreValidationDuringExecution: true, + }, + ], + }, + ], + displayOptions: { show: { mode: ['manual'] } }, + }, + { + displayName: 'Include in Output', + name: 'include', + type: 'options', + description: 'How to select the fields you want to include in your output items', + default: 'all', + options: [ + { + name: 'All Input Fields', + value: 'all', + description: 'Also include all unchanged fields from the input', + }, + { + name: 'No Input Fields', + value: 'none', + description: 'Include only the fields specified above', + }, + { + name: 'Selected Input Fields', + value: 'selected', + description: 'Also include the fields listed in the parameter “Fields to Include”', + }, + { + name: 'All Input Fields Except', + value: 'except', + description: 'Exclude the fields listed in the parameter “Fields to Exclude”', + }, + ], + }, + { + displayName: 'Fields to Include', + name: 'includeFields', + type: 'string', + default: '', + placeholder: 'e.g. fieldToInclude1,fieldToInclude2', + description: + 'Comma-separated list of the field names you want to include in the output. You can drag the selected fields from the input panel.', + requiresDataPath: 'multiple', + displayOptions: { show: { include: ['selected'] } }, + }, + { + displayName: 'Fields to Exclude', + name: 'excludeFields', + type: 'string', + default: '', + placeholder: 'e.g. fieldToExclude1,fieldToExclude2', + description: + 'Comma-separated list of the field names you want to exclude from the output. You can drag the selected fields from the input panel.', + requiresDataPath: 'multiple', + displayOptions: { show: { include: ['except'] } }, + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + options: [ + { + displayName: 'Include Binary Data', + name: 'includeBinary', + type: 'boolean', + default: true, + description: 'Whether binary data should be included if present in the input item', + }, + { + displayName: 'Ignore Type Conversion Errors', + name: 'ignoreConversionErrors', + type: 'boolean', + default: false, + description: + 'Whether to ignore field type errors and apply a less strict type conversion', + displayOptions: { show: { '/mode': ['manual'] } }, + }, + { + displayName: 'Support Dot Notation', + name: 'dotNotation', + type: 'boolean', + default: true, + description: + 'By default, dot-notation is used in property names. This means that "a.b" will set the property "b" underneath "a" so { "a": { "b": value} }. If that is not intended this can be deactivated, it will then set { "a.b": value } instead.', + }, + ], + }, + ], + codex: { + categories: ['Core Nodes'], + subcategories: { 'Core Nodes': ['Data Transformation'] }, + resources: { + primaryDocumentation: [ + { url: 'https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.set/' }, + ], + }, + alias: ['Set', 'JSON', 'Filter', 'Transform', 'Map'], + }, + }, +} satisfies { [version: string]: INodeTypeDescription }; diff --git a/packages/editor-ui/src/utils/testData/templateTestData.ts b/packages/editor-ui/src/utils/testData/templateTestData.ts index 4243d1871b913..6a1e156063756 100644 --- a/packages/editor-ui/src/utils/testData/templateTestData.ts +++ b/packages/editor-ui/src/utils/testData/templateTestData.ts @@ -290,3 +290,148 @@ export const fullSaveEmailAttachmentsToNextCloudTemplate = { image: [], full: true, } satisfies ITemplatesWorkflowFull; + +/** Template that doesn't contain nodes requiring credentials */ +export const fullCreateApiEndpointTemplate = { + id: 1750, + name: 'Creating an API endpoint', + views: 13265, + recentViews: 9899, + totalViews: 13265, + createdAt: '2022-07-06T14:45:19.659Z', + description: + '**Task:**\nCreate a simple API endpoint using the Webhook and Respond to Webhook nodes\n\n**Why:**\nYou can prototype or replace a backend process with a single workflow\n\n**Main use cases:**\nReplace backend logic with a workflow', + workflow: { + meta: { instanceId: '8c8c5237b8e37b006a7adce87f4369350c58e41f3ca9de16196d3197f69eabcd' }, + nodes: [ + { + id: 'f80aceed-b676-42aa-bf25-f7a44408b1bc', + name: 'Webhook', + type: 'n8n-nodes-base.webhook', + position: [375, 115], + webhookId: '6f7b288e-1efe-4504-a6fd-660931327269', + parameters: { + path: '6f7b288e-1efe-4504-a6fd-660931327269', + options: {}, + responseMode: 'responseNode', + }, + typeVersion: 1, + }, + { + id: '3b9ec913-0bbe-4906-bf8e-da352b556655', + name: 'Note1', + type: 'n8n-nodes-base.stickyNote', + position: [355, -25], + parameters: { + width: 600, + height: 280, + content: + '## Create a simple API endpoint\n\nIn this workflow we show how to create a simple API endpoint with `Webhook` and `Respond to Webhook` nodes\n\n', + }, + typeVersion: 1, + }, + { + id: '9c36dae5-0700-450c-9739-e9f3eff31bfe', + name: 'Respond to Webhook', + type: 'n8n-nodes-base.respondToWebhook', + position: [815, 115], + parameters: { + options: {}, + respondWith: 'text', + responseBody: + '=The URL of the Google search query for the term "{{$node["Webhook"].json["query"]["first_name"]}} {{$node["Webhook"].json["query"]["last_name"]}}" is: {{$json["product"]}}', + }, + typeVersion: 1, + }, + { + id: '5a228fcb-78b9-4a28-95d2-d7c9fdf1d4ea', + name: 'Create URL string', + type: 'n8n-nodes-base.set', + position: [595, 115], + parameters: { + values: { + string: [ + { + name: 'product', + value: + '=https://www.google.com/search?q={{$json["query"]["first_name"]}}+{{$json["query"]["last_name"]}}', + }, + ], + }, + options: {}, + keepOnlySet: true, + }, + typeVersion: 1, + }, + { + id: 'e7971820-45a8-4dc8-ba4c-b3220d65307a', + name: 'Note3', + type: 'n8n-nodes-base.stickyNote', + position: [355, 275], + parameters: { + width: 600, + height: 220, + content: + '### How to use it\n1. Execute the workflow so that the webhook starts listening\n2. Make a test request by pasting, **in a new browser tab**, the test URL from the `Webhook` node and appending the following test at the end `?first_name=bob&last_name=dylan`\n\nYou will receive the following output in the new tab `The URL of the Google search query for the term "bob dylan" is: https://www.google.com/search?q=bob+dylan`\n\n', + }, + typeVersion: 1, + }, + ], + connections: { + Webhook: { main: [[{ node: 'Create URL string', type: 'main', index: 0 }]] }, + 'Create URL string': { main: [[{ node: 'Respond to Webhook', type: 'main', index: 0 }]] }, + }, + }, + lastUpdatedBy: 1, + workflowInfo: null, + user: { username: 'jon-n8n' }, + nodes: [ + { + id: 38, + icon: 'fa:pen', + name: 'n8n-nodes-base.set', + defaults: { name: 'Edit Fields', color: '#0000FF' }, + iconData: { icon: 'pen', type: 'icon' }, + categories: [{ id: 9, name: 'Core Nodes' }], + displayName: 'Edit Fields (Set)', + typeVersion: 3, + }, + { + id: 47, + icon: 'file:webhook.svg', + name: 'n8n-nodes-base.webhook', + defaults: { name: 'Webhook' }, + iconData: { + type: 'file', + fileBuffer: + 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciICB2aWV3Qm94PSIwIDAgNDggNDgiIHdpZHRoPSI0OHB4IiBoZWlnaHQ9IjQ4cHgiPjxwYXRoIGZpbGw9IiMzNzQ3NGYiIGQ9Ik0zNSwzN2MtMi4yLDAtNC0xLjgtNC00czEuOC00LDQtNHM0LDEuOCw0LDRTMzcuMiwzNywzNSwzN3oiLz48cGF0aCBmaWxsPSIjMzc0NzRmIiBkPSJNMzUsNDNjLTMsMC01LjktMS40LTcuOC0zLjdsMy4xLTIuNWMxLjEsMS40LDIuOSwyLjMsNC43LDIuM2MzLjMsMCw2LTIuNyw2LTZzLTIuNy02LTYtNiBjLTEsMC0yLDAuMy0yLjksMC43bC0xLjcsMUwyMy4zLDE2bDMuNS0xLjlsNS4zLDkuNGMxLTAuMywyLTAuNSwzLTAuNWM1LjUsMCwxMCw0LjUsMTAsMTBTNDAuNSw0MywzNSw0M3oiLz48cGF0aCBmaWxsPSIjMzc0NzRmIiBkPSJNMTQsNDNDOC41LDQzLDQsMzguNSw0LDMzYzAtNC42LDMuMS04LjUsNy41LTkuN2wxLDMuOUM5LjksMjcuOSw4LDMwLjMsOCwzM2MwLDMuMywyLjcsNiw2LDYgczYtMi43LDYtNnYtMmgxNXY0SDIzLjhDMjIuOSwzOS42LDE4LjgsNDMsMTQsNDN6Ii8+PHBhdGggZmlsbD0iI2U5MWU2MyIgZD0iTTE0LDM3Yy0yLjIsMC00LTEuOC00LTRzMS44LTQsNC00czQsMS44LDQsNFMxNi4yLDM3LDE0LDM3eiIvPjxwYXRoIGZpbGw9IiMzNzQ3NGYiIGQ9Ik0yNSwxOWMtMi4yLDAtNC0xLjgtNC00czEuOC00LDQtNHM0LDEuOCw0LDRTMjcuMiwxOSwyNSwxOXoiLz48cGF0aCBmaWxsPSIjZTkxZTYzIiBkPSJNMTUuNywzNEwxMi4zLDMybDUuOS05LjdjLTItMS45LTMuMi00LjUtMy4yLTcuM2MwLTUuNSw0LjUtMTAsMTAtMTBjNS41LDAsMTAsNC41LDEwLDEwIGMwLDAuOS0wLjEsMS43LTAuMywyLjVsLTMuOS0xYzAuMS0wLjUsMC4yLTEsMC4yLTEuNWMwLTMuMy0yLjctNi02LTZzLTYsMi43LTYsNmMwLDIuMSwxLjEsNCwyLjksNS4xbDEuNywxTDE1LjcsMzR6Ii8+PC9zdmc+Cg==', + }, + categories: [ + { id: 5, name: 'Development' }, + { id: 9, name: 'Core Nodes' }, + ], + displayName: 'Webhook', + typeVersion: 1, + }, + { + id: 535, + icon: 'file:webhook.svg', + name: 'n8n-nodes-base.respondToWebhook', + defaults: { name: 'Respond to Webhook' }, + iconData: { + type: 'file', + fileBuffer: + 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciICB2aWV3Qm94PSIwIDAgNDggNDgiIHdpZHRoPSI0OHB4IiBoZWlnaHQ9IjQ4cHgiPjxwYXRoIGZpbGw9IiMzNzQ3NGYiIGQ9Ik0zNSwzN2MtMi4yLDAtNC0xLjgtNC00czEuOC00LDQtNHM0LDEuOCw0LDRTMzcuMiwzNywzNSwzN3oiLz48cGF0aCBmaWxsPSIjMzc0NzRmIiBkPSJNMzUsNDNjLTMsMC01LjktMS40LTcuOC0zLjdsMy4xLTIuNWMxLjEsMS40LDIuOSwyLjMsNC43LDIuM2MzLjMsMCw2LTIuNyw2LTZzLTIuNy02LTYtNiBjLTEsMC0yLDAuMy0yLjksMC43bC0xLjcsMUwyMy4zLDE2bDMuNS0xLjlsNS4zLDkuNGMxLTAuMywyLTAuNSwzLTAuNWM1LjUsMCwxMCw0LjUsMTAsMTBTNDAuNSw0MywzNSw0M3oiLz48cGF0aCBmaWxsPSIjMzc0NzRmIiBkPSJNMTQsNDNDOC41LDQzLDQsMzguNSw0LDMzYzAtNC42LDMuMS04LjUsNy41LTkuN2wxLDMuOUM5LjksMjcuOSw4LDMwLjMsOCwzM2MwLDMuMywyLjcsNiw2LDYgczYtMi43LDYtNnYtMmgxNXY0SDIzLjhDMjIuOSwzOS42LDE4LjgsNDMsMTQsNDN6Ii8+PHBhdGggZmlsbD0iI2U5MWU2MyIgZD0iTTE0LDM3Yy0yLjIsMC00LTEuOC00LTRzMS44LTQsNC00czQsMS44LDQsNFMxNi4yLDM3LDE0LDM3eiIvPjxwYXRoIGZpbGw9IiMzNzQ3NGYiIGQ9Ik0yNSwxOWMtMi4yLDAtNC0xLjgtNC00czEuOC00LDQtNHM0LDEuOCw0LDRTMjcuMiwxOSwyNSwxOXoiLz48cGF0aCBmaWxsPSIjZTkxZTYzIiBkPSJNMTUuNywzNEwxMi4zLDMybDUuOS05LjdjLTItMS45LTMuMi00LjUtMy4yLTcuM2MwLTUuNSw0LjUtMTAsMTAtMTBjNS41LDAsMTAsNC41LDEwLDEwIGMwLDAuOS0wLjEsMS43LTAuMywyLjVsLTMuOS0xYzAuMS0wLjUsMC4yLTEsMC4yLTEuNWMwLTMuMy0yLjctNi02LTZzLTYsMi43LTYsNmMwLDIuMSwxLjEsNCwyLjksNS4xbDEuNywxTDE1LjcsMzR6Ii8+PC9zdmc+Cg==', + }, + categories: [ + { id: 7, name: 'Utility' }, + { id: 9, name: 'Core Nodes' }, + ], + displayName: 'Respond to Webhook', + typeVersion: 1, + }, + ], + categories: [{ id: 20, name: 'Building Blocks' }], + image: [], + full: true, +} satisfies ITemplatesWorkflowFull; diff --git a/packages/editor-ui/src/views/SetupWorkflowFromTemplateView/setupTemplate.store.ts b/packages/editor-ui/src/views/SetupWorkflowFromTemplateView/setupTemplate.store.ts index 634f0ff3d6a3e..7c72c1c4485b9 100644 --- a/packages/editor-ui/src/views/SetupWorkflowFromTemplateView/setupTemplate.store.ts +++ b/packages/editor-ui/src/views/SetupWorkflowFromTemplateView/setupTemplate.store.ts @@ -8,28 +8,21 @@ 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 { - INodeCredentialDescription, - INodeCredentialsDetails, - INodeTypeDescription, -} from 'n8n-workflow'; -import type { - ICredentialsResponse, - INodeUi, - ITemplatesWorkflowFull, - IWorkflowTemplateNode, -} from '@/Interface'; +import type { INodeCredentialsDetails, INodeTypeDescription } from 'n8n-workflow'; +import type { ICredentialsResponse, INodeUi, IWorkflowTemplateNode } from '@/Interface'; import { VIEWS } from '@/constants'; import { createWorkflowFromTemplate } from '@/utils/templates/templateActions'; -import type { TemplateCredentialKey } from '@/utils/templates/templateTransforms'; +import type { + TemplateCredentialKey, + TemplateNodeWithRequiredCredential, +} from '@/utils/templates/templateTransforms'; import { + getNodesRequiringCredentials, 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; @@ -64,35 +57,8 @@ 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, -): TemplateNodeWithRequiredCredential[] => { - if (!template) { - return []; - } - - const nodesWithCredentials: TemplateNodeWithRequiredCredential[] = template.workflow.nodes - .map((node) => ({ - node, - requiredCredentials: getNodeTypeDisplayableCredentials(nodeTypeProvider, node), - })) - .filter(({ requiredCredentials }) => requiredCredentials.length > 0); - - return nodesWithCredentials; -}; - export const groupNodeCredentialsByKey = ( nodeWithRequiredCredentials: TemplateNodeWithRequiredCredential[], ) => { @@ -378,6 +344,16 @@ export const useSetupTemplateStore = defineStore('setupTemplate', () => { completed: true, }); + const telemetryPayload = { + source: 'workflow', + template_id: template.value.id, + wf_template_repo_session_id: templatesStore.currentSessionId, + }; + + telemetry.track('User inserted workflow template', telemetryPayload, { + withPostHog: true, + }); + // Replace the URL so back button doesn't come back to this setup view await router.replace({ name: VIEWS.WORKFLOW, diff --git a/packages/editor-ui/src/views/TemplatesCollectionView.vue b/packages/editor-ui/src/views/TemplatesCollectionView.vue index f9e69e07cca25..ad3fb6e56bf0d 100644 --- a/packages/editor-ui/src/views/TemplatesCollectionView.vue +++ b/packages/editor-ui/src/views/TemplatesCollectionView.vue @@ -66,11 +66,12 @@ import type { } from '@/Interface'; import { setPageTitle } from '@/utils/htmlUtils'; -import { TEMPLATE_CREDENTIAL_SETUP_EXPERIMENT, VIEWS } from '@/constants'; +import { VIEWS } from '@/constants'; import { useTemplatesStore } from '@/stores/templates.store'; import { usePostHog } from '@/stores/posthog.store'; -import { openTemplateCredentialSetup } from '@/utils/templates/templateActions'; +import { useTemplateWorkflow } from '@/utils/templates/templateActions'; import { useExternalHooks } from '@/composables/useExternalHooks'; +import { useNodeTypesStore } from '@/stores/nodeTypes.store'; export default defineComponent({ name: 'TemplatesCollectionView', @@ -152,23 +153,15 @@ export default defineComponent({ this.navigateTo(event, VIEWS.TEMPLATE, id); }, async onUseWorkflow({ event, id }: { event: MouseEvent; id: string }) { - if (this.posthogStore.isFeatureEnabled(TEMPLATE_CREDENTIAL_SETUP_EXPERIMENT)) { - const telemetryPayload = { - template_id: id, - wf_template_repo_session_id: this.templatesStore.currentSessionId, - source: 'collection', - }; - await this.externalHooks.run('templatesCollectionView.onUseWorkflow', telemetryPayload); - this.$telemetry.track('User inserted workflow template', telemetryPayload, { - withPostHog: true, - }); - } - - await openTemplateCredentialSetup({ + await useTemplateWorkflow({ posthogStore: this.posthogStore, router: this.$router, templateId: id, inNewBrowserTab: event.metaKey || event.ctrlKey, + templatesStore: useTemplatesStore(), + externalHooks: this.externalHooks, + nodeTypesStore: useNodeTypesStore(), + telemetry: this.$telemetry, }); }, navigateTo(e: MouseEvent, page: string, id: string) { diff --git a/packages/editor-ui/src/views/TemplatesWorkflowView.vue b/packages/editor-ui/src/views/TemplatesWorkflowView.vue index 9e8125b1223bf..f867384275e69 100644 --- a/packages/editor-ui/src/views/TemplatesWorkflowView.vue +++ b/packages/editor-ui/src/views/TemplatesWorkflowView.vue @@ -68,9 +68,9 @@ import { workflowHelpers } from '@/mixins/workflowHelpers'; import { setPageTitle } from '@/utils/htmlUtils'; import { useTemplatesStore } from '@/stores/templates.store'; import { usePostHog } from '@/stores/posthog.store'; -import { openTemplateCredentialSetup } from '@/utils/templates/templateActions'; +import { useTemplateWorkflow } from '@/utils/templates/templateActions'; import { useExternalHooks } from '@/composables/useExternalHooks'; -import { TEMPLATE_CREDENTIAL_SETUP_EXPERIMENT } from '@/constants'; +import { useNodeTypesStore } from '@/stores/nodeTypes.store'; export default defineComponent({ name: 'TemplatesWorkflowView', @@ -132,24 +132,15 @@ export default defineComponent({ }, methods: { async openTemplateSetup(id: string, e: PointerEvent) { - if (!this.posthogStore.isFeatureEnabled(TEMPLATE_CREDENTIAL_SETUP_EXPERIMENT)) { - const telemetryPayload = { - source: 'workflow', - template_id: id, - wf_template_repo_session_id: this.templatesStore.currentSessionId, - }; - - this.$telemetry.track('User inserted workflow template', telemetryPayload, { - withPostHog: true, - }); - await this.externalHooks.run('templatesWorkflowView.openWorkflow', telemetryPayload); - } - - await openTemplateCredentialSetup({ + await useTemplateWorkflow({ posthogStore: this.posthogStore, router: this.$router, templateId: id, inNewBrowserTab: e.metaKey || e.ctrlKey, + externalHooks: this.externalHooks, + nodeTypesStore: useNodeTypesStore(), + telemetry: this.$telemetry, + templatesStore: useTemplatesStore(), }); }, onHidePreview() {