mirror of
https://github.com/n8n-io/n8n.git
synced 2024-11-09 22:24:05 -08:00
fix: Fix template credential setup for nodes that dont have credentials (#8208)
Fix template credential setup for templates whose workflow includes nodes that require credentials but the workflow definition does not have them defined. Like for example https://n8n.io/workflows/1344-save-email-attachments-to-nextcloud/
This commit is contained in:
parent
4186884740
commit
cd3f5b5b1f
|
@ -1,4 +1,3 @@
|
||||||
import { CredentialsModal, MessageBox } from '../pages/modals';
|
|
||||||
import {
|
import {
|
||||||
clickUseWorkflowButtonByTitle,
|
clickUseWorkflowButtonByTitle,
|
||||||
visitTemplateCollectionPage,
|
visitTemplateCollectionPage,
|
||||||
|
@ -9,8 +8,6 @@ import { TemplateWorkflowPage } from '../pages/template-workflow';
|
||||||
import { WorkflowPage } from '../pages/workflow';
|
import { WorkflowPage } from '../pages/workflow';
|
||||||
|
|
||||||
const templateWorkflowPage = new TemplateWorkflowPage();
|
const templateWorkflowPage = new TemplateWorkflowPage();
|
||||||
const credentialsModal = new CredentialsModal();
|
|
||||||
const messageBox = new MessageBox();
|
|
||||||
const workflowPage = new WorkflowPage();
|
const workflowPage = new WorkflowPage();
|
||||||
|
|
||||||
const testTemplate = templateCredentialsSetupPage.testData.simpleTemplate;
|
const testTemplate = templateCredentialsSetupPage.testData.simpleTemplate;
|
||||||
|
@ -95,24 +92,48 @@ describe('Template credentials setup', () => {
|
||||||
// Continue button should be disabled if no credentials are created
|
// Continue button should be disabled if no credentials are created
|
||||||
templateCredentialsSetupPage.getters.continueButton().should('be.disabled');
|
templateCredentialsSetupPage.getters.continueButton().should('be.disabled');
|
||||||
|
|
||||||
templateCredentialsSetupPage.getters.createAppCredentialsButton('Shopify').click();
|
templateCredentialsSetupPage.fillInDummyCredentialsForApp('Shopify');
|
||||||
credentialsModal.getters.editCredentialModal().find('input:first()').type('test');
|
|
||||||
credentialsModal.actions.save(false);
|
|
||||||
credentialsModal.actions.close();
|
|
||||||
|
|
||||||
// Continue button should be enabled if at least one has been created
|
// Continue button should be enabled if at least one has been created
|
||||||
templateCredentialsSetupPage.getters.continueButton().should('be.enabled');
|
templateCredentialsSetupPage.getters.continueButton().should('be.enabled');
|
||||||
|
|
||||||
templateCredentialsSetupPage.getters.createAppCredentialsButton('X (Formerly Twitter)').click();
|
templateCredentialsSetupPage.fillInDummyCredentialsForAppWithConfirm('X (Formerly Twitter)');
|
||||||
credentialsModal.getters.editCredentialModal().find('input:first()').type('test');
|
templateCredentialsSetupPage.fillInDummyCredentialsForApp('Telegram');
|
||||||
credentialsModal.actions.save(false);
|
|
||||||
credentialsModal.actions.close();
|
|
||||||
messageBox.actions.cancel();
|
|
||||||
|
|
||||||
templateCredentialsSetupPage.getters.createAppCredentialsButton('Telegram').click();
|
cy.intercept('POST', '/rest/workflows').as('createWorkflow');
|
||||||
credentialsModal.getters.editCredentialModal().find('input:first()').type('test');
|
templateCredentialsSetupPage.getters.continueButton().should('be.enabled');
|
||||||
credentialsModal.actions.save(false);
|
templateCredentialsSetupPage.getters.continueButton().click();
|
||||||
credentialsModal.actions.close();
|
cy.wait('@createWorkflow');
|
||||||
|
|
||||||
|
workflowPage.getters.canvasNodes().should('have.length', 3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work with a template that has no credentials (ADO-1603)', () => {
|
||||||
|
const templateWithoutCreds = templateCredentialsSetupPage.testData.templateWithoutCredentials;
|
||||||
|
cy.intercept('GET', `https://api.n8n.io/api/templates/workflows/${templateWithoutCreds.id}`, {
|
||||||
|
fixture: templateWithoutCreds.fixture,
|
||||||
|
});
|
||||||
|
templateCredentialsSetupPage.visitTemplateCredentialSetupPage(templateWithoutCreds.id);
|
||||||
|
|
||||||
|
const expectedAppNames = ['1. Email (IMAP)', '2. Nextcloud'];
|
||||||
|
const expectedAppDescriptions = [
|
||||||
|
'The credential you select will be used in the IMAP Email node of the workflow template.',
|
||||||
|
'The credential you select will be used in the Nextcloud node of the workflow template.',
|
||||||
|
];
|
||||||
|
|
||||||
|
templateCredentialsSetupPage.getters.appCredentialSteps().each(($el, index) => {
|
||||||
|
templateCredentialsSetupPage.getters
|
||||||
|
.stepHeading($el)
|
||||||
|
.should('have.text', expectedAppNames[index]);
|
||||||
|
templateCredentialsSetupPage.getters
|
||||||
|
.stepDescription($el)
|
||||||
|
.should('have.text', expectedAppDescriptions[index]);
|
||||||
|
});
|
||||||
|
|
||||||
|
templateCredentialsSetupPage.getters.continueButton().should('be.disabled');
|
||||||
|
|
||||||
|
templateCredentialsSetupPage.fillInDummyCredentialsForApp('Email (IMAP)');
|
||||||
|
templateCredentialsSetupPage.fillInDummyCredentialsForApp('Nextcloud');
|
||||||
|
|
||||||
cy.intercept('POST', '/rest/workflows').as('createWorkflow');
|
cy.intercept('POST', '/rest/workflows').as('createWorkflow');
|
||||||
templateCredentialsSetupPage.getters.continueButton().should('be.enabled');
|
templateCredentialsSetupPage.getters.continueButton().should('be.enabled');
|
||||||
|
|
182
cypress/fixtures/Test_Template_2.json
Normal file
182
cypress/fixtures/Test_Template_2.json
Normal file
File diff suppressed because one or more lines are too long
|
@ -1,3 +1,5 @@
|
||||||
|
import { CredentialsModal, MessageBox } from './modals';
|
||||||
|
|
||||||
export type TemplateTestData = {
|
export type TemplateTestData = {
|
||||||
id: number;
|
id: number;
|
||||||
fixture: string;
|
fixture: string;
|
||||||
|
@ -8,8 +10,15 @@ export const testData = {
|
||||||
id: 1205,
|
id: 1205,
|
||||||
fixture: 'Test_Template_1.json',
|
fixture: 'Test_Template_1.json',
|
||||||
},
|
},
|
||||||
|
templateWithoutCredentials: {
|
||||||
|
id: 1344,
|
||||||
|
fixture: 'Test_Template_2.json',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const credentialsModal = new CredentialsModal();
|
||||||
|
const messageBox = new MessageBox();
|
||||||
|
|
||||||
export const getters = {
|
export const getters = {
|
||||||
continueButton: () => cy.getByTestId('continue-button'),
|
continueButton: () => cy.getByTestId('continue-button'),
|
||||||
skipLink: () => cy.get('a:contains("Skip")'),
|
skipLink: () => cy.get('a:contains("Skip")'),
|
||||||
|
@ -33,3 +42,23 @@ export const enableTemplateCredentialSetupFeatureFlag = () => {
|
||||||
win.featureFlags.override('016_template_credential_setup', true);
|
win.featureFlags.override('016_template_credential_setup', true);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fills in dummy credentials for the given app name.
|
||||||
|
*/
|
||||||
|
export const fillInDummyCredentialsForApp = (appName: string) => {
|
||||||
|
getters.createAppCredentialsButton(appName).click();
|
||||||
|
credentialsModal.getters.editCredentialModal().find('input:first()').type('test');
|
||||||
|
credentialsModal.actions.save(false);
|
||||||
|
credentialsModal.actions.close();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fills in dummy credentials for the given app name. Assumes
|
||||||
|
* that a confirmation message box will be shown, which will be
|
||||||
|
* handled.
|
||||||
|
*/
|
||||||
|
export const fillInDummyCredentialsForAppWithConfirm = (appName: string) => {
|
||||||
|
fillInDummyCredentialsForApp(appName);
|
||||||
|
messageBox.actions.cancel();
|
||||||
|
};
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
import type { INodeTypeDescription } from 'n8n-workflow';
|
import { type INodeTypeDescription } from 'n8n-workflow';
|
||||||
import type { NodeTypesByTypeNameAndVersion } from '@/Interface';
|
import type { NodeTypesByTypeNameAndVersion } from '@/Interface';
|
||||||
import { DEFAULT_NODETYPE_VERSION } from '@/constants';
|
import { DEFAULT_NODETYPE_VERSION } from '@/constants';
|
||||||
|
import type { NodeTypesStore } from '@/stores/nodeTypes.store';
|
||||||
|
|
||||||
|
export type NodeTypeProvider = Pick<NodeTypesStore, 'getNodeType'>;
|
||||||
|
|
||||||
export function getNodeVersions(nodeType: INodeTypeDescription) {
|
export function getNodeVersions(nodeType: INodeTypeDescription) {
|
||||||
return Array.isArray(nodeType.version) ? nodeType.version : [nodeType.version];
|
return Array.isArray(nodeType.version) ? nodeType.version : [nodeType.version];
|
||||||
|
|
33
packages/editor-ui/src/utils/nodes/nodeTransforms.ts
Normal file
33
packages/editor-ui/src/utils/nodes/nodeTransforms.ts
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
import type { INodeUi } from '@/Interface';
|
||||||
|
import type { NodeTypeProvider } from '@/utils/nodeTypes/nodeTypeTransforms';
|
||||||
|
import type { INodeCredentialDescription } from 'n8n-workflow';
|
||||||
|
import { NodeHelpers } from 'n8n-workflow';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the credentials that are displayable for the given node.
|
||||||
|
*/
|
||||||
|
export function getNodeTypeDisplayableCredentials(
|
||||||
|
nodeTypeProvider: NodeTypeProvider,
|
||||||
|
node: Pick<INodeUi, 'parameters' | 'type' | 'typeVersion'>,
|
||||||
|
): INodeCredentialDescription[] {
|
||||||
|
const nodeType = nodeTypeProvider.getNodeType(node.type, node.typeVersion);
|
||||||
|
if (!nodeType?.credentials) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const nodeTypeCreds = nodeType.credentials;
|
||||||
|
|
||||||
|
// We must populate the node's parameters with the default values
|
||||||
|
// before we can check which credentials are available, because
|
||||||
|
// credentials can have conditional requirements that depend on
|
||||||
|
// node parameters.
|
||||||
|
const nodeParameters =
|
||||||
|
NodeHelpers.getNodeParameters(nodeType.properties, node.parameters, true, false, node) ??
|
||||||
|
node.parameters;
|
||||||
|
|
||||||
|
const displayableCredentials = nodeTypeCreds.filter((credentialTypeDescription) => {
|
||||||
|
return NodeHelpers.displayParameter(nodeParameters, credentialTypeDescription, node);
|
||||||
|
});
|
||||||
|
|
||||||
|
return displayableCredentials;
|
||||||
|
}
|
|
@ -7,6 +7,9 @@ import { newWorkflowTemplateNode } from '@/utils/testData/templateTestData';
|
||||||
describe('templateTransforms', () => {
|
describe('templateTransforms', () => {
|
||||||
describe('replaceAllTemplateNodeCredentials', () => {
|
describe('replaceAllTemplateNodeCredentials', () => {
|
||||||
it('should replace credentials of nodes that have credentials', () => {
|
it('should replace credentials of nodes that have credentials', () => {
|
||||||
|
const nodeTypeProvider = {
|
||||||
|
getNodeType: vitest.fn(),
|
||||||
|
};
|
||||||
const node = newWorkflowTemplateNode({
|
const node = newWorkflowTemplateNode({
|
||||||
type: 'n8n-nodes-base.twitter',
|
type: 'n8n-nodes-base.twitter',
|
||||||
credentials: {
|
credentials: {
|
||||||
|
@ -21,7 +24,11 @@ describe('templateTransforms', () => {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const [replacedNode] = replaceAllTemplateNodeCredentials([node], toReplaceWith);
|
const [replacedNode] = replaceAllTemplateNodeCredentials(
|
||||||
|
nodeTypeProvider,
|
||||||
|
[node],
|
||||||
|
toReplaceWith,
|
||||||
|
);
|
||||||
|
|
||||||
expect(replacedNode.credentials).toEqual({
|
expect(replacedNode.credentials).toEqual({
|
||||||
twitterOAuth1Api: { id: 'new1', name: 'Twitter creds' },
|
twitterOAuth1Api: { id: 'new1', name: 'Twitter creds' },
|
||||||
|
@ -29,6 +36,9 @@ describe('templateTransforms', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not replace credentials of nodes that do not have credentials', () => {
|
it('should not replace credentials of nodes that do not have credentials', () => {
|
||||||
|
const nodeTypeProvider = {
|
||||||
|
getNodeType: vitest.fn(),
|
||||||
|
};
|
||||||
const node = newWorkflowTemplateNode({
|
const node = newWorkflowTemplateNode({
|
||||||
type: 'n8n-nodes-base.twitter',
|
type: 'n8n-nodes-base.twitter',
|
||||||
});
|
});
|
||||||
|
@ -39,7 +49,11 @@ describe('templateTransforms', () => {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const [replacedNode] = replaceAllTemplateNodeCredentials([node], toReplaceWith);
|
const [replacedNode] = replaceAllTemplateNodeCredentials(
|
||||||
|
nodeTypeProvider,
|
||||||
|
[node],
|
||||||
|
toReplaceWith,
|
||||||
|
);
|
||||||
|
|
||||||
expect(replacedNode.credentials).toBeUndefined();
|
expect(replacedNode.credentials).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
|
@ -5,6 +5,7 @@ import type { useRootStore } from '@/stores/n8nRoot.store';
|
||||||
import type { PosthogStore } from '@/stores/posthog.store';
|
import type { PosthogStore } from '@/stores/posthog.store';
|
||||||
import type { useWorkflowsStore } from '@/stores/workflows.store';
|
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 { TemplateCredentialKey } from '@/utils/templates/templateTransforms';
|
import type { TemplateCredentialKey } from '@/utils/templates/templateTransforms';
|
||||||
import { replaceAllTemplateNodeCredentials } from '@/utils/templates/templateTransforms';
|
import { replaceAllTemplateNodeCredentials } from '@/utils/templates/templateTransforms';
|
||||||
import type { INodeCredentialsDetails } from 'n8n-workflow';
|
import type { INodeCredentialsDetails } from 'n8n-workflow';
|
||||||
|
@ -13,14 +14,18 @@ import type { RouteLocationRaw, Router } from 'vue-router';
|
||||||
/**
|
/**
|
||||||
* Creates a new workflow from a template
|
* Creates a new workflow from a template
|
||||||
*/
|
*/
|
||||||
export async function createWorkflowFromTemplate(
|
export async function createWorkflowFromTemplate(opts: {
|
||||||
template: IWorkflowTemplate,
|
template: IWorkflowTemplate;
|
||||||
credentialOverrides: Record<TemplateCredentialKey, INodeCredentialsDetails>,
|
credentialOverrides: Record<TemplateCredentialKey, INodeCredentialsDetails>;
|
||||||
rootStore: ReturnType<typeof useRootStore>,
|
rootStore: ReturnType<typeof useRootStore>;
|
||||||
workflowsStore: ReturnType<typeof useWorkflowsStore>,
|
workflowsStore: ReturnType<typeof useWorkflowsStore>;
|
||||||
) {
|
nodeTypeProvider: NodeTypeProvider;
|
||||||
|
}) {
|
||||||
|
const { credentialOverrides, nodeTypeProvider, rootStore, template, workflowsStore } = opts;
|
||||||
|
|
||||||
const workflowData = await getNewWorkflow(rootStore.getRestApiContext, template.name);
|
const workflowData = await getNewWorkflow(rootStore.getRestApiContext, template.name);
|
||||||
const nodesWithCreds = replaceAllTemplateNodeCredentials(
|
const nodesWithCreds = replaceAllTemplateNodeCredentials(
|
||||||
|
nodeTypeProvider,
|
||||||
template.workflow.nodes,
|
template.workflow.nodes,
|
||||||
credentialOverrides,
|
credentialOverrides,
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
import type { IWorkflowTemplateNode, IWorkflowTemplateNodeCredentials } from '@/Interface';
|
import type { IWorkflowTemplateNode, IWorkflowTemplateNodeCredentials } from '@/Interface';
|
||||||
|
import type { NodeTypeProvider } from '@/utils/nodeTypes/nodeTypeTransforms';
|
||||||
|
import { getNodeTypeDisplayableCredentials } from '@/utils/nodes/nodeTransforms';
|
||||||
import type { NormalizedTemplateNodeCredentials } from '@/utils/templates/templateTypes';
|
import type { NormalizedTemplateNodeCredentials } from '@/utils/templates/templateTypes';
|
||||||
import type { INodeCredentials, INodeCredentialsDetails } from 'n8n-workflow';
|
import type { INodeCredentials, INodeCredentialsDetails } from 'n8n-workflow';
|
||||||
|
|
||||||
|
@ -23,14 +25,6 @@ export const keyFromCredentialTypeAndName = (
|
||||||
credentialName: string,
|
credentialName: string,
|
||||||
): TemplateCredentialKey => `${credentialTypeName}-${credentialName}` as TemplateCredentialKey;
|
): TemplateCredentialKey => `${credentialTypeName}-${credentialName}` as TemplateCredentialKey;
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if a template workflow node has credentials defined
|
|
||||||
*/
|
|
||||||
export const hasNodeCredentials = (
|
|
||||||
node: IWorkflowTemplateNode,
|
|
||||||
): node is IWorkflowTemplateNodeWithCredentials =>
|
|
||||||
!!node.credentials && Object.keys(node.credentials).length > 0;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Normalizes the credentials of a template node. Templates created with
|
* Normalizes the credentials of a template node. Templates created with
|
||||||
* different versions of n8n may have different credential formats.
|
* different versions of n8n may have different credential formats.
|
||||||
|
@ -57,26 +51,49 @@ export const normalizeTemplateNodeCredentials = (
|
||||||
* replaceTemplateNodeCredentials(nodeCredentials, toReplaceByKey);
|
* replaceTemplateNodeCredentials(nodeCredentials, toReplaceByKey);
|
||||||
* // => { twitterOAuth1Api: { id: "BrEOZ5Cje6VYh9Pc", name: "X OAuth account" } }
|
* // => { twitterOAuth1Api: { id: "BrEOZ5Cje6VYh9Pc", name: "X OAuth account" } }
|
||||||
*/
|
*/
|
||||||
export const replaceTemplateNodeCredentials = (
|
export const getReplacedTemplateNodeCredentials = (
|
||||||
nodeCredentials: IWorkflowTemplateNodeCredentials,
|
nodeCredentials: IWorkflowTemplateNodeCredentials | undefined,
|
||||||
toReplaceByKey: Record<TemplateCredentialKey, INodeCredentialsDetails>,
|
toReplaceByKey: Record<TemplateCredentialKey, INodeCredentialsDetails>,
|
||||||
) => {
|
) => {
|
||||||
if (!nodeCredentials) {
|
if (!nodeCredentials) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const newNodeCredentials: INodeCredentials = {};
|
const replacedNodeCredentials: INodeCredentials = {};
|
||||||
const normalizedCredentials = normalizeTemplateNodeCredentials(nodeCredentials);
|
const normalizedCredentials = normalizeTemplateNodeCredentials(nodeCredentials);
|
||||||
for (const credentialType in normalizedCredentials) {
|
for (const credentialType in normalizedCredentials) {
|
||||||
const credentialNameInTemplate = normalizedCredentials[credentialType];
|
const credentialNameInTemplate = normalizedCredentials[credentialType];
|
||||||
const key = keyFromCredentialTypeAndName(credentialType, credentialNameInTemplate);
|
const key = keyFromCredentialTypeAndName(credentialType, credentialNameInTemplate);
|
||||||
const toReplaceWith = toReplaceByKey[key];
|
const toReplaceWith = toReplaceByKey[key];
|
||||||
if (toReplaceWith) {
|
if (toReplaceWith) {
|
||||||
newNodeCredentials[credentialType] = toReplaceWith;
|
replacedNodeCredentials[credentialType] = toReplaceWith;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return newNodeCredentials;
|
return replacedNodeCredentials;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns credentials for the given node that are missing from it
|
||||||
|
* but are present in the given replacements
|
||||||
|
*/
|
||||||
|
export const getMissingTemplateNodeCredentials = (
|
||||||
|
nodeTypeProvider: NodeTypeProvider,
|
||||||
|
node: IWorkflowTemplateNode,
|
||||||
|
replacementsByKey: Record<TemplateCredentialKey, INodeCredentialsDetails>,
|
||||||
|
): INodeCredentials => {
|
||||||
|
const nodeCredentialsToAdd: INodeCredentials = {};
|
||||||
|
const usableCredentials = getNodeTypeDisplayableCredentials(nodeTypeProvider, node);
|
||||||
|
|
||||||
|
for (const usableCred of usableCredentials) {
|
||||||
|
const credentialKey = keyFromCredentialTypeAndName(usableCred.name, '');
|
||||||
|
|
||||||
|
if (replacementsByKey[credentialKey]) {
|
||||||
|
nodeCredentialsToAdd[usableCred.name] = replacementsByKey[credentialKey];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nodeCredentialsToAdd;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -84,17 +101,22 @@ export const replaceTemplateNodeCredentials = (
|
||||||
* replacements
|
* replacements
|
||||||
*/
|
*/
|
||||||
export const replaceAllTemplateNodeCredentials = (
|
export const replaceAllTemplateNodeCredentials = (
|
||||||
|
nodeTypeProvider: NodeTypeProvider,
|
||||||
nodes: IWorkflowTemplateNode[],
|
nodes: IWorkflowTemplateNode[],
|
||||||
toReplaceWith: Record<TemplateCredentialKey, INodeCredentialsDetails>,
|
toReplaceWith: Record<TemplateCredentialKey, INodeCredentialsDetails>,
|
||||||
) => {
|
) => {
|
||||||
return nodes.map((node) => {
|
return nodes.map((node) => {
|
||||||
if (hasNodeCredentials(node)) {
|
const replacedCredentials = getReplacedTemplateNodeCredentials(node.credentials, toReplaceWith);
|
||||||
return {
|
const newCredentials = getMissingTemplateNodeCredentials(nodeTypeProvider, node, toReplaceWith);
|
||||||
...node,
|
|
||||||
credentials: replaceTemplateNodeCredentials(node.credentials, toReplaceWith),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return node;
|
const credentials = {
|
||||||
|
...replacedCredentials,
|
||||||
|
...newCredentials,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
...node,
|
||||||
|
credentials: Object.keys(credentials).length > 0 ? credentials : undefined,
|
||||||
|
};
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
/**
|
||||||
|
* Credential type test data
|
||||||
|
*/
|
||||||
|
import type { ICredentialType } from 'n8n-workflow';
|
||||||
|
|
||||||
|
export const newCredentialType = (name: string): ICredentialType => ({
|
||||||
|
name,
|
||||||
|
displayName: name,
|
||||||
|
documentationUrl: name,
|
||||||
|
properties: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
export const credentialTypeTelegram = {
|
||||||
|
name: 'telegramApi',
|
||||||
|
displayName: 'Telegram API',
|
||||||
|
documentationUrl: 'telegram',
|
||||||
|
properties: [
|
||||||
|
{
|
||||||
|
displayName: 'Access Token',
|
||||||
|
name: 'accessToken',
|
||||||
|
type: 'string',
|
||||||
|
typeOptions: {
|
||||||
|
password: true,
|
||||||
|
},
|
||||||
|
default: '',
|
||||||
|
description:
|
||||||
|
'Chat with the <a href="https://telegram.me/botfather">bot father</a> to obtain the access token',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
test: {
|
||||||
|
request: {
|
||||||
|
baseURL: '=https://api.telegram.org/bot{{$credentials.accessToken}}',
|
||||||
|
url: '/getMe',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} satisfies ICredentialType;
|
4720
packages/editor-ui/src/utils/testData/nodeTypeTestData.ts
Normal file
4720
packages/editor-ui/src/utils/testData/nodeTypeTestData.ts
Normal file
File diff suppressed because it is too large
Load diff
File diff suppressed because one or more lines are too long
|
@ -16,6 +16,7 @@ import * as testData from './setupTemplate.store.testData';
|
||||||
import { createTestingPinia } from '@pinia/testing';
|
import { createTestingPinia } from '@pinia/testing';
|
||||||
import { useCredentialsStore } from '@/stores/credentials.store';
|
import { useCredentialsStore } from '@/stores/credentials.store';
|
||||||
import { newWorkflowTemplateNode } from '@/utils/testData/templateTestData';
|
import { newWorkflowTemplateNode } from '@/utils/testData/templateTestData';
|
||||||
|
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||||
|
|
||||||
const objToMap = <TKey extends string, T>(obj: Record<TKey, T>) => {
|
const objToMap = <TKey extends string, T>(obj: Record<TKey, T>) => {
|
||||||
return new Map(Object.entries(obj)) as Map<TKey, T>;
|
return new Map(Object.entries(obj)) as Map<TKey, T>;
|
||||||
|
@ -36,33 +37,6 @@ describe('SetupWorkflowFromTemplateView store', () => {
|
||||||
},
|
},
|
||||||
typeVersion: 1,
|
typeVersion: 1,
|
||||||
},
|
},
|
||||||
Telegram: {
|
|
||||||
name: 'Telegram',
|
|
||||||
type: 'n8n-nodes-base.telegram',
|
|
||||||
position: [720, -20],
|
|
||||||
parameters: {
|
|
||||||
text: '=Hey there, my design is now on a new product!\nVisit my {{$json["vendor"]}} shop to get this cool{{$json["title"]}} (and check out more {{$json["product_type"]}})',
|
|
||||||
chatId: '123456',
|
|
||||||
additionalFields: {},
|
|
||||||
},
|
|
||||||
credentials: {
|
|
||||||
telegramApi: 'telegram',
|
|
||||||
},
|
|
||||||
typeVersion: 1,
|
|
||||||
},
|
|
||||||
shopify: {
|
|
||||||
name: 'shopify',
|
|
||||||
type: 'n8n-nodes-base.shopifyTrigger',
|
|
||||||
position: [540, -110],
|
|
||||||
webhookId: '2a7e0e50-8f09-4a2b-bf54-a849a6ac4fe0',
|
|
||||||
parameters: {
|
|
||||||
topic: 'products/create',
|
|
||||||
},
|
|
||||||
credentials: {
|
|
||||||
shopifyApi: 'shopify',
|
|
||||||
},
|
|
||||||
typeVersion: 1,
|
|
||||||
},
|
|
||||||
} satisfies Record<string, IWorkflowTemplateNodeWithCredentials>;
|
} satisfies Record<string, IWorkflowTemplateNodeWithCredentials>;
|
||||||
|
|
||||||
describe('groupNodeCredentialsByTypeAndName', () => {
|
describe('groupNodeCredentialsByTypeAndName', () => {
|
||||||
|
@ -71,7 +45,14 @@ describe('SetupWorkflowFromTemplateView store', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns credentials grouped by type and name', () => {
|
it('returns credentials grouped by type and name', () => {
|
||||||
expect(groupNodeCredentialsByKey(Object.values(nodesByName))).toEqual(
|
expect(
|
||||||
|
groupNodeCredentialsByKey([
|
||||||
|
{
|
||||||
|
node: nodesByName.Twitter,
|
||||||
|
requiredCredentials: testData.nodeTypeTwitterV1.credentials,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
).toEqual(
|
||||||
objToMap({
|
objToMap({
|
||||||
'twitterOAuth1Api-twitter': {
|
'twitterOAuth1Api-twitter': {
|
||||||
key: 'twitterOAuth1Api-twitter',
|
key: 'twitterOAuth1Api-twitter',
|
||||||
|
@ -80,20 +61,6 @@ describe('SetupWorkflowFromTemplateView store', () => {
|
||||||
nodeTypeName: 'n8n-nodes-base.twitter',
|
nodeTypeName: 'n8n-nodes-base.twitter',
|
||||||
usedBy: [nodesByName.Twitter],
|
usedBy: [nodesByName.Twitter],
|
||||||
},
|
},
|
||||||
'telegramApi-telegram': {
|
|
||||||
key: 'telegramApi-telegram',
|
|
||||||
credentialName: 'telegram',
|
|
||||||
credentialType: 'telegramApi',
|
|
||||||
nodeTypeName: 'n8n-nodes-base.telegram',
|
|
||||||
usedBy: [nodesByName.Telegram],
|
|
||||||
},
|
|
||||||
'shopifyApi-shopify': {
|
|
||||||
key: 'shopifyApi-shopify',
|
|
||||||
credentialName: 'shopify',
|
|
||||||
credentialType: 'shopifyApi',
|
|
||||||
nodeTypeName: 'n8n-nodes-base.shopifyTrigger',
|
|
||||||
usedBy: [nodesByName.shopify],
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -114,7 +81,18 @@ describe('SetupWorkflowFromTemplateView store', () => {
|
||||||
}) as IWorkflowTemplateNodeWithCredentials,
|
}) as IWorkflowTemplateNodeWithCredentials,
|
||||||
];
|
];
|
||||||
|
|
||||||
expect(groupNodeCredentialsByKey([node1, node2])).toEqual(
|
expect(
|
||||||
|
groupNodeCredentialsByKey([
|
||||||
|
{
|
||||||
|
node: node1,
|
||||||
|
requiredCredentials: testData.nodeTypeTwitterV1.credentials,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
node: node2,
|
||||||
|
requiredCredentials: testData.nodeTypeTelegramV1.credentials,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
).toEqual(
|
||||||
objToMap({
|
objToMap({
|
||||||
'twitterOAuth1Api-credential': {
|
'twitterOAuth1Api-credential': {
|
||||||
key: 'twitterOAuth1Api-credential',
|
key: 'twitterOAuth1Api-credential',
|
||||||
|
@ -206,32 +184,35 @@ describe('SetupWorkflowFromTemplateView store', () => {
|
||||||
stubActions: false,
|
stubActions: false,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
});
|
|
||||||
|
|
||||||
it('returns an empty object if there are no credential overrides', () => {
|
|
||||||
// Setup
|
|
||||||
const credentialsStore = useCredentialsStore();
|
const credentialsStore = useCredentialsStore();
|
||||||
credentialsStore.setCredentialTypes([testData.credentialTypeTelegram]);
|
credentialsStore.setCredentialTypes([testData.credentialTypeTelegram]);
|
||||||
const templatesStore = useTemplatesStore();
|
const templatesStore = useTemplatesStore();
|
||||||
templatesStore.addWorkflows([testData.fullShopifyTelegramTwitterTemplate]);
|
templatesStore.addWorkflows([testData.fullShopifyTelegramTwitterTemplate]);
|
||||||
|
const nodeTypesStore = useNodeTypesStore();
|
||||||
|
nodeTypesStore.setNodeTypes([
|
||||||
|
testData.nodeTypeTelegramV1,
|
||||||
|
testData.nodeTypeTwitterV1,
|
||||||
|
testData.nodeTypeShopifyTriggerV1,
|
||||||
|
]);
|
||||||
const setupTemplateStore = useSetupTemplateStore();
|
const setupTemplateStore = useSetupTemplateStore();
|
||||||
setupTemplateStore.setTemplateId(testData.fullShopifyTelegramTwitterTemplate.id.toString());
|
setupTemplateStore.setTemplateId(testData.fullShopifyTelegramTwitterTemplate.id.toString());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return an empty object if there are no credential overrides', () => {
|
||||||
|
// Setup
|
||||||
|
const setupTemplateStore = useSetupTemplateStore();
|
||||||
|
|
||||||
expect(setupTemplateStore.credentialUsages.length).toBe(3);
|
expect(setupTemplateStore.credentialUsages.length).toBe(3);
|
||||||
expect(setupTemplateStore.credentialOverrides).toEqual({});
|
expect(setupTemplateStore.credentialOverrides).toEqual({});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns overrides for one node', () => {
|
it('should return overrides for one node', () => {
|
||||||
// Setup
|
// Setup
|
||||||
const credentialsStore = useCredentialsStore();
|
const credentialsStore = useCredentialsStore();
|
||||||
credentialsStore.setCredentialTypes([testData.credentialTypeTelegram]);
|
|
||||||
credentialsStore.setCredentials([testData.credentialsTelegram1]);
|
credentialsStore.setCredentials([testData.credentialsTelegram1]);
|
||||||
const templatesStore = useTemplatesStore();
|
|
||||||
templatesStore.addWorkflows([testData.fullShopifyTelegramTwitterTemplate]);
|
|
||||||
|
|
||||||
const setupTemplateStore = useSetupTemplateStore();
|
const setupTemplateStore = useSetupTemplateStore();
|
||||||
setupTemplateStore.setTemplateId(testData.fullShopifyTelegramTwitterTemplate.id.toString());
|
|
||||||
setupTemplateStore.setSelectedCredentialId(
|
setupTemplateStore.setSelectedCredentialId(
|
||||||
keyFromCredentialTypeAndName('twitterOAuth1Api', 'twitter'),
|
keyFromCredentialTypeAndName('twitterOAuth1Api', 'twitter'),
|
||||||
testData.credentialsTelegram1.id,
|
testData.credentialsTelegram1.id,
|
||||||
|
@ -254,17 +235,25 @@ describe('SetupWorkflowFromTemplateView store', () => {
|
||||||
stubActions: false,
|
stubActions: false,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
});
|
|
||||||
|
|
||||||
it("selects no credentials when there isn't any available", () => {
|
|
||||||
// Setup
|
|
||||||
const credentialsStore = useCredentialsStore();
|
const credentialsStore = useCredentialsStore();
|
||||||
credentialsStore.setCredentialTypes([testData.credentialTypeTelegram]);
|
credentialsStore.setCredentialTypes([testData.credentialTypeTelegram]);
|
||||||
const templatesStore = useTemplatesStore();
|
const templatesStore = useTemplatesStore();
|
||||||
templatesStore.addWorkflows([testData.fullShopifyTelegramTwitterTemplate]);
|
templatesStore.addWorkflows([testData.fullShopifyTelegramTwitterTemplate]);
|
||||||
|
const nodeTypesStore = useNodeTypesStore();
|
||||||
|
nodeTypesStore.setNodeTypes([
|
||||||
|
testData.nodeTypeTelegramV1,
|
||||||
|
testData.nodeTypeTwitterV1,
|
||||||
|
testData.nodeTypeShopifyTriggerV1,
|
||||||
|
testData.nodeTypeHttpRequestV1,
|
||||||
|
]);
|
||||||
const setupTemplateStore = useSetupTemplateStore();
|
const setupTemplateStore = useSetupTemplateStore();
|
||||||
setupTemplateStore.setTemplateId(testData.fullShopifyTelegramTwitterTemplate.id.toString());
|
setupTemplateStore.setTemplateId(testData.fullShopifyTelegramTwitterTemplate.id.toString());
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should select no credentials when there isn't any available", () => {
|
||||||
|
// Setup
|
||||||
|
const setupTemplateStore = useSetupTemplateStore();
|
||||||
|
|
||||||
// Execute
|
// Execute
|
||||||
setupTemplateStore.setInitialCredentialSelection();
|
setupTemplateStore.setInitialCredentialSelection();
|
||||||
|
@ -272,16 +261,12 @@ describe('SetupWorkflowFromTemplateView store', () => {
|
||||||
expect(setupTemplateStore.selectedCredentialIdByKey).toEqual({});
|
expect(setupTemplateStore.selectedCredentialIdByKey).toEqual({});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("selects credential when there's only one", () => {
|
it("should select credential when there's only one", () => {
|
||||||
// Setup
|
// Setup
|
||||||
const credentialsStore = useCredentialsStore();
|
const credentialsStore = useCredentialsStore();
|
||||||
credentialsStore.setCredentialTypes([testData.credentialTypeTelegram]);
|
|
||||||
credentialsStore.setCredentials([testData.credentialsTelegram1]);
|
credentialsStore.setCredentials([testData.credentialsTelegram1]);
|
||||||
const templatesStore = useTemplatesStore();
|
|
||||||
templatesStore.addWorkflows([testData.fullShopifyTelegramTwitterTemplate]);
|
|
||||||
|
|
||||||
const setupTemplateStore = useSetupTemplateStore();
|
const setupTemplateStore = useSetupTemplateStore();
|
||||||
setupTemplateStore.setTemplateId(testData.fullShopifyTelegramTwitterTemplate.id.toString());
|
|
||||||
|
|
||||||
// Execute
|
// Execute
|
||||||
setupTemplateStore.setInitialCredentialSelection();
|
setupTemplateStore.setInitialCredentialSelection();
|
||||||
|
@ -291,19 +276,15 @@ describe('SetupWorkflowFromTemplateView store', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('selects no credentials when there are more than 1 available', () => {
|
it('should select no credentials when there are more than 1 available', () => {
|
||||||
// Setup
|
// Setup
|
||||||
const credentialsStore = useCredentialsStore();
|
const credentialsStore = useCredentialsStore();
|
||||||
credentialsStore.setCredentialTypes([testData.credentialTypeTelegram]);
|
|
||||||
credentialsStore.setCredentials([
|
credentialsStore.setCredentials([
|
||||||
testData.credentialsTelegram1,
|
testData.credentialsTelegram1,
|
||||||
testData.credentialsTelegram2,
|
testData.credentialsTelegram2,
|
||||||
]);
|
]);
|
||||||
const templatesStore = useTemplatesStore();
|
|
||||||
templatesStore.addWorkflows([testData.fullShopifyTelegramTwitterTemplate]);
|
|
||||||
|
|
||||||
const setupTemplateStore = useSetupTemplateStore();
|
const setupTemplateStore = useSetupTemplateStore();
|
||||||
setupTemplateStore.setTemplateId(testData.fullShopifyTelegramTwitterTemplate.id.toString());
|
|
||||||
|
|
||||||
// Execute
|
// Execute
|
||||||
setupTemplateStore.setInitialCredentialSelection();
|
setupTemplateStore.setInitialCredentialSelection();
|
||||||
|
@ -312,14 +293,14 @@ describe('SetupWorkflowFromTemplateView store', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test.each([
|
test.each([
|
||||||
['httpBasicAuth'],
|
['httpBasicAuth', 'basicAuth'],
|
||||||
['httpCustomAuth'],
|
['httpCustomAuth', 'basicAuth'],
|
||||||
['httpDigestAuth'],
|
['httpDigestAuth', 'digestAuth'],
|
||||||
['httpHeaderAuth'],
|
['httpHeaderAuth', 'headerAuth'],
|
||||||
['oAuth1Api'],
|
['oAuth1Api', 'oAuth1'],
|
||||||
['oAuth2Api'],
|
['oAuth2Api', 'oAuth2'],
|
||||||
['httpQueryAuth'],
|
['httpQueryAuth', 'queryAuth'],
|
||||||
])('does not auto-select credentials for %s', (credentialType) => {
|
])('should not auto-select credentials for %s', (credentialType, auth) => {
|
||||||
// Setup
|
// Setup
|
||||||
const credentialsStore = useCredentialsStore();
|
const credentialsStore = useCredentialsStore();
|
||||||
credentialsStore.setCredentialTypes([testData.newCredentialType(credentialType)]);
|
credentialsStore.setCredentialTypes([testData.newCredentialType(credentialType)]);
|
||||||
|
@ -338,7 +319,9 @@ describe('SetupWorkflowFromTemplateView store', () => {
|
||||||
credentials: {
|
credentials: {
|
||||||
[credentialType]: 'Test',
|
[credentialType]: 'Test',
|
||||||
},
|
},
|
||||||
parameters: {},
|
parameters: {
|
||||||
|
authentication: auth,
|
||||||
|
},
|
||||||
position: [250, 300],
|
position: [250, 300],
|
||||||
});
|
});
|
||||||
templatesStore.addWorkflows([workflow]);
|
templatesStore.addWorkflows([workflow]);
|
||||||
|
@ -353,4 +336,84 @@ describe('SetupWorkflowFromTemplateView store', () => {
|
||||||
expect(setupTemplateStore.selectedCredentialIdByKey).toEqual({});
|
expect(setupTemplateStore.selectedCredentialIdByKey).toEqual({});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("With template that has nodes requiring credentials but workflow doesn't have them", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
setActivePinia(
|
||||||
|
createTestingPinia({
|
||||||
|
stubActions: false,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Setup
|
||||||
|
const credentialsStore = useCredentialsStore();
|
||||||
|
credentialsStore.setCredentialTypes([testData.credentialTypeTelegram]);
|
||||||
|
const templatesStore = useTemplatesStore();
|
||||||
|
templatesStore.addWorkflows([testData.fullSaveEmailAttachmentsToNextCloudTemplate]);
|
||||||
|
const nodeTypesStore = useNodeTypesStore();
|
||||||
|
nodeTypesStore.setNodeTypes([
|
||||||
|
testData.nodeTypeReadImapV1,
|
||||||
|
testData.nodeTypeReadImapV2,
|
||||||
|
testData.nodeTypeNextCloudV1,
|
||||||
|
]);
|
||||||
|
const setupTemplateStore = useSetupTemplateStore();
|
||||||
|
setupTemplateStore.setTemplateId(
|
||||||
|
testData.fullSaveEmailAttachmentsToNextCloudTemplate.id.toString(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const templateImapNode = testData.fullSaveEmailAttachmentsToNextCloudTemplate.workflow.nodes[0];
|
||||||
|
const templateNextcloudNode =
|
||||||
|
testData.fullSaveEmailAttachmentsToNextCloudTemplate.workflow.nodes[1];
|
||||||
|
|
||||||
|
it('should return correct credential usages', () => {
|
||||||
|
const setupTemplateStore = useSetupTemplateStore();
|
||||||
|
expect(setupTemplateStore.credentialUsages).toEqual([
|
||||||
|
{
|
||||||
|
credentialName: '',
|
||||||
|
credentialType: 'imap',
|
||||||
|
key: 'imap-',
|
||||||
|
nodeTypeName: 'n8n-nodes-base.emailReadImap',
|
||||||
|
usedBy: [templateImapNode],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
credentialName: '',
|
||||||
|
credentialType: 'nextCloudApi',
|
||||||
|
key: 'nextCloudApi-',
|
||||||
|
nodeTypeName: 'n8n-nodes-base.nextCloud',
|
||||||
|
usedBy: [templateNextcloudNode],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return correct app credentials', () => {
|
||||||
|
const setupTemplateStore = useSetupTemplateStore();
|
||||||
|
expect(setupTemplateStore.appCredentials).toEqual([
|
||||||
|
{
|
||||||
|
appName: 'Email (IMAP)',
|
||||||
|
credentials: [
|
||||||
|
{
|
||||||
|
credentialName: '',
|
||||||
|
credentialType: 'imap',
|
||||||
|
key: 'imap-',
|
||||||
|
nodeTypeName: 'n8n-nodes-base.emailReadImap',
|
||||||
|
usedBy: [templateImapNode],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
appName: 'Nextcloud',
|
||||||
|
credentials: [
|
||||||
|
{
|
||||||
|
credentialName: '',
|
||||||
|
credentialType: 'nextCloudApi',
|
||||||
|
key: 'nextCloudApi-',
|
||||||
|
nodeTypeName: 'n8n-nodes-base.nextCloud',
|
||||||
|
usedBy: [templateNextcloudNode],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -4,7 +4,6 @@ import type {
|
||||||
ITemplatesWorkflowFull,
|
ITemplatesWorkflowFull,
|
||||||
IWorkflowTemplateNode,
|
IWorkflowTemplateNode,
|
||||||
} from '@/Interface';
|
} from '@/Interface';
|
||||||
import type { ICredentialType } from 'n8n-workflow';
|
|
||||||
|
|
||||||
export const newFullOneNodeTemplate = (node: IWorkflowTemplateNode): ITemplatesWorkflowFull => ({
|
export const newFullOneNodeTemplate = (node: IWorkflowTemplateNode): ITemplatesWorkflowFull => ({
|
||||||
full: true,
|
full: true,
|
||||||
|
@ -45,183 +44,6 @@ export const newFullOneNodeTemplate = (node: IWorkflowTemplateNode): ITemplatesW
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const fullShopifyTelegramTwitterTemplate: ITemplatesWorkflowFull = {
|
|
||||||
full: true,
|
|
||||||
id: 1205,
|
|
||||||
name: 'Promote new Shopify products on Twitter and Telegram',
|
|
||||||
totalViews: 485,
|
|
||||||
createdAt: '2021-08-24T10:40:50.007Z',
|
|
||||||
description:
|
|
||||||
'This workflow automatically promotes your new Shopify products on Twitter and Telegram. This workflow is also featured in the blog post [*6 e-commerce workflows to power up your Shopify store*](https://n8n.io/blog/no-code-ecommerce-workflow-automations/#promote-your-new-products-on-social-media).\n\n## Prerequisites\n\n- A Shopify account and [credentials](https://docs.n8n.io/integrations/credentials/shopify/)\n- A Twitter account and [credentials](https://docs.n8n.io/integrations/credentials/twitter/)\n- A Telegram account and [credentials](https://docs.n8n.io/integrations/credentials/telegram/) for the channel you want to send messages to.\n\n## Nodes\n\n- [Shopify Trigger node](https://docs.n8n.io/integrations/trigger-nodes/n8n-nodes-base.shopifytrigger/) triggers the workflow when you create a new product in Shopify.\n- [Twitter node](https://docs.n8n.io/integrations/nodes/n8n-nodes-base.twitter/) posts a tweet with the text "Hey there, my design is now on a new product! Visit my {shop name} to get this cool {product title} (and check out more {product type})".\n- [Telegram node](https://docs.n8n.io/integrations/nodes/n8n-nodes-base.telegram/) posts a message with the same text as above in a Telegram channel.',
|
|
||||||
workflow: {
|
|
||||||
nodes: [
|
|
||||||
{
|
|
||||||
name: 'Twitter',
|
|
||||||
type: 'n8n-nodes-base.twitter',
|
|
||||||
position: [720, -220],
|
|
||||||
parameters: {
|
|
||||||
text: '=Hey there, my design is now on a new product ✨\nVisit my {{$json["vendor"]}} shop to get this cool{{$json["title"]}} (and check out more {{$json["product_type"]}}) 🛍️',
|
|
||||||
additionalFields: {},
|
|
||||||
},
|
|
||||||
credentials: {
|
|
||||||
twitterOAuth1Api: 'twitter',
|
|
||||||
},
|
|
||||||
typeVersion: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Telegram',
|
|
||||||
type: 'n8n-nodes-base.telegram',
|
|
||||||
position: [720, -20],
|
|
||||||
parameters: {
|
|
||||||
text: '=Hey there, my design is now on a new product!\nVisit my {{$json["vendor"]}} shop to get this cool{{$json["title"]}} (and check out more {{$json["product_type"]}})',
|
|
||||||
chatId: '123456',
|
|
||||||
additionalFields: {},
|
|
||||||
},
|
|
||||||
credentials: {
|
|
||||||
telegramApi: 'telegram_habot',
|
|
||||||
},
|
|
||||||
typeVersion: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'product created',
|
|
||||||
type: 'n8n-nodes-base.shopifyTrigger',
|
|
||||||
position: [540, -110],
|
|
||||||
webhookId: '2a7e0e50-8f09-4a2b-bf54-a849a6ac4fe0',
|
|
||||||
parameters: {
|
|
||||||
topic: 'products/create',
|
|
||||||
},
|
|
||||||
credentials: {
|
|
||||||
shopifyApi: 'shopify_nodeqa',
|
|
||||||
},
|
|
||||||
typeVersion: 1,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
connections: {
|
|
||||||
'product created': {
|
|
||||||
main: [
|
|
||||||
[
|
|
||||||
{
|
|
||||||
node: 'Twitter',
|
|
||||||
type: 'main',
|
|
||||||
index: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
node: 'Telegram',
|
|
||||||
type: 'main',
|
|
||||||
index: 0,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
workflowInfo: {
|
|
||||||
nodeCount: 3,
|
|
||||||
nodeTypes: {
|
|
||||||
'n8n-nodes-base.twitter': {
|
|
||||||
count: 1,
|
|
||||||
},
|
|
||||||
'n8n-nodes-base.telegram': {
|
|
||||||
count: 1,
|
|
||||||
},
|
|
||||||
'n8n-nodes-base.shopifyTrigger': {
|
|
||||||
count: 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
user: {
|
|
||||||
username: 'lorenanda',
|
|
||||||
},
|
|
||||||
nodes: [
|
|
||||||
{
|
|
||||||
id: 49,
|
|
||||||
icon: 'file:telegram.svg',
|
|
||||||
name: 'n8n-nodes-base.telegram',
|
|
||||||
defaults: {
|
|
||||||
name: 'Telegram',
|
|
||||||
},
|
|
||||||
iconData: {
|
|
||||||
type: 'file',
|
|
||||||
fileBuffer:
|
|
||||||
'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSIwIDAgNjYgNjYiIGZpbGw9IiNmZmYiIGZpbGwtcnVsZT0iZXZlbm9kZCIgc3Ryb2tlPSIjMDAwIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjx1c2UgeGxpbms6aHJlZj0iI2EiIHg9Ii41IiB5PSIuNSIvPjxzeW1ib2wgaWQ9ImEiIG92ZXJmbG93PSJ2aXNpYmxlIj48ZyBzdHJva2U9Im5vbmUiIGZpbGwtcnVsZT0ibm9uemVybyI+PHBhdGggZD0iTTAgMzJjMCAxNy42NzMgMTQuMzI3IDMyIDMyIDMyczMyLTE0LjMyNyAzMi0zMlM0OS42NzMgMCAzMiAwIDAgMTQuMzI3IDAgMzIiIGZpbGw9IiMzN2FlZTIiLz48cGF0aCBkPSJNMjEuNjYxIDM0LjMzOGwzLjc5NyAxMC41MDhzLjQ3NS45ODMuOTgzLjk4MyA4LjA2OC03Ljg2NCA4LjA2OC03Ljg2NGw4LjQwNy0xNi4yMzctMjEuMTE5IDkuODk4eiIgZmlsbD0iI2M4ZGFlYSIvPjxwYXRoIGQ9Ik0yNi42OTUgMzcuMDM0bC0uNzI5IDcuNzQ2cy0uMzA1IDIuMzczIDIuMDY4IDBsNC42NDQtNC4yMDMiIGZpbGw9IiNhOWM2ZDgiLz48cGF0aCBkPSJNMjEuNzMgMzQuNzEybC03LjgwOS0yLjU0NXMtLjkzMi0uMzc4LS42MzMtMS4yMzdjLjA2Mi0uMTc3LjE4Ni0uMzI4LjU1OS0uNTg4IDEuNzMxLTEuMjA2IDMyLjAyOC0xMi4wOTYgMzIuMDI4LTEyLjA5NnMuODU2LS4yODggMS4zNjEtLjA5N2MuMjMxLjA4OC4zNzguMTg3LjUwMy41NDguMDQ1LjEzMi4wNzEuNDExLjA2OC42ODktLjAwMy4yMDEtLjAyNy4zODYtLjA0NS42NzgtLjE4NCAyLjk3OC01LjcwNiAyNS4xOTgtNS43MDYgMjUuMTk4cy0uMzMgMS4zLTEuNTE0IDEuMzQ1Yy0uNDMyLjAxNi0uOTU2LS4wNzEtMS41ODItLjYxLTIuMzIzLTEuOTk4LTEwLjM1Mi03LjM5NC0xMi4xMjYtOC41OGEuMzQuMzQgMCAwMS0uMTQ2LS4yMzljLS4wMjUtLjEyNS4xMDgtLjI4LjEwOC0uMjhzMTMuOTgtMTIuNDI3IDE0LjM1Mi0xMy43MzFjLjAyOS0uMTAxLS4wNzktLjE1MS0uMjI2LS4xMDctLjkyOS4zNDItMTcuMDI1IDEwLjUwNi0xOC44MDEgMTEuNjI5LS4xMDQuMDY2LS4zOTUuMDIzLS4zOTUuMDIzIi8+PC9nPjwvc3ltYm9sPjwvc3ZnPg==',
|
|
||||||
},
|
|
||||||
categories: [
|
|
||||||
{
|
|
||||||
id: 6,
|
|
||||||
name: 'Communication',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
displayName: 'Telegram',
|
|
||||||
typeVersion: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 107,
|
|
||||||
icon: 'file:shopify.svg',
|
|
||||||
name: 'n8n-nodes-base.shopifyTrigger',
|
|
||||||
defaults: {
|
|
||||||
name: 'Shopify Trigger',
|
|
||||||
},
|
|
||||||
iconData: {
|
|
||||||
type: 'file',
|
|
||||||
fileBuffer:
|
|
||||||
'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSIwIDAgNTggNjYiIGZpbGw9IiNmZmYiIGZpbGwtcnVsZT0iZXZlbm9kZCIgc3Ryb2tlPSIjMDAwIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjx1c2UgeGxpbms6aHJlZj0iI2EiIHg9Ii41IiB5PSIuNSIvPjxzeW1ib2wgaWQ9ImEiIG92ZXJmbG93PSJ2aXNpYmxlIj48ZyBzdHJva2U9Im5vbmUiIGZpbGwtcnVsZT0ibm9uemVybyI+PHBhdGggZD0iTTQ5LjI1NSAxMi40ODRhLjYzMy42MzMgMCAwMC0uNTY0LS41MjdjLS4yMjUtLjAzNy01LjE3LS4zNzYtNS4xNy0uMzc2bC0zLjc3LTMuNzdjLS4zNC0uMzc2LTEuMDkyLS4yNjYtMS4zNzYtLjE4OC0uMDM3IDAtLjc1Mi4yMjUtMS45MjIuNjA1LTEuMTM3LTMuMy0zLjE1LTYuMzA2LTYuNjk2LTYuMzA2aC0uMzAzQzI4LjQzOC42MDUgMjcuMTk0IDAgMjYuMTQ0IDBjLTguMjU2LjAzNy0xMi4yIDEwLjMzMy0xMy40MzQgMTUuNTk0bC01Ljc3IDEuNzdjLTEuNzcuNTY0LTEuODM1LjYwNS0yLjA3MyAyLjI5M0wwIDU3LjE3NSAzNi40NjggNjRsMTkuNzYzLTQuMjZjMC0uMDM3LTYuOTQtNDYuODk3LTYuOTc2LTQ3LjI1NXpNMzQuNDMxIDguODZjLS45MTcuMzAzLTEuOTYzLjYwNS0zLjEuOTQ1di0uNjhhMTUuMDMgMTUuMDMgMCAwMC0uNzUyLTQuOTk5YzEuODQ4LjI4NCAzLjEgMi4zNTcgMy44NDMgNC43MzN6bS02LjA2OC00LjI5OGMuNjAzIDEuNzc4Ljg4MyAzLjY1LjgyNiA1LjUyN3YuMzRsLTYuMzc1IDEuOTYzYzEuMjQ4LTQuNjYgMy41NS02Ljk2MiA1LjU1LTcuODN6bS0yLjQ1LTIuMjkzYTEuOTQgMS45NCAwIDAxMS4wNTUuMzM5Yy0yLjY2IDEuMjM4LTUuNDcyIDQuMzY2LTYuNjc4IDEwLjYyN2wtNS4wNDUgMS41NDZDMTYuNjY4IDEwLjAzIDE5Ljk4OCAyLjI2IDI1LjkxIDIuMjZ6IiBmaWxsPSIjOTViZjQ3Ii8+PHBhdGggZD0iTTQ4LjY5MSAxMS45NTdjLS4yMjUtLjAzNy01LjE3LS4zNzYtNS4xNy0uMzc2bC0zLjc3LTMuNzdhLjc1My43NTMgMCAwMC0uNTI3LS4yMjVMMzYuNDcyIDY0bDE5Ljc2My00LjI2LTYuOTgtNDcuMjE4YS42OC42OCAwIDAwLS41NjQtLjU2NHoiIGZpbGw9IiM1ZThlM2UiLz48cGF0aCBkPSJNMjkuNzU4IDIyLjlsLTIuNDU0IDcuMjQyYTExLjM2IDExLjM2IDAgMDAtNC43NTItMS4xMzNjLTMuODQ4IDAtNC4wMzYgMi40MTItNC4wMzYgMy4wMTggMCAzLjI5OCA4LjYzNiA0LjU2NCA4LjYzNiAxMi4zMzMgMCA2LjEtMy44ODUgMTAuMDMtOS4xIDEwLjAzLTYuMjYgMC05LjQ2Ny0zLjg4NS05LjQ2Ny0zLjg4NWwxLjY2NS01LjU1czMuMjggMi44MyA2LjA3MyAyLjgzYTIuNDcgMi40NyAwIDAwMi41NjQtMi40OWMwLTQuMzQtNy4xLTQuNTI3LTcuMS0xMS42MTggMC01Ljk2MiA0LjI5OC0xMS43NyAxMi45MzQtMTEuNzcgMy4zOTQuMDUgNS4wMTggMSA1LjAxOCAxeiIvPjwvZz48L3N5bWJvbD48L3N2Zz4=',
|
|
||||||
},
|
|
||||||
categories: [
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
name: 'Sales',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
displayName: 'Shopify Trigger',
|
|
||||||
typeVersion: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 325,
|
|
||||||
icon: 'file:x.svg',
|
|
||||||
name: 'n8n-nodes-base.twitter',
|
|
||||||
defaults: {
|
|
||||||
name: 'X',
|
|
||||||
},
|
|
||||||
iconData: {
|
|
||||||
type: 'file',
|
|
||||||
fileBuffer:
|
|
||||||
'data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMjQgMjQiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTE4LjI0NCAyLjI1aDMuMzA4bC03LjIyNyA4LjI2IDguNTAyIDExLjI0SDE2LjE3bC01LjIxNC02LjgxN0w0Ljk5IDIxLjc1SDEuNjhsNy43My04LjgzNUwxLjI1NCAyLjI1SDguMDhsNC43MTMgNi4yMzF6bS0xLjE2MSAxNy41MmgxLjgzM0w3LjA4NCA0LjEyNkg1LjExN3oiPjwvcGF0aD48L3N2Zz4K',
|
|
||||||
},
|
|
||||||
categories: [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
name: 'Marketing & Content',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
displayName: 'X (Formerly Twitter)',
|
|
||||||
typeVersion: 2,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
categories: [
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
name: 'Sales',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 19,
|
|
||||||
name: 'Marketing & Growth',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
image: [
|
|
||||||
{
|
|
||||||
id: 527,
|
|
||||||
url: 'https://n8niostorageaccount.blob.core.windows.net/n8nio-strapi-blobs-prod/assets/89a078b208fe4c6181902608b1cd1332.png',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const newCredentialType = (name: string): ICredentialType => ({
|
|
||||||
name,
|
|
||||||
displayName: name,
|
|
||||||
documentationUrl: name,
|
|
||||||
properties: [],
|
|
||||||
});
|
|
||||||
|
|
||||||
export const newCredential = (
|
export const newCredential = (
|
||||||
opts: Pick<ICredentialsResponse, 'type'> & Partial<ICredentialsResponse>,
|
opts: Pick<ICredentialsResponse, 'type'> & Partial<ICredentialsResponse>,
|
||||||
): ICredentialsResponse => ({
|
): ICredentialsResponse => ({
|
||||||
|
@ -233,31 +55,6 @@ export const newCredential = (
|
||||||
...opts,
|
...opts,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const credentialTypeTelegram: ICredentialType = {
|
|
||||||
name: 'telegramApi',
|
|
||||||
displayName: 'Telegram API',
|
|
||||||
documentationUrl: 'telegram',
|
|
||||||
properties: [
|
|
||||||
{
|
|
||||||
displayName: 'Access Token',
|
|
||||||
name: 'accessToken',
|
|
||||||
type: 'string',
|
|
||||||
typeOptions: {
|
|
||||||
password: true,
|
|
||||||
},
|
|
||||||
default: '',
|
|
||||||
description:
|
|
||||||
'Chat with the <a href="https://telegram.me/botfather">bot father</a> to obtain the access token',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
test: {
|
|
||||||
request: {
|
|
||||||
baseURL: '=https://api.telegram.org/bot{{$credentials.accessToken}}',
|
|
||||||
url: '/getMe',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const credentialsTelegram1: ICredentialsResponse = {
|
export const credentialsTelegram1: ICredentialsResponse = {
|
||||||
createdAt: '2023-11-23T14:26:07.969Z',
|
createdAt: '2023-11-23T14:26:07.969Z',
|
||||||
updatedAt: '2023-11-23T14:26:07.964Z',
|
updatedAt: '2023-11-23T14:26:07.964Z',
|
||||||
|
@ -307,3 +104,22 @@ export const credentialsTelegram2: ICredentialsResponse = {
|
||||||
},
|
},
|
||||||
sharedWith: [],
|
sharedWith: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
fullSaveEmailAttachmentsToNextCloudTemplate,
|
||||||
|
fullShopifyTelegramTwitterTemplate,
|
||||||
|
} from '@/utils/testData/templateTestData';
|
||||||
|
|
||||||
|
export { credentialTypeTelegram, newCredentialType } from '@/utils/testData/credentialTypeTestData';
|
||||||
|
|
||||||
|
export {
|
||||||
|
nodeTypeHttpRequestV1,
|
||||||
|
nodeTypeNextCloudV1,
|
||||||
|
nodeTypeReadImapV1,
|
||||||
|
nodeTypeReadImapV2,
|
||||||
|
nodeTypeShopifyTriggerV1,
|
||||||
|
nodeTypeTelegramV1,
|
||||||
|
nodeTypeTelegramV1_1,
|
||||||
|
nodeTypeTwitterV1,
|
||||||
|
nodeTypeTwitterV2,
|
||||||
|
} from '@/utils/testData/nodeTypeTestData';
|
||||||
|
|
|
@ -8,7 +8,11 @@ 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 { INodeCredentialsDetails, INodeTypeDescription } from 'n8n-workflow';
|
import type {
|
||||||
|
INodeCredentialDescription,
|
||||||
|
INodeCredentialsDetails,
|
||||||
|
INodeTypeDescription,
|
||||||
|
} from 'n8n-workflow';
|
||||||
import type {
|
import type {
|
||||||
ICredentialsResponse,
|
ICredentialsResponse,
|
||||||
INodeUi,
|
INodeUi,
|
||||||
|
@ -17,17 +21,15 @@ import type {
|
||||||
} from '@/Interface';
|
} from '@/Interface';
|
||||||
import { VIEWS } from '@/constants';
|
import { VIEWS } from '@/constants';
|
||||||
import { createWorkflowFromTemplate } from '@/utils/templates/templateActions';
|
import { createWorkflowFromTemplate } from '@/utils/templates/templateActions';
|
||||||
import type {
|
import type { TemplateCredentialKey } from '@/utils/templates/templateTransforms';
|
||||||
TemplateCredentialKey,
|
|
||||||
IWorkflowTemplateNodeWithCredentials,
|
|
||||||
} from '@/utils/templates/templateTransforms';
|
|
||||||
import {
|
import {
|
||||||
hasNodeCredentials,
|
|
||||||
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;
|
||||||
|
@ -62,33 +64,56 @@ 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 = (
|
export const getNodesRequiringCredentials = (
|
||||||
|
nodeTypeProvider: NodeTypeProvider,
|
||||||
template: ITemplatesWorkflowFull,
|
template: ITemplatesWorkflowFull,
|
||||||
): IWorkflowTemplateNodeWithCredentials[] => {
|
): TemplateNodeWithRequiredCredential[] => {
|
||||||
if (!template) {
|
if (!template) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return template.workflow.nodes.filter(hasNodeCredentials);
|
const nodesWithCredentials: TemplateNodeWithRequiredCredential[] = template.workflow.nodes
|
||||||
|
.map((node) => ({
|
||||||
|
node,
|
||||||
|
requiredCredentials: getNodeTypeDisplayableCredentials(nodeTypeProvider, node),
|
||||||
|
}))
|
||||||
|
.filter(({ requiredCredentials }) => requiredCredentials.length > 0);
|
||||||
|
|
||||||
|
return nodesWithCredentials;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const groupNodeCredentialsByKey = (nodes: IWorkflowTemplateNodeWithCredentials[]) => {
|
export const groupNodeCredentialsByKey = (
|
||||||
|
nodeWithRequiredCredentials: TemplateNodeWithRequiredCredential[],
|
||||||
|
) => {
|
||||||
const credentialsByTypeName = new Map<TemplateCredentialKey, CredentialUsages>();
|
const credentialsByTypeName = new Map<TemplateCredentialKey, CredentialUsages>();
|
||||||
|
|
||||||
for (const node of nodes) {
|
for (const { node, requiredCredentials } of nodeWithRequiredCredentials) {
|
||||||
const normalizedCreds = normalizeTemplateNodeCredentials(node.credentials);
|
const normalizedNodeCreds = node.credentials
|
||||||
for (const credentialType in normalizedCreds) {
|
? normalizeTemplateNodeCredentials(node.credentials)
|
||||||
const credentialName = normalizedCreds[credentialType];
|
: {};
|
||||||
const key = keyFromCredentialTypeAndName(credentialType, credentialName);
|
|
||||||
|
for (const credentialDescription of requiredCredentials) {
|
||||||
|
const credentialType = credentialDescription.name;
|
||||||
|
const nodeCredentialName = normalizedNodeCreds[credentialDescription.name] ?? '';
|
||||||
|
const key = keyFromCredentialTypeAndName(credentialType, nodeCredentialName);
|
||||||
|
|
||||||
let credentialUsages = credentialsByTypeName.get(key);
|
let credentialUsages = credentialsByTypeName.get(key);
|
||||||
if (!credentialUsages) {
|
if (!credentialUsages) {
|
||||||
credentialUsages = {
|
credentialUsages = {
|
||||||
key,
|
key,
|
||||||
nodeTypeName: node.type,
|
nodeTypeName: node.type,
|
||||||
credentialName,
|
credentialName: nodeCredentialName,
|
||||||
credentialType,
|
credentialType,
|
||||||
usedBy: [],
|
usedBy: [],
|
||||||
};
|
};
|
||||||
|
@ -184,10 +209,12 @@ export const useSetupTemplateStore = defineStore('setupTemplate', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const nodesRequiringCredentialsSorted = computed(() => {
|
const nodesRequiringCredentialsSorted = computed(() => {
|
||||||
const credentials = template.value ? getNodesRequiringCredentials(template.value) : [];
|
const nodesWithCredentials = template.value
|
||||||
|
? getNodesRequiringCredentials(nodeTypesStore, template.value)
|
||||||
|
: [];
|
||||||
|
|
||||||
// Order by the X coordinate of the node
|
// Order by the X coordinate of the node
|
||||||
return sortBy(credentials, ({ position }) => position[0]);
|
return sortBy(nodesWithCredentials, ({ node }) => node.position[0]);
|
||||||
});
|
});
|
||||||
|
|
||||||
const appNameByNodeType = (nodeTypeName: string, version?: number) => {
|
const appNameByNodeType = (nodeTypeName: string, version?: number) => {
|
||||||
|
@ -339,12 +366,13 @@ export const useSetupTemplateStore = defineStore('setupTemplate', () => {
|
||||||
try {
|
try {
|
||||||
isSaving.value = true;
|
isSaving.value = true;
|
||||||
|
|
||||||
const createdWorkflow = await createWorkflowFromTemplate(
|
const createdWorkflow = await createWorkflowFromTemplate({
|
||||||
template.value,
|
template: template.value,
|
||||||
credentialOverrides.value,
|
credentialOverrides: credentialOverrides.value,
|
||||||
rootStore,
|
rootStore,
|
||||||
workflowsStore,
|
workflowsStore,
|
||||||
);
|
nodeTypeProvider: nodeTypesStore,
|
||||||
|
});
|
||||||
|
|
||||||
telemetry.track('User closed cred setup', {
|
telemetry.track('User closed cred setup', {
|
||||||
completed: true,
|
completed: true,
|
||||||
|
|
|
@ -293,7 +293,7 @@ export function applySpecialNodeParameters(nodeType: INodeType): void {
|
||||||
export function displayParameter(
|
export function displayParameter(
|
||||||
nodeValues: INodeParameters,
|
nodeValues: INodeParameters,
|
||||||
parameter: INodeProperties | INodeCredentialDescription,
|
parameter: INodeProperties | INodeCredentialDescription,
|
||||||
node: INode | null, // Allow null as it does also get used by credentials and they do not have versioning yet
|
node: Pick<INode, 'typeVersion'> | null, // Allow null as it does also get used by credentials and they do not have versioning yet
|
||||||
nodeValuesRoot?: INodeParameters,
|
nodeValuesRoot?: INodeParameters,
|
||||||
) {
|
) {
|
||||||
if (!parameter.displayOptions) {
|
if (!parameter.displayOptions) {
|
||||||
|
@ -391,7 +391,7 @@ export function displayParameterPath(
|
||||||
nodeValues: INodeParameters,
|
nodeValues: INodeParameters,
|
||||||
parameter: INodeProperties | INodeCredentialDescription,
|
parameter: INodeProperties | INodeCredentialDescription,
|
||||||
path: string,
|
path: string,
|
||||||
node: INode | null,
|
node: Pick<INode, 'typeVersion'> | null,
|
||||||
) {
|
) {
|
||||||
let resolvedNodeValues = nodeValues;
|
let resolvedNodeValues = nodeValues;
|
||||||
if (path !== '') {
|
if (path !== '') {
|
||||||
|
@ -567,7 +567,7 @@ export function getNodeParameters(
|
||||||
nodeValues: INodeParameters | null,
|
nodeValues: INodeParameters | null,
|
||||||
returnDefaults: boolean,
|
returnDefaults: boolean,
|
||||||
returnNoneDisplayed: boolean,
|
returnNoneDisplayed: boolean,
|
||||||
node: INode | null,
|
node: Pick<INode, 'typeVersion'> | null,
|
||||||
onlySimpleTypes = false,
|
onlySimpleTypes = false,
|
||||||
dataIsResolved = false,
|
dataIsResolved = false,
|
||||||
nodeValuesRoot?: INodeParameters,
|
nodeValuesRoot?: INodeParameters,
|
||||||
|
|
Loading…
Reference in a new issue