From 1ca0708ba8965cfc0b981400a9ae0da2d56fdad7 Mon Sep 17 00:00:00 2001 From: Michael Drury Date: Wed, 5 Aug 2026 12:55:42 +0100 Subject: [PATCH] fix(editor): Show agent tool credentials above configuration (#35590) --- .../__tests__/AgentToolConfigModal.test.ts | 41 +++++++++++++++++-- .../components/AgentToolConfigModal.vue | 21 ++++++++++ .../components/NodeCredentials.test.ts | 30 +++++++++++++- .../components/NodeCredentials.vue | 2 + 4 files changed, 89 insertions(+), 5 deletions(-) diff --git a/packages/frontend/editor-ui/src/features/agents/__tests__/AgentToolConfigModal.test.ts b/packages/frontend/editor-ui/src/features/agents/__tests__/AgentToolConfigModal.test.ts index 8aeba9d4532..25a34954d33 100644 --- a/packages/frontend/editor-ui/src/features/agents/__tests__/AgentToolConfigModal.test.ts +++ b/packages/frontend/editor-ui/src/features/agents/__tests__/AgentToolConfigModal.test.ts @@ -5,6 +5,7 @@ import { mockedStore } from '@/__tests__/utils'; import { useUIStore } from '@/app/stores/ui.store'; import { fireEvent, waitFor } from '@testing-library/vue'; import { defineComponent, onMounted, nextTick } from 'vue'; +import { CREDENTIAL_EDIT_MODAL_KEY } from '@/features/credentials/credentials.constants'; import AgentToolConfigModal from '../components/AgentToolConfigModal.vue'; import type { AgentJsonToolRef, CustomToolEntry } from '../types'; @@ -31,9 +32,24 @@ vi.mock('@n8n/design-system', async () => { const actual = await vi.importActual('@n8n/design-system'); const N8nDialog = { name: 'N8nDialog', - props: ['open', 'size', 'showCloseButton'], - emits: ['update:open'], - template: '
', + props: { + open: Boolean, + size: String, + showCloseButton: Boolean, + trapFocus: { type: Boolean, default: true }, + disableOutsidePointerEvents: { type: Boolean, default: true }, + }, + emits: ['update:open', 'interactOutside'], + template: ` +
+ +
+ `, }; return { ...actual, N8nDialog }; }); @@ -141,6 +157,9 @@ function renderModal({ global: { stubs: { NodeIcon: { template: '
' }, + FocusScope: { + template: '
', + }, AgentToolConfigNodeContent: createToolSettingsStub(valid), AgentToolConfigWorkflowContent: createWorkflowToolConfigStub(valid), N8nSwitch2: { @@ -180,6 +199,22 @@ describe('AgentToolConfigModal', () => { expect(getByTestId('node-tool-settings-content')).toBeTruthy(); }); + it('releases dialog focus handling while the credential modal is open', async () => { + const { getByRole, getByTestId, queryByTestId } = renderModal(); + const dialog = getByRole('dialog'); + + expect(dialog).toHaveAttribute('data-trap-focus', 'true'); + expect(dialog).toHaveAttribute('data-disable-outside-pointer-events', 'true'); + expect(queryByTestId('nested-credential-focus-scope')).toBeNull(); + + uiStore.openModal(CREDENTIAL_EDIT_MODAL_KEY); + await nextTick(); + + expect(dialog).toHaveAttribute('data-trap-focus', 'false'); + expect(dialog).toHaveAttribute('data-disable-outside-pointer-events', 'false'); + expect(getByTestId('nested-credential-focus-scope')).toBeInTheDocument(); + }); + it('passes agent project context to the node-tool settings content', () => { const { getByTestId } = renderModal({ projectId: 'project-1', agentId: 'agent-1' }); diff --git a/packages/frontend/editor-ui/src/features/agents/components/AgentToolConfigModal.vue b/packages/frontend/editor-ui/src/features/agents/components/AgentToolConfigModal.vue index d4898ad0cce..8893b68a183 100644 --- a/packages/frontend/editor-ui/src/features/agents/components/AgentToolConfigModal.vue +++ b/packages/frontend/editor-ui/src/features/agents/components/AgentToolConfigModal.vue @@ -13,7 +13,9 @@ import { } from '@n8n/design-system'; import { useI18n } from '@n8n/i18n'; import type { INode } from 'n8n-workflow'; +import { FocusScope } from 'reka-ui'; +import { CREDENTIAL_EDIT_MODAL_KEY } from '@/features/credentials/credentials.constants'; import type { AgentJsonMcpServerConfig, AgentJsonToolRef, @@ -73,6 +75,9 @@ const props = defineProps<{ const i18n = useI18n(); const uiStore = useUIStore(); +const isCredentialModalOpen = computed( + () => uiStore.modalsById[CREDENTIAL_EDIT_MODAL_KEY]?.open === true, +); const isOpen = computed({ get: () => uiStore.modalsById[props.modalName]?.open === true, @@ -203,6 +208,10 @@ function closeDialog() { uiStore.closeModal(props.modalName); } +function handleInteractOutside(event: Event) { + if (isCredentialModalOpen.value) event.preventDefault(); +} + function withApprovalRequirement(ref: AgentJsonToolRef): AgentJsonToolRef { if (!supportsApproval.value) { const { requireApproval: _requireApproval, ...rest } = ref; @@ -311,9 +320,21 @@ function handleNodeUpdate(node: INode) { v-if="canRender" v-model:open="isOpen" size="2xlarge" + :trap-focus="!isCredentialModalOpen" + :disable-outside-pointer-events="!isCredentialModalOpen" :show-close-button="false" data-test-id="agent-tool-config-modal" + @interact-outside="handleInteractOutside" > + + + { ); }); - it('should hide the assistant when opening credentials from a tool context', async () => { + it('should configure the new credential modal for a tool context', async () => { ndvStore.activeNode = httpNode; credentialsStore.state.credentials = { c8vqdPpPClh4TgIO: createCredential(), @@ -394,7 +394,7 @@ describe('NodeCredentials', () => { undefined, httpNode.name, httpNode, - { hideAskAssistant: true, closeOnSave: true }, + { hideAskAssistant: true, closeOnSave: true, appendToBody: true }, ); }); @@ -1282,6 +1282,32 @@ describe('NodeCredentials', () => { expect(screen.queryByTestId('credential-edit-button')).toBeInTheDocument(); }); + + it('should configure the edit credential modal for a tool context', async () => { + ndvStore.activeNode = httpNode; + credentialsStore.state.credentials = { + c8vqdPpPClh4TgIO: createCredential(), + }; + + renderComponent({ + global: { + provide: { + [ChatHubToolContextKey as symbol]: true, + }, + }, + }); + + const editIcon = screen + .getByTestId('credential-edit-button') + .querySelector('[data-icon="pen"]'); + expect(editIcon).not.toBeNull(); + await userEvent.click(editIcon!); + + expect(uiStore.openExistingCredential).toHaveBeenCalledWith('c8vqdPpPClh4TgIO', { + hideAskAssistant: true, + appendToBody: true, + }); + }); }); it('should not show "Set up credential" button when user cannot create credentials', () => { 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 ebe7de242d5..0d8b40516af 100644 --- a/packages/frontend/editor-ui/src/features/credentials/components/NodeCredentials.vue +++ b/packages/frontend/editor-ui/src/features/credentials/components/NodeCredentials.vue @@ -525,6 +525,7 @@ function createNewCredential( { hideAskAssistant: hideAskAssistant.value, closeOnSave: true, + ...(isToolContext ? { appendToBody: true } : {}), instanceAiCredentialHelp: instanceAiCredentialHelp(), }, ); @@ -815,6 +816,7 @@ function editCredential(credentialType: string): void { uiStore.openExistingCredential(credential.id, { hideAskAssistant: hideAskAssistant.value, + ...(isToolContext ? { appendToBody: true } : {}), instanceAiCredentialHelp: instanceAiCredentialHelp(), });