mirror of
https://github.com/n8n-io/n8n.git
synced 2024-11-12 15:44:06 -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()]);
|
return JSON.stringify([query.search || '', [...query.categories].sort()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type TemplatesStore = ReturnType<typeof useTemplatesStore>;
|
||||||
|
|
||||||
export const useTemplatesStore = defineStore(STORES.TEMPLATES, {
|
export const useTemplatesStore = defineStore(STORES.TEMPLATES, {
|
||||||
state: (): ITemplateState => ({
|
state: (): ITemplateState => ({
|
||||||
categories: {},
|
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 { getNewWorkflow } from '@/api/workflows';
|
||||||
import { TEMPLATE_CREDENTIAL_SETUP_EXPERIMENT, VIEWS } from '@/constants';
|
import { TEMPLATE_CREDENTIAL_SETUP_EXPERIMENT, VIEWS } from '@/constants';
|
||||||
import type { useRootStore } from '@/stores/n8nRoot.store';
|
import type { useRootStore } from '@/stores/n8nRoot.store';
|
||||||
|
@ -7,9 +12,19 @@ import type { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
import { getFixedNodesList } from '@/utils/nodeViewUtils';
|
import { getFixedNodesList } from '@/utils/nodeViewUtils';
|
||||||
import type { NodeTypeProvider } from '@/utils/nodeTypes/nodeTypeTransforms';
|
import type { NodeTypeProvider } from '@/utils/nodeTypes/nodeTypeTransforms';
|
||||||
import type { TemplateCredentialKey } from '@/utils/templates/templateTransforms';
|
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 { INodeCredentialsDetails } from 'n8n-workflow';
|
||||||
import type { RouteLocationRaw, Router } from 'vue-router';
|
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
|
* 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
|
* Opens the template credential setup view
|
||||||
* if the feature flag is disabled)
|
|
||||||
*/
|
*/
|
||||||
export async function openTemplateCredentialSetup(opts: {
|
async function openTemplateCredentialSetup(opts: {
|
||||||
posthogStore: PosthogStore;
|
|
||||||
templateId: string;
|
templateId: string;
|
||||||
router: Router;
|
router: Router;
|
||||||
inNewBrowserTab?: boolean;
|
inNewBrowserTab?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const { router, templateId, inNewBrowserTab = false, posthogStore } = opts;
|
const { router, templateId, inNewBrowserTab = false } = opts;
|
||||||
|
|
||||||
const routeLocation: RouteLocationRaw = posthogStore.isFeatureEnabled(
|
const routeLocation: RouteLocationRaw = {
|
||||||
TEMPLATE_CREDENTIAL_SETUP_EXPERIMENT,
|
name: VIEWS.TEMPLATE_SETUP,
|
||||||
)
|
params: { id: templateId },
|
||||||
? {
|
};
|
||||||
name: VIEWS.TEMPLATE_SETUP,
|
|
||||||
params: { id: templateId },
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
name: VIEWS.TEMPLATE_IMPORT,
|
|
||||||
params: { id: templateId },
|
|
||||||
};
|
|
||||||
|
|
||||||
if (inNewBrowserTab) {
|
if (inNewBrowserTab) {
|
||||||
const route = router.resolve(routeLocation);
|
const route = router.resolve(routeLocation);
|
||||||
|
@ -79,3 +85,93 @@ export async function openTemplateCredentialSetup(opts: {
|
||||||
await router.push(routeLocation);
|
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 type { NodeTypeProvider } from '@/utils/nodeTypes/nodeTypeTransforms';
|
||||||
import { getNodeTypeDisplayableCredentials } from '@/utils/nodes/nodeTransforms';
|
import { getNodeTypeDisplayableCredentials } from '@/utils/nodes/nodeTransforms';
|
||||||
import type { NormalizedTemplateNodeCredentials } from '@/utils/templates/templateTypes';
|
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 &
|
export type IWorkflowTemplateNodeWithCredentials = IWorkflowTemplateNode &
|
||||||
Required<Pick<IWorkflowTemplateNode, 'credentials'>>;
|
Required<Pick<IWorkflowTemplateNode, 'credentials'>>;
|
||||||
|
@ -17,6 +25,11 @@ const credentialKeySymbol = Symbol('credentialKey');
|
||||||
*/
|
*/
|
||||||
export type TemplateCredentialKey = string & { [credentialKeySymbol]: never };
|
export type TemplateCredentialKey = string & { [credentialKeySymbol]: never };
|
||||||
|
|
||||||
|
export type TemplateNodeWithRequiredCredential = {
|
||||||
|
node: IWorkflowTemplateNode;
|
||||||
|
requiredCredentials: INodeCredentialDescription[];
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Forms a key from credential type name and credential name
|
* 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: [],
|
image: [],
|
||||||
full: true,
|
full: true,
|
||||||
} satisfies ITemplatesWorkflowFull;
|
} 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 { useTemplatesStore } from '@/stores/templates.store';
|
||||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
import { getAppNameFromNodeName } from '@/utils/nodeTypesUtils';
|
import { getAppNameFromNodeName } from '@/utils/nodeTypesUtils';
|
||||||
import type {
|
import type { INodeCredentialsDetails, INodeTypeDescription } from 'n8n-workflow';
|
||||||
INodeCredentialDescription,
|
import type { ICredentialsResponse, INodeUi, IWorkflowTemplateNode } from '@/Interface';
|
||||||
INodeCredentialsDetails,
|
|
||||||
INodeTypeDescription,
|
|
||||||
} from 'n8n-workflow';
|
|
||||||
import type {
|
|
||||||
ICredentialsResponse,
|
|
||||||
INodeUi,
|
|
||||||
ITemplatesWorkflowFull,
|
|
||||||
IWorkflowTemplateNode,
|
|
||||||
} from '@/Interface';
|
|
||||||
import { VIEWS } from '@/constants';
|
import { VIEWS } from '@/constants';
|
||||||
import { createWorkflowFromTemplate } from '@/utils/templates/templateActions';
|
import { createWorkflowFromTemplate } from '@/utils/templates/templateActions';
|
||||||
import type { TemplateCredentialKey } from '@/utils/templates/templateTransforms';
|
import type {
|
||||||
|
TemplateCredentialKey,
|
||||||
|
TemplateNodeWithRequiredCredential,
|
||||||
|
} from '@/utils/templates/templateTransforms';
|
||||||
import {
|
import {
|
||||||
|
getNodesRequiringCredentials,
|
||||||
keyFromCredentialTypeAndName,
|
keyFromCredentialTypeAndName,
|
||||||
normalizeTemplateNodeCredentials,
|
normalizeTemplateNodeCredentials,
|
||||||
} from '@/utils/templates/templateTransforms';
|
} from '@/utils/templates/templateTransforms';
|
||||||
import { useExternalHooks } from '@/composables/useExternalHooks';
|
import { useExternalHooks } from '@/composables/useExternalHooks';
|
||||||
import { useTelemetry } from '@/composables/useTelemetry';
|
import { useTelemetry } from '@/composables/useTelemetry';
|
||||||
import { getNodeTypeDisplayableCredentials } from '@/utils/nodes/nodeTransforms';
|
|
||||||
import type { NodeTypeProvider } from '@/utils/nodeTypes/nodeTypeTransforms';
|
|
||||||
|
|
||||||
export type NodeAndType = {
|
export type NodeAndType = {
|
||||||
node: INodeUi;
|
node: INodeUi;
|
||||||
|
@ -64,35 +57,8 @@ export type AppCredentialCount = {
|
||||||
count: number;
|
count: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TemplateNodeWithRequiredCredential = {
|
|
||||||
node: IWorkflowTemplateNode;
|
|
||||||
requiredCredentials: INodeCredentialDescription[];
|
|
||||||
};
|
|
||||||
|
|
||||||
//#region Getter functions
|
//#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 = (
|
export const groupNodeCredentialsByKey = (
|
||||||
nodeWithRequiredCredentials: TemplateNodeWithRequiredCredential[],
|
nodeWithRequiredCredentials: TemplateNodeWithRequiredCredential[],
|
||||||
) => {
|
) => {
|
||||||
|
@ -343,6 +309,9 @@ export const useSetupTemplateStore = defineStore('setupTemplate', () => {
|
||||||
|
|
||||||
telemetry.track('User closed cred setup', {
|
telemetry.track('User closed cred setup', {
|
||||||
completed: false,
|
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
|
// 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', {
|
telemetry.track('User closed cred setup', {
|
||||||
completed: true,
|
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
|
// Replace the URL so back button doesn't come back to this setup view
|
||||||
|
|
|
@ -66,11 +66,12 @@ import type {
|
||||||
} from '@/Interface';
|
} from '@/Interface';
|
||||||
|
|
||||||
import { setPageTitle } from '@/utils/htmlUtils';
|
import { setPageTitle } from '@/utils/htmlUtils';
|
||||||
import { TEMPLATE_CREDENTIAL_SETUP_EXPERIMENT, VIEWS } from '@/constants';
|
import { VIEWS } from '@/constants';
|
||||||
import { useTemplatesStore } from '@/stores/templates.store';
|
import { useTemplatesStore } from '@/stores/templates.store';
|
||||||
import { usePostHog } from '@/stores/posthog.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 { useExternalHooks } from '@/composables/useExternalHooks';
|
||||||
|
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'TemplatesCollectionView',
|
name: 'TemplatesCollectionView',
|
||||||
|
@ -152,23 +153,15 @@ export default defineComponent({
|
||||||
this.navigateTo(event, VIEWS.TEMPLATE, id);
|
this.navigateTo(event, VIEWS.TEMPLATE, id);
|
||||||
},
|
},
|
||||||
async onUseWorkflow({ event, id }: { event: MouseEvent; id: string }) {
|
async onUseWorkflow({ event, id }: { event: MouseEvent; id: string }) {
|
||||||
if (this.posthogStore.isFeatureEnabled(TEMPLATE_CREDENTIAL_SETUP_EXPERIMENT)) {
|
await useTemplateWorkflow({
|
||||||
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({
|
|
||||||
posthogStore: this.posthogStore,
|
posthogStore: this.posthogStore,
|
||||||
router: this.$router,
|
router: this.$router,
|
||||||
templateId: id,
|
templateId: id,
|
||||||
inNewBrowserTab: event.metaKey || event.ctrlKey,
|
inNewBrowserTab: event.metaKey || event.ctrlKey,
|
||||||
|
templatesStore: useTemplatesStore(),
|
||||||
|
externalHooks: this.externalHooks,
|
||||||
|
nodeTypesStore: useNodeTypesStore(),
|
||||||
|
telemetry: this.$telemetry,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
navigateTo(e: MouseEvent, page: string, id: string) {
|
navigateTo(e: MouseEvent, page: string, id: string) {
|
||||||
|
|
|
@ -68,9 +68,9 @@ import { workflowHelpers } from '@/mixins/workflowHelpers';
|
||||||
import { setPageTitle } from '@/utils/htmlUtils';
|
import { setPageTitle } from '@/utils/htmlUtils';
|
||||||
import { useTemplatesStore } from '@/stores/templates.store';
|
import { useTemplatesStore } from '@/stores/templates.store';
|
||||||
import { usePostHog } from '@/stores/posthog.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 { useExternalHooks } from '@/composables/useExternalHooks';
|
||||||
import { TEMPLATE_CREDENTIAL_SETUP_EXPERIMENT } from '@/constants';
|
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'TemplatesWorkflowView',
|
name: 'TemplatesWorkflowView',
|
||||||
|
@ -132,24 +132,15 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async openTemplateSetup(id: string, e: PointerEvent) {
|
async openTemplateSetup(id: string, e: PointerEvent) {
|
||||||
if (!this.posthogStore.isFeatureEnabled(TEMPLATE_CREDENTIAL_SETUP_EXPERIMENT)) {
|
await useTemplateWorkflow({
|
||||||
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({
|
|
||||||
posthogStore: this.posthogStore,
|
posthogStore: this.posthogStore,
|
||||||
router: this.$router,
|
router: this.$router,
|
||||||
templateId: id,
|
templateId: id,
|
||||||
inNewBrowserTab: e.metaKey || e.ctrlKey,
|
inNewBrowserTab: e.metaKey || e.ctrlKey,
|
||||||
|
externalHooks: this.externalHooks,
|
||||||
|
nodeTypesStore: useNodeTypesStore(),
|
||||||
|
telemetry: this.$telemetry,
|
||||||
|
templatesStore: useTemplatesStore(),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onHidePreview() {
|
onHidePreview() {
|
||||||
|
|
Loading…
Reference in a new issue