mirror of
https://github.com/n8n-io/n8n.git
synced 2024-11-10 06:34:05 -08:00
fix: Adjust 'use template' feature telemetry (no-changelog) (#8232)
- Add extra params to 'User closed cred setup' event - Fire 'User closed cred setup' only when template has creds - Skip cred setup page if template has no creds required - Fire 'User inserted workflow template' also in cred setup
This commit is contained in:
parent
cda49a4747
commit
6e78d2346e
|
@ -28,6 +28,8 @@ function getSearchKey(query: ITemplatesQuery): string {
|
|||
return JSON.stringify([query.search || '', [...query.categories].sort()]);
|
||||
}
|
||||
|
||||
export type TemplatesStore = ReturnType<typeof useTemplatesStore>;
|
||||
|
||||
export const useTemplatesStore = defineStore(STORES.TEMPLATES, {
|
||||
state: (): ITemplateState => ({
|
||||
categories: {},
|
||||
|
|
|
@ -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 },
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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<typeof useExternalHooks>;
|
||||
|
||||
/**
|
||||
* Creates a new workflow from a template
|
||||
|
@ -49,28 +64,19 @@ 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, posthogStore } = opts;
|
||||
const { router, templateId, inNewBrowserTab = false } = 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 routeLocation: RouteLocationRaw = {
|
||||
name: VIEWS.TEMPLATE_SETUP,
|
||||
params: { id: templateId },
|
||||
};
|
||||
|
||||
if (inNewBrowserTab) {
|
||||
const route = router.resolve(routeLocation);
|
||||
|
@ -79,3 +85,93 @@ export async function openTemplateCredentialSetup(opts: {
|
|||
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 { 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);
|
||||
window.open(route.href, '_blank');
|
||||
} else {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<Pick<IWorkflowTemplateNode, 'credentials'>>;
|
||||
|
@ -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;
|
||||
};
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -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;
|
||||
|
|
|
@ -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[],
|
||||
) => {
|
||||
|
@ -343,6 +309,9 @@ export const useSetupTemplateStore = defineStore('setupTemplate', () => {
|
|||
|
||||
telemetry.track('User closed cred setup', {
|
||||
completed: false,
|
||||
creds_filled: 0,
|
||||
creds_needed: credentialUsages.value.length,
|
||||
workflow_id: null,
|
||||
});
|
||||
|
||||
// Replace the URL so back button doesn't come back to this setup view
|
||||
|
@ -376,6 +345,19 @@ export const useSetupTemplateStore = defineStore('setupTemplate', () => {
|
|||
|
||||
telemetry.track('User closed cred setup', {
|
||||
completed: true,
|
||||
creds_filled: numFilledCredentials.value,
|
||||
creds_needed: credentialUsages.value.length,
|
||||
workflow_id: createdWorkflow.id,
|
||||
});
|
||||
|
||||
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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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() {
|
||||
|
|
Loading…
Reference in a new issue