From 3cf6704dbb347cf4d59848cad508db926c54bc4b Mon Sep 17 00:00:00 2001
From: Tomi Turtiainen <10324676+tomi@users.noreply.github.com>
Date: Fri, 5 Jan 2024 18:07:57 +0200
Subject: [PATCH] feat: Enable cred setup for workflows created from templates
(no-changelog) (#8240)
## Summary
Enable users to open credential setup for workflows that have been
created from templates if they skip it.
Next steps (will be their own PRs):
- Add telemetry events
- Add e2e test
- Hide the button when user sets up all the credentials
- Change the feature flag to a new one
## Related tickets and issues
https://linear.app/n8n/issue/ADO-1637/feature-support-template-credential-setup-for-http-request-nodes-that
---
cypress/e2e/16-form-trigger-node.cy.ts | 5 +-
.../CredentialPicker/CredentialPicker.vue | 27 +-
.../src/components/MainHeader/MainHeader.vue | 1 -
packages/editor-ui/src/components/Modal.vue | 3 +-
packages/editor-ui/src/components/Modals.vue | 14 ++
.../SetupWorkflowCredentialsButton.vue | 44 ++++
.../SetupWorkflowCredentialsModal.vue | 89 +++++++
.../useSetupWorkflowCredentialsModalState.ts | 129 ++++++++++
packages/editor-ui/src/constants.ts | 1 +
.../src/plugins/i18n/locales/en.json | 4 +-
packages/editor-ui/src/stores/ui.store.ts | 156 ++++++------
.../src/utils/nodes/nodeTransforms.ts | 9 +
.../src/utils/templates/templateActions.ts | 12 +-
.../src/utils/templates/templateTransforms.ts | 34 +--
packages/editor-ui/src/views/NodeView.vue | 16 ++
.../AppsRequiringCredsNotice.vue | 18 +-
.../SetupTemplateFormStep.vue | 49 ++--
.../SetupWorkflowFromTemplateView.vue | 17 +-
.../__tests__/setupTemplate.store.test.ts | 222 +----------------
.../__tests__/useCredentialSetupState.test.ts | 141 +++++++++++
.../setupTemplate.store.ts | 196 ++-------------
.../useCredentialSetupState.ts | 231 ++++++++++++++++++
22 files changed, 858 insertions(+), 560 deletions(-)
create mode 100644 packages/editor-ui/src/components/SetupWorkflowCredentialsButton/SetupWorkflowCredentialsButton.vue
create mode 100644 packages/editor-ui/src/components/SetupWorkflowCredentialsModal/SetupWorkflowCredentialsModal.vue
create mode 100644 packages/editor-ui/src/components/SetupWorkflowCredentialsModal/useSetupWorkflowCredentialsModalState.ts
create mode 100644 packages/editor-ui/src/views/SetupWorkflowFromTemplateView/__tests__/useCredentialSetupState.test.ts
create mode 100644 packages/editor-ui/src/views/SetupWorkflowFromTemplateView/useCredentialSetupState.ts
diff --git a/cypress/e2e/16-form-trigger-node.cy.ts b/cypress/e2e/16-form-trigger-node.cy.ts
index 1ec2abc640..8226df6b33 100644
--- a/cypress/e2e/16-form-trigger-node.cy.ts
+++ b/cypress/e2e/16-form-trigger-node.cy.ts
@@ -11,10 +11,7 @@ describe('n8n Form Trigger', () => {
it("add node by clicking on 'On form submission'", () => {
workflowPage.getters.canvasPlusButton().click();
- cy.get('#node-view-root > div:nth-child(2) > div > div > aside ')
- .find('span')
- .contains('On form submission')
- .click();
+ workflowPage.getters.nodeCreatorNodeItems().contains('On form submission').click();
ndv.getters.parameterInput('formTitle').type('Test Form');
ndv.getters.parameterInput('formDescription').type('Test Form Description');
ndv.getters.parameterInput('fieldLabel').type('Test Field 1');
diff --git a/packages/editor-ui/src/components/CredentialPicker/CredentialPicker.vue b/packages/editor-ui/src/components/CredentialPicker/CredentialPicker.vue
index 4ffb4d431d..1f3980aca8 100644
--- a/packages/editor-ui/src/components/CredentialPicker/CredentialPicker.vue
+++ b/packages/editor-ui/src/components/CredentialPicker/CredentialPicker.vue
@@ -1,10 +1,11 @@
diff --git a/packages/editor-ui/src/components/MainHeader/MainHeader.vue b/packages/editor-ui/src/components/MainHeader/MainHeader.vue
index 8d223d188e..e4b171555b 100644
--- a/packages/editor-ui/src/components/MainHeader/MainHeader.vue
+++ b/packages/editor-ui/src/components/MainHeader/MainHeader.vue
@@ -154,7 +154,6 @@ export default defineComponent({
params: { name: routeWorkflowId },
});
}
- // this.modalBus.emit('closeAll');
this.activeHeaderTab = MAIN_HEADER_TABS.EXECUTIONS;
break;
default:
diff --git a/packages/editor-ui/src/components/Modal.vue b/packages/editor-ui/src/components/Modal.vue
index d1bd99f3a1..5c7ddeff3b 100644
--- a/packages/editor-ui/src/components/Modal.vue
+++ b/packages/editor-ui/src/components/Modal.vue
@@ -134,7 +134,6 @@ export default defineComponent({
window.addEventListener('keydown', this.onWindowKeydown);
this.eventBus?.on('close', this.closeDialog);
- this.eventBus?.on('closeAll', this.uiStore.closeAllModals);
const activeElement = document.activeElement as HTMLElement;
if (activeElement) {
@@ -143,7 +142,6 @@ export default defineComponent({
},
beforeUnmount() {
this.eventBus?.off('close', this.closeDialog);
- this.eventBus?.off('closeAll', this.uiStore.closeAllModals);
window.removeEventListener('keydown', this.onWindowKeydown);
},
computed: {
@@ -227,6 +225,7 @@ export default defineComponent({
.modal-content {
overflow: hidden;
+ overflow-y: auto;
flex-grow: 1;
}
diff --git a/packages/editor-ui/src/components/Modals.vue b/packages/editor-ui/src/components/Modals.vue
index 29b0d23c4f..e1454215b2 100644
--- a/packages/editor-ui/src/components/Modals.vue
+++ b/packages/editor-ui/src/components/Modals.vue
@@ -162,6 +162,16 @@
/>
+
+
+
+
+
+
@@ -197,6 +207,7 @@ import {
MFA_SETUP_MODAL_KEY,
WORKFLOW_HISTORY_VERSION_RESTORE,
SUGGESTED_TEMPLATES_PREVIEW_MODAL_KEY,
+ SETUP_CREDENTIALS_MODAL_KEY,
} from '@/constants';
import AboutModal from './AboutModal.vue';
@@ -229,6 +240,7 @@ import ExternalSecretsProviderModal from '@/components/ExternalSecretsProviderMo
import DebugPaywallModal from '@/components/DebugPaywallModal.vue';
import WorkflowHistoryVersionRestoreModal from '@/components/WorkflowHistory/WorkflowHistoryVersionRestoreModal.vue';
import SuggestedTemplatesPreviewModal from '@/components/SuggestedTemplates/SuggestedTemplatesPreviewModal.vue';
+import SetupWorkflowCredentialsModal from '@/components/SetupWorkflowCredentialsModal/SetupWorkflowCredentialsModal.vue';
export default defineComponent({
name: 'Modals',
@@ -263,6 +275,7 @@ export default defineComponent({
MfaSetupModal,
WorkflowHistoryVersionRestoreModal,
SuggestedTemplatesPreviewModal,
+ SetupWorkflowCredentialsModal,
},
data: () => ({
CHAT_EMBED_MODAL_KEY,
@@ -294,6 +307,7 @@ export default defineComponent({
MFA_SETUP_MODAL_KEY,
WORKFLOW_HISTORY_VERSION_RESTORE,
SUGGESTED_TEMPLATES_PREVIEW_MODAL_KEY,
+ SETUP_CREDENTIALS_MODAL_KEY,
}),
});
diff --git a/packages/editor-ui/src/components/SetupWorkflowCredentialsButton/SetupWorkflowCredentialsButton.vue b/packages/editor-ui/src/components/SetupWorkflowCredentialsButton/SetupWorkflowCredentialsButton.vue
new file mode 100644
index 0000000000..8d7bc0e53a
--- /dev/null
+++ b/packages/editor-ui/src/components/SetupWorkflowCredentialsButton/SetupWorkflowCredentialsButton.vue
@@ -0,0 +1,44 @@
+
+
+
+
+
diff --git a/packages/editor-ui/src/components/SetupWorkflowCredentialsModal/SetupWorkflowCredentialsModal.vue b/packages/editor-ui/src/components/SetupWorkflowCredentialsModal/SetupWorkflowCredentialsModal.vue
new file mode 100644
index 0000000000..2a0bf164ca
--- /dev/null
+++ b/packages/editor-ui/src/components/SetupWorkflowCredentialsModal/SetupWorkflowCredentialsModal.vue
@@ -0,0 +1,89 @@
+
+
+
+
+
+
+ {{ i18n.baseText('setupCredentialsModal.title') }}
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/editor-ui/src/components/SetupWorkflowCredentialsModal/useSetupWorkflowCredentialsModalState.ts b/packages/editor-ui/src/components/SetupWorkflowCredentialsModal/useSetupWorkflowCredentialsModalState.ts
new file mode 100644
index 0000000000..cd5657f015
--- /dev/null
+++ b/packages/editor-ui/src/components/SetupWorkflowCredentialsModal/useSetupWorkflowCredentialsModalState.ts
@@ -0,0 +1,129 @@
+import { computed } from 'vue';
+import type { INodeCredentialsDetails } from 'n8n-workflow';
+import { useNodeHelpers } from '@/composables/useNodeHelpers';
+import { useCredentialsStore } from '@/stores/credentials.store';
+import { useWorkflowsStore } from '@/stores/workflows.store';
+import type { TemplateCredentialKey } from '@/utils/templates/templateTransforms';
+import { useCredentialSetupState } from '@/views/SetupWorkflowFromTemplateView/useCredentialSetupState';
+
+export const useSetupWorkflowCredentialsModalState = () => {
+ const workflowsStore = useWorkflowsStore();
+ const credentialsStore = useCredentialsStore();
+ const nodeHelpers = useNodeHelpers();
+
+ const workflowNodes = computed(() => {
+ return workflowsStore.allNodes;
+ });
+
+ const {
+ appCredentials,
+ credentialOverrides,
+ credentialUsages,
+ credentialsByKey,
+ numFilledCredentials,
+ selectedCredentialIdByKey,
+ setSelectedCredentialId,
+ unsetSelectedCredential,
+ } = useCredentialSetupState(workflowNodes);
+
+ /**
+ * Selects initial credentials. For existing workflows this means using
+ * the credentials that are already set on the nodes.
+ */
+ const setInitialCredentialSelection = () => {
+ selectedCredentialIdByKey.value = {};
+
+ for (const credUsage of credentialUsages.value) {
+ const typeCredentials = credentialsStore.getCredentialsByType(credUsage.credentialType);
+ // Make sure there is a credential for this type with the given name
+ const credential = typeCredentials.find((cred) => cred.name === credUsage.credentialName);
+ if (!credential) {
+ continue;
+ }
+
+ selectedCredentialIdByKey.value[credUsage.key] = credential.id;
+ }
+ };
+
+ /**
+ * Sets the given credential to all nodes that use it.
+ */
+ const setCredential = (credentialKey: TemplateCredentialKey, credentialId: string) => {
+ setSelectedCredentialId(credentialKey, credentialId);
+
+ const usages = credentialsByKey.value.get(credentialKey);
+ if (!usages) {
+ return;
+ }
+
+ const credentialName = credentialsStore.getCredentialById(credentialId)?.name;
+ const credential: INodeCredentialsDetails = {
+ id: credentialId,
+ name: credentialName,
+ };
+
+ usages.usedBy.forEach((node) => {
+ workflowsStore.updateNodeProperties({
+ name: node.name,
+ properties: {
+ position: node.position,
+ credentials: {
+ ...node.credentials,
+ [usages.credentialType]: credential,
+ },
+ },
+ });
+
+ // We can't use updateNodeCredentialIssues because the previous
+ // step creates a new instance of the node in the store and
+ // `node` no longer points to the correct node.
+ nodeHelpers.updateNodeCredentialIssuesByName(node.name);
+ });
+
+ setInitialCredentialSelection();
+ };
+
+ /**
+ * Removes given credential from all nodes that use it.
+ */
+ const unsetCredential = (credentialKey: TemplateCredentialKey) => {
+ unsetSelectedCredential(credentialKey);
+
+ const usages = credentialsByKey.value.get(credentialKey);
+ if (!usages) {
+ return;
+ }
+
+ usages.usedBy.forEach((node) => {
+ const credentials = { ...node.credentials };
+ delete credentials[usages.credentialType];
+
+ workflowsStore.updateNodeProperties({
+ name: node.name,
+ properties: {
+ position: node.position,
+ credentials,
+ },
+ });
+
+ // We can't use updateNodeCredentialIssues because the previous
+ // step creates a new instance of the node in the store and
+ // `node` no longer points to the correct node.
+ nodeHelpers.updateNodeCredentialIssuesByName(node.name);
+ });
+
+ setInitialCredentialSelection();
+ };
+
+ return {
+ appCredentials,
+ credentialOverrides,
+ credentialUsages,
+ credentialsByKey,
+ numFilledCredentials,
+ selectedCredentialIdByKey,
+ setInitialCredentialSelection,
+ setCredential,
+ unsetCredential,
+ };
+};
diff --git a/packages/editor-ui/src/constants.ts b/packages/editor-ui/src/constants.ts
index 68693fb573..42371e9946 100644
--- a/packages/editor-ui/src/constants.ts
+++ b/packages/editor-ui/src/constants.ts
@@ -57,6 +57,7 @@ export const DEBUG_PAYWALL_MODAL_KEY = 'debugPaywall';
export const MFA_SETUP_MODAL_KEY = 'mfaSetup';
export const WORKFLOW_HISTORY_VERSION_RESTORE = 'workflowHistoryVersionRestore';
export const SUGGESTED_TEMPLATES_PREVIEW_MODAL_KEY = 'suggestedTemplatePreview';
+export const SETUP_CREDENTIALS_MODAL_KEY = 'setupCredentials';
export const EXTERNAL_SECRETS_PROVIDER_MODAL_KEY = 'externalSecretsProvider';
diff --git a/packages/editor-ui/src/plugins/i18n/locales/en.json b/packages/editor-ui/src/plugins/i18n/locales/en.json
index 6bd1383745..562eae33bf 100644
--- a/packages/editor-ui/src/plugins/i18n/locales/en.json
+++ b/packages/editor-ui/src/plugins/i18n/locales/en.json
@@ -1095,6 +1095,7 @@
"nodeView.zoomOut": "Zoom Out",
"nodeView.zoomToFit": "Zoom to Fit",
"nodeView.replaceMe": "Replace Me",
+ "nodeView.setupTemplate": "Set up Template",
"contextMenu.node": "node | nodes",
"contextMenu.sticky": "sticky note | sticky notes",
"contextMenu.selectAll": "Select all",
@@ -2359,5 +2360,6 @@
"templateSetup.skip": "Skip",
"templateSetup.continue.button": "Continue",
"templateSetup.credential.description": "The credential you select will be used in the {0} node of the workflow template. | The credential you select will be used in the {0} nodes of the workflow template.",
- "templateSetup.continue.button.fillRemaining": "Fill remaining credentials to continue"
+ "templateSetup.continue.button.fillRemaining": "Fill remaining credentials to continue",
+ "setupCredentialsModal.title": "Setup credentials"
}
diff --git a/packages/editor-ui/src/stores/ui.store.ts b/packages/editor-ui/src/stores/ui.store.ts
index 9667f80e5c..e127d36cdb 100644
--- a/packages/editor-ui/src/stores/ui.store.ts
+++ b/packages/editor-ui/src/stores/ui.store.ts
@@ -38,6 +38,7 @@ import {
N8N_PRICING_PAGE_URL,
WORKFLOW_HISTORY_VERSION_RESTORE,
SUGGESTED_TEMPLATES_PREVIEW_MODAL_KEY,
+ SETUP_CREDENTIALS_MODAL_KEY,
} from '@/constants';
import type {
CloudUpdateLinkSourceType,
@@ -84,67 +85,47 @@ try {
}
} catch (e) {}
+export type UiStore = ReturnType;
+
export const useUIStore = defineStore(STORES.UI, {
state: (): UIState => ({
activeActions: [],
activeCredentialType: null,
theme: savedTheme,
modals: {
- [ABOUT_MODAL_KEY]: {
- open: false,
- },
- [CHAT_EMBED_MODAL_KEY]: {
- open: false,
- },
- [CHANGE_PASSWORD_MODAL_KEY]: {
- open: false,
- },
- [CONTACT_PROMPT_MODAL_KEY]: {
- open: false,
- },
- [CREDENTIAL_SELECT_MODAL_KEY]: {
- open: false,
- },
+ ...Object.fromEntries(
+ [
+ ABOUT_MODAL_KEY,
+ CHAT_EMBED_MODAL_KEY,
+ CHANGE_PASSWORD_MODAL_KEY,
+ CONTACT_PROMPT_MODAL_KEY,
+ CREDENTIAL_SELECT_MODAL_KEY,
+ DUPLICATE_MODAL_KEY,
+ ONBOARDING_CALL_SIGNUP_MODAL_KEY,
+ PERSONALIZATION_MODAL_KEY,
+ INVITE_USER_MODAL_KEY,
+ TAGS_MANAGER_MODAL_KEY,
+ VALUE_SURVEY_MODAL_KEY,
+ VERSIONS_MODAL_KEY,
+ WORKFLOW_LM_CHAT_MODAL_KEY,
+ WORKFLOW_SETTINGS_MODAL_KEY,
+ WORKFLOW_SHARE_MODAL_KEY,
+ WORKFLOW_ACTIVE_MODAL_KEY,
+ COMMUNITY_PACKAGE_INSTALL_MODAL_KEY,
+ MFA_SETUP_MODAL_KEY,
+ SOURCE_CONTROL_PUSH_MODAL_KEY,
+ SOURCE_CONTROL_PULL_MODAL_KEY,
+ EXTERNAL_SECRETS_PROVIDER_MODAL_KEY,
+ DEBUG_PAYWALL_MODAL_KEY,
+ WORKFLOW_HISTORY_VERSION_RESTORE,
+ SUGGESTED_TEMPLATES_PREVIEW_MODAL_KEY,
+ SETUP_CREDENTIALS_MODAL_KEY,
+ ].map((modalKey) => [modalKey, { open: false }]),
+ ),
[DELETE_USER_MODAL_KEY]: {
open: false,
activeId: null,
},
- [DUPLICATE_MODAL_KEY]: {
- open: false,
- },
- [ONBOARDING_CALL_SIGNUP_MODAL_KEY]: {
- open: false,
- },
- [PERSONALIZATION_MODAL_KEY]: {
- open: false,
- },
- [INVITE_USER_MODAL_KEY]: {
- open: false,
- },
- [TAGS_MANAGER_MODAL_KEY]: {
- open: false,
- },
- [VALUE_SURVEY_MODAL_KEY]: {
- open: false,
- },
- [VERSIONS_MODAL_KEY]: {
- open: false,
- },
- [WORKFLOW_LM_CHAT_MODAL_KEY]: {
- open: false,
- },
- [WORKFLOW_SETTINGS_MODAL_KEY]: {
- open: false,
- },
- [WORKFLOW_SHARE_MODAL_KEY]: {
- open: false,
- },
- [WORKFLOW_ACTIVE_MODAL_KEY]: {
- open: false,
- },
- [COMMUNITY_PACKAGE_INSTALL_MODAL_KEY]: {
- open: false,
- },
[COMMUNITY_PACKAGE_CONFIRM_MODAL_KEY]: {
open: false,
mode: '',
@@ -155,9 +136,6 @@ export const useUIStore = defineStore(STORES.UI, {
curlCommand: '',
httpNodeParameters: '',
},
- [MFA_SETUP_MODAL_KEY]: {
- open: false,
- },
[LOG_STREAM_MODAL_KEY]: {
open: false,
data: undefined,
@@ -168,24 +146,6 @@ export const useUIStore = defineStore(STORES.UI, {
activeId: null,
showAuthSelector: false,
},
- [SOURCE_CONTROL_PUSH_MODAL_KEY]: {
- open: false,
- },
- [SOURCE_CONTROL_PULL_MODAL_KEY]: {
- open: false,
- },
- [EXTERNAL_SECRETS_PROVIDER_MODAL_KEY]: {
- open: false,
- },
- [DEBUG_PAYWALL_MODAL_KEY]: {
- open: false,
- },
- [WORKFLOW_HISTORY_VERSION_RESTORE]: {
- open: false,
- },
- [SUGGESTED_TEMPLATES_PREVIEW_MODAL_KEY]: {
- open: false,
- },
},
modalStack: [],
sidebarMenuCollapsed: true,
@@ -463,17 +423,6 @@ export const useUIStore = defineStore(STORES.UI, {
return name !== openModalName;
});
},
- closeAllModals(): void {
- Object.keys(this.modals).forEach((name) => {
- if (this.modals[name].open) {
- this.modals[name] = {
- ...this.modals[name],
- open: false,
- };
- }
- });
- this.modalStack = [];
- },
draggableStartDragging(type: string, data: string): void {
this.draggable = {
isDragging: true,
@@ -672,3 +621,44 @@ export const useUIStore = defineStore(STORES.UI, {
},
},
});
+
+/**
+ * Helper function for listening to credential changes in the store
+ */
+export const listenForModalChanges = (opts: {
+ store: UiStore;
+ onModalOpened?: (name: keyof Modals) => void;
+ onModalClosed?: (name: keyof Modals) => void;
+}): void => {
+ const { store, onModalClosed, onModalOpened } = opts;
+ const listeningForActions = ['openModal', 'openModalWithData', 'closeModal'];
+
+ store.$onAction((result) => {
+ const { name, after, args } = result;
+ after(async () => {
+ if (!listeningForActions.includes(name)) {
+ return;
+ }
+
+ switch (name) {
+ case 'openModal': {
+ const modalName = args[0];
+ onModalOpened?.(modalName);
+ break;
+ }
+
+ case 'openModalWithData': {
+ const { name: modalName } = args[0] ?? {};
+ onModalOpened?.(modalName);
+ break;
+ }
+
+ case 'closeModal': {
+ const modalName = args[0];
+ onModalClosed?.(modalName);
+ break;
+ }
+ }
+ });
+ });
+};
diff --git a/packages/editor-ui/src/utils/nodes/nodeTransforms.ts b/packages/editor-ui/src/utils/nodes/nodeTransforms.ts
index 80c4a4a2a6..0cdad985ae 100644
--- a/packages/editor-ui/src/utils/nodes/nodeTransforms.ts
+++ b/packages/editor-ui/src/utils/nodes/nodeTransforms.ts
@@ -31,3 +31,12 @@ export function getNodeTypeDisplayableCredentials(
return displayableCredentials;
}
+
+export function doesNodeHaveCredentialsToFill(
+ nodeTypeProvider: NodeTypeProvider,
+ node: Pick,
+): boolean {
+ const requiredCredentials = getNodeTypeDisplayableCredentials(nodeTypeProvider, node);
+
+ return requiredCredentials.length > 0;
+}
diff --git a/packages/editor-ui/src/utils/templates/templateActions.ts b/packages/editor-ui/src/utils/templates/templateActions.ts
index 210ec3f10b..ba85cf3178 100644
--- a/packages/editor-ui/src/utils/templates/templateActions.ts
+++ b/packages/editor-ui/src/utils/templates/templateActions.ts
@@ -12,10 +12,7 @@ import type { useWorkflowsStore } from '@/stores/workflows.store';
import { getFixedNodesList } from '@/utils/nodeViewUtils';
import type { NodeTypeProvider } from '@/utils/nodeTypes/nodeTypeTransforms';
import type { TemplateCredentialKey } from '@/utils/templates/templateTransforms';
-import {
- getNodesRequiringCredentials,
- replaceAllTemplateNodeCredentials,
-} from '@/utils/templates/templateTransforms';
+import { replaceAllTemplateNodeCredentials } from '@/utils/templates/templateTransforms';
import type { INodeCredentialsDetails } from 'n8n-workflow';
import type { RouteLocationRaw, Router } from 'vue-router';
import type { TemplatesStore } from '@/stores/templates.store';
@@ -23,6 +20,7 @@ import type { NodeTypesStore } from '@/stores/nodeTypes.store';
import type { Telemetry } from '@/plugins/telemetry';
import type { useExternalHooks } from '@/composables/useExternalHooks';
import { assert } from '@/utils/assert';
+import { doesNodeHaveCredentialsToFill } from '@/utils/nodes/nodeTransforms';
type ExternalHooks = ReturnType;
@@ -126,9 +124,9 @@ function hasTemplateCredentials(
nodeTypeProvider: NodeTypeProvider,
template: ITemplatesWorkflowFull,
) {
- const nodesRequiringCreds = getNodesRequiringCredentials(nodeTypeProvider, template);
-
- return nodesRequiringCreds.length > 0;
+ return template.workflow.nodes.some((node) =>
+ doesNodeHaveCredentialsToFill(nodeTypeProvider, node),
+ );
}
async function getFullTemplate(templatesStore: TemplatesStore, templateId: string) {
diff --git a/packages/editor-ui/src/utils/templates/templateTransforms.ts b/packages/editor-ui/src/utils/templates/templateTransforms.ts
index e92b7a2347..a1eba68c67 100644
--- a/packages/editor-ui/src/utils/templates/templateTransforms.ts
+++ b/packages/editor-ui/src/utils/templates/templateTransforms.ts
@@ -1,8 +1,4 @@
-import type {
- ITemplatesWorkflowFull,
- 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';
@@ -43,8 +39,12 @@ export const keyFromCredentialTypeAndName = (
* different versions of n8n may have different credential formats.
*/
export const normalizeTemplateNodeCredentials = (
- credentials: IWorkflowTemplateNodeCredentials,
+ credentials?: IWorkflowTemplateNodeCredentials,
): NormalizedTemplateNodeCredentials => {
+ if (!credentials) {
+ return {};
+ }
+
return Object.fromEntries(
Object.entries(credentials).map(([key, value]) => {
return typeof value === 'string' ? [key, value] : [key, value.name];
@@ -133,25 +133,3 @@ export const replaceAllTemplateNodeCredentials = (
};
});
};
-
-/**
- * Returns the nodes in the template that require credentials
- * and the required credentials for each node.
- */
-export const getNodesRequiringCredentials = (
- nodeTypeProvider: NodeTypeProvider,
- template: ITemplatesWorkflowFull,
-): TemplateNodeWithRequiredCredential[] => {
- if (!template) {
- return [];
- }
-
- const nodesWithCredentials: TemplateNodeWithRequiredCredential[] = template.workflow.nodes
- .map((node) => ({
- node,
- requiredCredentials: getNodeTypeDisplayableCredentials(nodeTypeProvider, node),
- }))
- .filter(({ requiredCredentials }) => requiredCredentials.length > 0);
-
- return nodesWithCredentials;
-};
diff --git a/packages/editor-ui/src/views/NodeView.vue b/packages/editor-ui/src/views/NodeView.vue
index f163879ef0..86806ca93c 100644
--- a/packages/editor-ui/src/views/NodeView.vue
+++ b/packages/editor-ui/src/views/NodeView.vue
@@ -96,6 +96,11 @@
@stopExecution="stopExecution"
@saveKeyboardShortcut="onSaveKeyboardShortcut"
/>
+
+
+
+
+
import('@/components/Node/NodeCreation.vue'));
const CanvasControls = defineAsyncComponent(async () => import('@/components/CanvasControls.vue'));
+const SetupWorkflowCredentialsButton = defineAsyncComponent(
+ async () =>
+ import('@/components/SetupWorkflowCredentialsButton/SetupWorkflowCredentialsButton.vue'),
+);
export default defineComponent({
name: 'NodeView',
@@ -393,6 +402,7 @@ export default defineComponent({
NodeCreation,
CanvasControls,
ContextMenu,
+ SetupWorkflowCredentialsButton,
},
mixins: [moveNodeWorkflow, workflowHelpers, workflowRun, debounceHelper],
async beforeRouteLeave(to, from, next) {
@@ -5180,4 +5190,10 @@ export default defineComponent({
transform: translate3d(4px, 0, 0);
}
}
+
+.setupCredentialsButtonWrapper {
+ position: absolute;
+ left: 35px;
+ top: var(--spacing-s);
+}
diff --git a/packages/editor-ui/src/views/SetupWorkflowFromTemplateView/AppsRequiringCredsNotice.vue b/packages/editor-ui/src/views/SetupWorkflowFromTemplateView/AppsRequiringCredsNotice.vue
index 401a122ae2..a640b49f9d 100644
--- a/packages/editor-ui/src/views/SetupWorkflowFromTemplateView/AppsRequiringCredsNotice.vue
+++ b/packages/editor-ui/src/views/SetupWorkflowFromTemplateView/AppsRequiringCredsNotice.vue
@@ -1,20 +1,24 @@