fix(editor): Close add-channel modal on outside click (#37171)

This commit is contained in:
Benjamin Schroth
2026-08-27 10:58:05 +00:00
committed by GitHub
parent 106166abf7
commit c7f3cebbd2
2 changed files with 63 additions and 4 deletions
@@ -35,6 +35,7 @@ const connectedCredentials = ref<Record<string, string>>({});
const selectedCredentials = ref<Record<string, string>>({});
const loadingMap = ref<Record<string, boolean>>({});
const runtimeErrors = ref<Record<string, string>>({});
const credentialModalOpen = ref(false);
vi.mock('@n8n/i18n', () => ({
useI18n: () => ({ baseText: (key: string) => key }),
@@ -168,7 +169,7 @@ vi.mock('../composables/useAgentChannelSetup', () => ({
selectedCredentials,
credentialsLoading: ref(false),
credentialPermissions: ref({ create: true }),
credentialModalOpen: ref(false),
credentialModalOpen,
getChannelCredentialId: (type?: string | null) =>
type ? (selectedCredentials.value[type] ?? connectedCredentials.value[type] ?? '') : '',
getCredentials: () => [
@@ -195,9 +196,19 @@ function mountModal(view: ChannelView = 'example_setup', isPublished = false) {
stubs: {
Dialog: {
props: ['open', 'showCloseButton'],
emits: ['update:open'],
emits: ['update:open', 'interactOutside'],
methods: {
// Mirrors reka-ui's own DismissableLayer: an outside click fires
// `interact-outside` first, and only dismisses if that event
// wasn't prevented.
clickOutside() {
const event = new Event('interact-outside', { cancelable: true });
this.$emit('interactOutside', event);
if (!event.defaultPrevented) this.$emit('update:open', false);
},
},
template:
'<div v-if="open"><button data-testid="close-dialog" @click="$emit(\'update:open\', false)" /><slot /></div>',
'<div v-if="open"><button data-testid="close-dialog" @click="$emit(\'update:open\', false)" /><button data-testid="click-outside" @click="clickOutside" /><slot /></div>',
},
DialogHeader: { template: '<div><slot /></div>' },
DialogTitle: { template: '<h3><slot /></h3>' },
@@ -248,6 +259,7 @@ describe('AgentChannelModal', () => {
selectedCredentials.value = {};
loadingMap.value = {};
runtimeErrors.value = {};
credentialModalOpen.value = false;
mocks.connect.mockImplementation(async (type: string, credentialId: string) => {
statuses.value[type] = 'connected';
connectedCredentials.value[type] = credentialId;
@@ -418,6 +430,48 @@ describe('AgentChannelModal', () => {
});
});
describe('outside click', () => {
it('closes the modal', async () => {
const wrapper = mountModal('list');
await flushPromises();
await wrapper.get('[data-testid="click-outside"]').trigger('click');
expect(wrapper.emitted('update:open')).toEqual([[false]]);
});
it('is ignored while the nested credential modal is open', async () => {
credentialModalOpen.value = true;
const wrapper = mountModal('list');
await flushPromises();
await wrapper.get('[data-testid="click-outside"]').trigger('click');
expect(wrapper.emitted('update:open')).toBeUndefined();
});
it('is ignored while an action is in flight', async () => {
let release: () => void = () => {};
mocks.ensureAgentPersisted.mockImplementation(
async () =>
await new Promise<void>((resolve) => {
release = resolve;
}),
);
selectedCredentials.value.example = 'credential-new';
const wrapper = mountModal('example_setup');
await flushPromises();
await wrapper.get('[data-testid="connect-channel"]').trigger('click');
await wrapper.get('[data-testid="click-outside"]').trigger('click');
expect(wrapper.emitted('update:open')).toBeUndefined();
release();
await flushPromises();
});
});
it('surfaces a failed pre-save step instead of connecting', async () => {
mocks.beforeSave.mockRejectedValue(new Error('settings could not be saved'));
selectedCredentials.value.example = 'credential-new';
@@ -333,6 +333,11 @@ function handleModalOpenUpdate(isOpen: boolean) {
emit('update:open', isOpen);
}
// Only block outside-close while the teleported credential modal is open.
function handleInteractOutside(event: Event) {
if (credentialModalOpen.value) event.preventDefault();
}
async function persistAgent(): Promise<boolean> {
try {
await props.ensureAgentPersisted?.();
@@ -525,7 +530,7 @@ watch(
:trap-focus="!credentialModalOpen"
:disable-outside-pointer-events="!credentialModalOpen"
:show-close-button="false"
@interact-outside="(e) => e.preventDefault()"
@interact-outside="handleInteractOutside"
@update:open="handleModalOpenUpdate"
>
<FocusScope