mirror of
https://github.com/n8n-io/n8n
synced 2026-04-21 15:47:20 +00:00
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:
parent
db83a95522
commit
1b13d325f1
7 changed files with 131 additions and 6 deletions
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
/>
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in a new issue