fix(editor): Show agent tool credentials above configuration (#35590)

This commit is contained in:
Michael Drury 2026-08-05 12:55:42 +01:00 committed by GitHub
parent bbf5da6350
commit 1ca0708ba8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 89 additions and 5 deletions

View File

@ -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<typeof import('@n8n/design-system')>('@n8n/design-system');
const N8nDialog = {
name: 'N8nDialog',
props: ['open', 'size', 'showCloseButton'],
emits: ['update:open'],
template: '<div v-if="open" role="dialog"><slot /></div>',
props: {
open: Boolean,
size: String,
showCloseButton: Boolean,
trapFocus: { type: Boolean, default: true },
disableOutsidePointerEvents: { type: Boolean, default: true },
},
emits: ['update:open', 'interactOutside'],
template: `
<div
v-if="open"
role="dialog"
:data-trap-focus="trapFocus"
:data-disable-outside-pointer-events="disableOutsidePointerEvents"
>
<slot />
</div>
`,
};
return { ...actual, N8nDialog };
});
@ -141,6 +157,9 @@ function renderModal({
global: {
stubs: {
NodeIcon: { template: '<div data-test-id="header-node-icon" />' },
FocusScope: {
template: '<div data-test-id="nested-credential-focus-scope"><slot /></div>',
},
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' });

View File

@ -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"
>
<FocusScope
v-if="isCredentialModalOpen"
as-child
@mount-auto-focus.prevent
@unmount-auto-focus.prevent
>
<span hidden aria-hidden="true" />
</FocusScope>
<N8nDialogHeader>
<AgentToolConfigModalHeader
:kind="headerKind"

View File

@ -367,7 +367,7 @@ describe('NodeCredentials', () => {
);
});
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', () => {

View File

@ -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(),
});