diff --git a/packages/frontend/editor-ui/src/features/credentials/components/NodeCredentials.test.ts b/packages/frontend/editor-ui/src/features/credentials/components/NodeCredentials.test.ts index d659a981ffc..14b4fa09b0e 100644 --- a/packages/frontend/editor-ui/src/features/credentials/components/NodeCredentials.test.ts +++ b/packages/frontend/editor-ui/src/features/credentials/components/NodeCredentials.test.ts @@ -21,6 +21,7 @@ import { useNDVStore } from '@/features/ndv/shared/ndv.store'; import { useUIStore } from '@/app/stores/ui.store'; import { useSettingsStore } from '@/app/stores/settings.store'; import { useNodeTypesStore } from '@/app/stores/nodeTypes.store'; +import { useWorkflowsStore } from '@/app/stores/workflows.store'; import { useAiGateway } from '@/app/composables/useAiGateway'; import { WorkflowDocumentStoreKey } from '@/app/constants/injectionKeys'; import { @@ -153,6 +154,7 @@ describe('NodeCredentials', () => { let uiStore: ReturnType>; let projectsStore: ReturnType>; let settingsStore: ReturnType>; + let workflowsStore: ReturnType>; let workflowDocumentStore: ReturnType; let workflowDocumentStoreRef: ReturnType< typeof shallowRef | null> @@ -190,11 +192,15 @@ describe('NodeCredentials', () => { credentialsStore = mockedStore(useCredentialsStore); // Component triggers this on mount; avoid a real XHR with stubActions: false. - credentialsStore.fetchAllCredentials = vi.fn().mockResolvedValue([]); + credentialsStore.fetchAllCredentialsForWorkflow = vi.fn().mockResolvedValue([]); + ndvStore = mockedStore(useNDVStore, createWorkflowDocumentId('1')); uiStore = mockedStore(useUIStore); projectsStore = mockedStore(useProjectsStore); settingsStore = mockedStore(useSettingsStore); + workflowsStore = mockedStore(useWorkflowsStore); + + workflowsStore.isNewWorkflow = false; projectsStore.currentProject = { id: 'default', scopes: ['credential:create'] } as Project; settingsStore.settings = { @@ -231,7 +237,36 @@ describe('NodeCredentials', () => { renderComponent(); - expect(credentialsStore.fetchAllCredentials).toHaveBeenCalled(); + expect(credentialsStore.fetchAllCredentialsForWorkflow).toHaveBeenCalledWith({ + workflowId: '1', + }); + }); + + it('should fetch credentials scoped to the project for an unsaved workflow', () => { + workflowsStore.isNewWorkflow = true; + projectsStore.currentProject = { id: 'project-1' } as Project; + ndvStore.activeNode = httpNode; + credentialsStore.state.credentials = {}; + + renderComponent(); + + expect(credentialsStore.fetchAllCredentialsForWorkflow).toHaveBeenCalledWith({ + projectId: 'project-1', + }); + }); + + it('should fall back to the personal project for an unsaved workflow without a current project', () => { + workflowsStore.isNewWorkflow = true; + projectsStore.currentProject = null; + projectsStore.personalProject = { id: 'personal-project' } as Project; + ndvStore.activeNode = httpNode; + credentialsStore.state.credentials = {}; + + renderComponent(); + + expect(credentialsStore.fetchAllCredentialsForWorkflow).toHaveBeenCalledWith({ + projectId: 'personal-project', + }); }); it('should ignore managed credentials in the dropdown if active node is the HTTP node', async () => { diff --git a/packages/frontend/editor-ui/src/features/credentials/components/NodeCredentials.vue b/packages/frontend/editor-ui/src/features/credentials/components/NodeCredentials.vue index 25b01d873d8..5761534e9b6 100644 --- a/packages/frontend/editor-ui/src/features/credentials/components/NodeCredentials.vue +++ b/packages/frontend/editor-ui/src/features/credentials/components/NodeCredentials.vue @@ -32,6 +32,7 @@ import { injectNDVStore } from '@/features/ndv/shared/ndv.store'; import { useNodeTypesStore } from '@/app/stores/nodeTypes.store'; import { useUIStore } from '@/app/stores/ui.store'; import { useProjectsStore } from '@/features/collaboration/projects/projects.store'; +import { useWorkflowsStore } from '@/app/stores/workflows.store'; import { assert } from '@n8n/utils/assert'; import { isEmpty } from '@/app/utils/typesUtils'; import { getResourcePermissions } from '@n8n/permissions'; @@ -97,6 +98,7 @@ const nodeTypesStore = useNodeTypesStore(); const ndvStore = injectNDVStore(); const uiStore = useUIStore(); const projectsStore = useProjectsStore(); +const workflowsStore = useWorkflowsStore(); const workflowDocumentStore = props.standalone ? undefined : injectWorkflowDocumentStore(); const { isEnabled: isDynamicCredentialsEnabled } = useDynamicCredentials(); @@ -256,6 +258,18 @@ watch( { immediate: true }, ); +function getCredentialFetchScope(): { workflowId: string } | { projectId: string } | undefined { + const workflowId = workflowDocumentStore?.value.workflowId; + if (workflowId && !workflowsStore.isNewWorkflow) { + return { workflowId }; + } + + const projectId = + props.projectId ?? projectsStore.currentProject?.id ?? projectsStore.personalProject?.id; + + return projectId ? { projectId } : undefined; +} + onMounted(() => { credentialsStore.$onAction(({ name, after, args }) => { const listeningForActions = ['createNewCredential', 'updateCredential', 'deleteCredential']; @@ -325,9 +339,10 @@ onMounted(() => { ndvEventBus.on('credential.createNew', onCreateAndAssignNewCredential); - void credentialsStore.fetchAllCredentials({ - projectId: projectsStore.currentProject?.id, - }); + const scope = getCredentialFetchScope(); + if (scope) { + void credentialsStore.fetchAllCredentialsForWorkflow(scope); + } void aiGateway.fetchConfig();