From 11dfca24d9ccc8f48a5dd3aa9ffa42e8b56a1a70 Mon Sep 17 00:00:00 2001 From: Bernhard Wittmann Date: Thu, 4 Jun 2026 10:36:42 +0200 Subject: [PATCH] feat(editor): Add shared tools-connection modal (#31381) Co-authored-by: Elias Meire --- .../RecycleScroller.test.ts | 34 ++ .../N8nRecycleScroller/RecycleScroller.vue | 16 + .../frontend/@n8n/i18n/src/locales/en.json | 41 +- packages/frontend/editor-ui/eslint.config.mjs | 5 + .../toolsConnection/DefaultDetailBody.vue | 49 +++ .../shared/toolsConnection/McpDetailBody.vue | 232 ++++++++++ .../McpToolSettingsContent.vue | 194 +++++++++ .../toolsConnection/ToolCredentialPicker.vue | 267 ++++++++++++ .../shared/toolsConnection/ToolDetailView.vue | 146 +++++++ .../shared/toolsConnection/ToolRow.vue | 238 +++++++++++ .../toolsConnection/ToolSettingsView.vue | 222 ++++++++++ .../ToolsConnectionModal.stories.ts | 360 ++++++++++++++++ .../toolsConnection/ToolsConnectionModal.vue | 399 ++++++++++++++++++ .../__tests__/DefaultDetailBody.test.ts | 65 +++ .../__tests__/ToolCredentialPicker.test.ts | 99 +++++ .../toolsConnection/__tests__/ToolRow.test.ts | 153 +++++++ .../__tests__/ToolsConnectionModal.test.ts | 298 +++++++++++++ .../shared/toolsConnection/fixtures.ts | 363 ++++++++++++++++ .../shared/toolsConnection/toolItemIcon.ts | 5 + .../features/shared/toolsConnection/types.ts | 120 ++++++ 20 files changed, 3305 insertions(+), 1 deletion(-) create mode 100644 packages/frontend/editor-ui/src/features/shared/toolsConnection/DefaultDetailBody.vue create mode 100644 packages/frontend/editor-ui/src/features/shared/toolsConnection/McpDetailBody.vue create mode 100644 packages/frontend/editor-ui/src/features/shared/toolsConnection/McpToolSettingsContent.vue create mode 100644 packages/frontend/editor-ui/src/features/shared/toolsConnection/ToolCredentialPicker.vue create mode 100644 packages/frontend/editor-ui/src/features/shared/toolsConnection/ToolDetailView.vue create mode 100644 packages/frontend/editor-ui/src/features/shared/toolsConnection/ToolRow.vue create mode 100644 packages/frontend/editor-ui/src/features/shared/toolsConnection/ToolSettingsView.vue create mode 100644 packages/frontend/editor-ui/src/features/shared/toolsConnection/ToolsConnectionModal.stories.ts create mode 100644 packages/frontend/editor-ui/src/features/shared/toolsConnection/ToolsConnectionModal.vue create mode 100644 packages/frontend/editor-ui/src/features/shared/toolsConnection/__tests__/DefaultDetailBody.test.ts create mode 100644 packages/frontend/editor-ui/src/features/shared/toolsConnection/__tests__/ToolCredentialPicker.test.ts create mode 100644 packages/frontend/editor-ui/src/features/shared/toolsConnection/__tests__/ToolRow.test.ts create mode 100644 packages/frontend/editor-ui/src/features/shared/toolsConnection/__tests__/ToolsConnectionModal.test.ts create mode 100644 packages/frontend/editor-ui/src/features/shared/toolsConnection/fixtures.ts create mode 100644 packages/frontend/editor-ui/src/features/shared/toolsConnection/toolItemIcon.ts create mode 100644 packages/frontend/editor-ui/src/features/shared/toolsConnection/types.ts diff --git a/packages/frontend/@n8n/design-system/src/components/N8nRecycleScroller/RecycleScroller.test.ts b/packages/frontend/@n8n/design-system/src/components/N8nRecycleScroller/RecycleScroller.test.ts index 8324ef9aa26..da28ca8d518 100644 --- a/packages/frontend/@n8n/design-system/src/components/N8nRecycleScroller/RecycleScroller.test.ts +++ b/packages/frontend/@n8n/design-system/src/components/N8nRecycleScroller/RecycleScroller.test.ts @@ -1,4 +1,6 @@ import { render } from '@testing-library/vue'; +import { mount } from '@vue/test-utils'; +import { nextTick } from 'vue'; import N8nRecycleScroller from './RecycleScroller.vue'; @@ -25,5 +27,37 @@ describe('components', () => { ); expect(wrapper.html()).toMatchSnapshot(); }); + + it('scrolls to an item by key using cached item positions', async () => { + const originalOffsetHeight = Object.getOwnPropertyDescriptor( + HTMLElement.prototype, + 'offsetHeight', + ); + Object.defineProperty(HTMLElement.prototype, 'offsetHeight', { + configurable: true, + value: itemSize, + }); + + try { + const wrapper = mount(N8nRecycleScroller, { + props: { + itemSize, + itemKey, + items, + }, + }); + + await nextTick(); + wrapper.vm.scrollToKey('10'); + + expect(wrapper.find('.recycle-scroller-wrapper').element.scrollTop).toBe(1000); + } finally { + if (originalOffsetHeight) { + Object.defineProperty(HTMLElement.prototype, 'offsetHeight', originalOffsetHeight); + } else { + Reflect.deleteProperty(HTMLElement.prototype, 'offsetHeight'); + } + } + }); }); }); diff --git a/packages/frontend/@n8n/design-system/src/components/N8nRecycleScroller/RecycleScroller.vue b/packages/frontend/@n8n/design-system/src/components/N8nRecycleScroller/RecycleScroller.vue index ece198bdf0c..cc3eee31531 100644 --- a/packages/frontend/@n8n/design-system/src/components/N8nRecycleScroller/RecycleScroller.vue +++ b/packages/frontend/@n8n/design-system/src/components/N8nRecycleScroller/RecycleScroller.vue @@ -182,6 +182,22 @@ function onScroll() { scrollTop.value = wrapperRef.value.scrollTop; } + +function scrollToKey(key: Item[Key]) { + if (!wrapperRef.value) { + return; + } + + const position = itemPositionCache.value[key]; + if (position === undefined) { + return; + } + + wrapperRef.value.scrollTop = position; + scrollTop.value = position; +} + +defineExpose({ scrollToKey });