fix(editor): Show auth type selector in Instance AI workflow setup (#28707)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Svetoslav Dekov 2026-04-20 19:50:21 +03:00 committed by GitHub
parent db83a95522
commit 1b13d325f1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 131 additions and 6 deletions

View file

@ -642,6 +642,7 @@ export interface NewCredentialsModal extends ModalState {
forceManualMode?: boolean;
projectId?: string;
suggestedName?: string;
nodeName?: string;
}
export type IRunDataDisplayMode = 'table' | 'json' | 'binary' | 'schema' | 'html' | 'ai';

View file

@ -550,6 +550,7 @@ export const useUIStore = defineStore(STORES.UI, () => {
forceManualMode = false,
projectId?: string,
suggestedName?: string,
nodeName?: string,
) => {
setActiveId(CREDENTIAL_EDIT_MODAL_KEY, type);
setShowAuthSelector(CREDENTIAL_EDIT_MODAL_KEY, showAuthOptions);
@ -558,6 +559,7 @@ export const useUIStore = defineStore(STORES.UI, () => {
forceManualMode,
projectId,
suggestedName,
nodeName,
} as NewCredentialsModal;
setMode(CREDENTIAL_EDIT_MODAL_KEY, 'new');
openModal(CREDENTIAL_EDIT_MODAL_KEY);

View file

@ -3,6 +3,9 @@ import { createTestingPinia } from '@pinia/testing';
import { setActivePinia } from 'pinia';
import { computed } from 'vue';
import type { InstanceAiWorkflowSetupNode } from '@n8n/api-types';
import type { INodeTypeDescription } from 'n8n-workflow';
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
import { useUIStore } from '@/app/stores/ui.store';
import { useWorkflowsStore } from '@/app/stores/workflows.store';
import { useCredentialsStore } from '@/features/credentials/credentials.store';
import { useCredentialGroupSelection } from '../composables/useCredentialGroupSelection';
@ -46,12 +49,16 @@ function makeCard(overrides: Partial<SetupCard> = {}): SetupCard {
describe('useCredentialGroupSelection', () => {
let workflowsStore: ReturnType<typeof useWorkflowsStore>;
let credentialsStore: ReturnType<typeof useCredentialsStore>;
let nodeTypesStore: ReturnType<typeof useNodeTypesStore>;
let uiStore: ReturnType<typeof useUIStore>;
beforeEach(() => {
const pinia = createTestingPinia({ stubActions: false });
setActivePinia(pinia);
workflowsStore = useWorkflowsStore();
credentialsStore = useCredentialsStore();
nodeTypesStore = useNodeTypesStore();
uiStore = useUIStore();
});
describe('initCredGroupSelections', () => {
@ -228,6 +235,67 @@ describe('useCredentialGroupSelection', () => {
});
});
describe('openNewCredentialForSection', () => {
function mockNodeTypeWithAuth(authOptions: Array<{ name: string; value: string }>) {
const nodeType = {
name: 'n8n-nodes-base.slack',
properties: [
{
name: 'authentication',
displayName: 'Authentication',
type: 'options',
default: authOptions[0]?.value ?? '',
options: authOptions,
},
],
credentials: [
{
name: 'slackApi',
displayOptions: {
show: { authentication: [authOptions[0]?.value ?? ''] },
},
},
],
} as unknown as INodeTypeDescription;
// @ts-expect-error Known pinia issue when spying on store getters
vi.spyOn(nodeTypesStore, 'getNodeType', 'get').mockReturnValue(() => nodeType);
}
test('passes showAuthOptions=true and the node name when node has multiple auth options', () => {
mockNodeTypeWithAuth([
{ name: 'API Token', value: 'apiToken' },
{ name: 'OAuth2', value: 'oAuth2' },
]);
const card = makeCard({
nodes: [makeSetupNode({ credentialType: 'slackApi' })],
});
const cards = computed(() => [card]);
const { openNewCredentialForSection } = useCredentialGroupSelection(cards, vi.fn());
const openSpy = vi.spyOn(uiStore, 'openNewCredential');
openNewCredentialForSection('slackApi', 'slackApi');
expect(openSpy).toHaveBeenCalledWith('slackApi', true, false, undefined, undefined, 'Slack');
});
test('passes showAuthOptions=false when node has no main auth field', () => {
// @ts-expect-error Known pinia issue when spying on store getters
vi.spyOn(nodeTypesStore, 'getNodeType', 'get').mockReturnValue(() => null);
const card = makeCard({
nodes: [makeSetupNode({ credentialType: 'slackApi' })],
});
const cards = computed(() => [card]);
const { openNewCredentialForSection } = useCredentialGroupSelection(cards, vi.fn());
const openSpy = vi.spyOn(uiStore, 'openNewCredential');
openNewCredentialForSection('slackApi', 'slackApi');
expect(openSpy).toHaveBeenCalledWith('slackApi', false, false, undefined, undefined, 'Slack');
});
});
describe('cardHasExistingCredentials', () => {
test('returns true when node has existing credentials', () => {
const card = makeCard({

View file

@ -1,7 +1,9 @@
import type { ComputedRef } from 'vue';
import { ref } from 'vue';
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
import { useUIStore } from '@/app/stores/ui.store';
import { useWorkflowsStore } from '@/app/stores/workflows.store';
import { getMainAuthField } from '@/app/utils/nodeTypesUtils';
import { useCredentialsStore } from '@/features/credentials/credentials.store';
import { credGroupKey, type SetupCard } from '../instanceAiWorkflowSetup.utils';
@ -13,6 +15,7 @@ export function useCredentialGroupSelection(
const uiStore = useUIStore();
const workflowsStore = useWorkflowsStore();
const credentialsStore = useCredentialsStore();
const nodeTypesStore = useNodeTypesStore();
// Shared credential selection keyed by credGroupKey — single source of truth
// for all cards in the same credential group (including escalated per-node cards).
@ -135,9 +138,38 @@ export function useCredentialGroupSelection(
);
}
function findCardForGroup(credentialType: string, groupKey: string): SetupCard | undefined {
return cards.value.find(
(c) =>
c.credentialType === credentialType && c.nodes[0] && credGroupKey(c.nodes[0]) === groupKey,
);
}
function shouldShowAuthOptions(card: SetupCard | undefined): boolean {
const setupNode = card?.nodes[0]?.node;
if (!setupNode) return false;
const nodeType = nodeTypesStore.getNodeType(setupNode.type, setupNode.typeVersion);
const mainAuthField = getMainAuthField(nodeType);
return (
mainAuthField !== null &&
Array.isArray(mainAuthField.options) &&
mainAuthField.options.length > 0
);
}
function openNewCredentialForSection(credentialType: string, groupKey: string) {
activeCredentialTarget.value = { groupKey, credentialType };
uiStore.openNewCredential(credentialType, false, false, projectId);
const card = findCardForGroup(credentialType, groupKey);
const showAuthOptions = shouldShowAuthOptions(card);
const nodeName = card?.nodes[0]?.node.name;
uiStore.openNewCredential(
credentialType,
showAuthOptions,
false,
projectId,
undefined,
nodeName,
);
}
return {

View file

@ -5,6 +5,7 @@ import { getAppNameFromCredType } from '@/app/utils/nodeTypesUtils';
import type {
ICredentialDataDecryptedObject,
ICredentialType,
INode,
INodeProperties,
} from 'n8n-workflow';
import { isCommunityPackageName } from 'n8n-workflow';
@ -70,6 +71,7 @@ type Props = {
managedOauthAvailable?: boolean;
useCustomOauth?: boolean;
isQuickConnectMode?: boolean;
contextNode?: INode | null;
};
const props = withDefaults(defineProps<Props>(), {
@ -286,6 +288,7 @@ watch(showOAuthSuccessBanner, (newValue, oldValue) => {
:show-managed-oauth-options="managedOauthAvailable"
:quick-connect-available="quickConnectAvailable"
:is-quick-connect-mode="isQuickConnectMode"
:context-node="contextNode"
@update:auth-type="onAuthTypeChange"
/>

View file

@ -31,6 +31,10 @@ import { useNDVStore } from '@/features/ndv/shared/ndv.store';
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
import { useSettingsStore } from '@/app/stores/settings.store';
import { useUIStore } from '@/app/stores/ui.store';
import {
createWorkflowDocumentId,
useWorkflowDocumentStore,
} from '@/app/stores/workflowDocument.store';
import { useWorkflowsStore } from '@/app/stores/workflows.store';
import type { Project, ProjectSharingData } from '@/features/collaboration/projects/projects.types';
import { getResourcePermissions } from '@n8n/permissions';
@ -125,8 +129,21 @@ const useCustomOAuth = ref(false);
const pendingAuthType = ref<string | null>(null);
const credentialDataCache = ref<Record<string, ICredentialDataDecryptedObject>>({});
const workflowDocumentStore = computed(() =>
workflowsStore.workflowId
? useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId))
: undefined,
);
const contextNode = computed<INode | null>(() => {
if (ndvStore.activeNode) return ndvStore.activeNode;
const modalState = uiStore.modalsById[CREDENTIAL_EDIT_MODAL_KEY];
const fallbackName = isCredentialModalState(modalState) ? modalState.nodeName : undefined;
return fallbackName ? (workflowDocumentStore.value?.getNodeByName(fallbackName) ?? null) : null;
});
const activeNodeType = computed(() => {
const activeNode = ndvStore.activeNode;
const activeNode = contextNode.value;
if (activeNode) {
return nodeTypesStore.getNodeType(activeNode.type, activeNode.typeVersion);
@ -871,8 +888,8 @@ async function saveCredential(): Promise<ICredentialsResponse | null> {
}
const appliedAuthType = pendingAuthType.value;
if (appliedAuthType && ndvStore.activeNode) {
updateNodeAuthType(workflowsStore.workflowId, ndvStore.activeNode, appliedAuthType);
if (appliedAuthType && contextNode.value) {
updateNodeAuthType(workflowsStore.workflowId, contextNode.value, appliedAuthType);
pendingAuthType.value = null;
}
@ -1477,6 +1494,7 @@ const { width } = useElementSize(credNameRef);
:managed-oauth-available="managedOAuthAvailable"
:use-custom-oauth="useCustomOAuth"
:is-quick-connect-mode="isQuickConnectMode"
:context-node="contextNode"
@update="onDataChange"
@oauth="oAuthCredentialAuthorize"
@quick-connect="onQuickConnect"

View file

@ -2,7 +2,7 @@
import { useI18n } from '@n8n/i18n';
import { useNDVStore } from '@/features/ndv/shared/ndv.store';
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
import type { ICredentialType, INodeTypeDescription } from 'n8n-workflow';
import type { ICredentialType, INode, INodeTypeDescription } from 'n8n-workflow';
import { computed } from 'vue';
import { N8nButton, N8nIcon, N8nText } from '@n8n/design-system';
import {
@ -29,6 +29,7 @@ const props = defineProps<{
showManagedOauthOptions?: boolean;
quickConnectAvailable?: boolean;
isQuickConnectMode?: boolean;
contextNode?: INode | null;
}>();
const emit = defineEmits<{
@ -40,7 +41,7 @@ const ndvStore = useNDVStore();
const i18n = useI18n();
const { isOAuthCredentialType } = useCredentialOAuth();
const activeNode = computed(() => ndvStore.activeNode);
const activeNode = computed<INode | null>(() => props.contextNode ?? ndvStore.activeNode);
const activeNodeType = computed<INodeTypeDescription | null>(() => {
if (!activeNode.value) return null;
return nodeTypesStore.getNodeType(activeNode.value.type, activeNode.value.typeVersion);