refactor(editor): Migrate mapper popover to ruka UI (#19564)

This commit is contained in:
Suguru Inoue
2025-09-17 10:42:40 +02:00
committed by GitHub
parent ae1af1101b
commit 0173d8f707
13 changed files with 212 additions and 566 deletions

View File

@@ -244,6 +244,9 @@ describe('AI Assistant::enabled', () => {
ndv.getters.nodeExecuteButton().click();
// Wait for a message from AI to be shown
aiAssistant.getters.chatMessagesAssistant().should('have.length', 3);
getEditor()
.type('{selectall}')
.paste(

View File

@@ -113,4 +113,32 @@ describe('N8nPopoverReka', () => {
expect(wrapper.props('maxHeight')).toBeUndefined();
});
describe('auto-focus behavior', () => {
it('should focus an element in the content slot by default', async () => {
const wrapper = render(N8nPopoverReka, {
props: { open: true },
slots: {
trigger: '<button />',
content: '<input />',
},
});
const popover = await wrapper.findByRole('dialog');
expect(popover.contains(document.activeElement)).toBe(true);
});
it('should suppress auto-focus when suppressAutoFocus is true', async () => {
const wrapper = render(N8nPopoverReka, {
props: { open: true, suppressAutoFocus: true },
slots: {
trigger: '<button />',
content: '<input />',
},
});
const popover = await wrapper.findByRole('dialog');
expect(popover.contains(document.activeElement)).toBe(false);
});
});
});

View File

@@ -1,14 +1,31 @@
<script setup lang="ts">
import { PopoverContent, PopoverPortal, PopoverRoot, PopoverTrigger } from 'reka-ui';
import {
PopoverContent,
type PopoverContentProps,
PopoverPortal,
PopoverRoot,
type PopoverRootProps,
PopoverTrigger,
} from 'reka-ui';
import type { CSSProperties } from 'vue';
import N8nScrollArea from '../N8nScrollArea/N8nScrollArea.vue';
interface Props {
open?: boolean;
interface Props
extends Pick<PopoverContentProps, 'side' | 'align' | 'sideFlip' | 'sideOffset' | 'reference'>,
Pick<PopoverRootProps, 'open'> {
/**
* Whether to enable scrolling in the popover content
*/
enableScrolling?: boolean;
/**
* Whether to enable slide-in animation
*/
enableSlideIn?: boolean;
/**
* Whether to suppress auto-focus behavior when the content includes focusable element
*/
suppressAutoFocus?: boolean;
/**
* Scrollbar visibility behavior
*/
@@ -17,14 +34,18 @@ interface Props {
* Popover width
*/
width?: string;
/**
* z-index of popover content
*/
zIndex?: number | CSSProperties['zIndex'];
/**
* Popover max height
*/
maxHeight?: string;
/**
* The preferred alignment against the trigger. May change when collisions occur.
* Additional class name set to PopperContent
*/
align?: 'start' | 'center' | 'end';
contentClass?: string;
}
interface Emits {
@@ -32,15 +53,24 @@ interface Emits {
}
const props = withDefaults(defineProps<Props>(), {
open: undefined,
maxHeight: undefined,
width: undefined,
enableScrolling: true,
enableSlideIn: true,
scrollType: 'hover',
align: undefined,
sideOffset: 5,
sideFlip: undefined,
suppressAutoFocus: false,
zIndex: 999,
});
const emit = defineEmits<Emits>();
function handleOpenAutoFocus(e: Event) {
if (props.suppressAutoFocus) {
e.preventDefault();
}
}
</script>
<template>
@@ -49,21 +79,29 @@ const emit = defineEmits<Emits>();
<slot name="trigger"></slot>
</PopoverTrigger>
<PopoverPortal>
<PopoverContent side="bottom" :align="align" :side-offset="5" :class="$style.popoverContent">
<PopoverContent
role="dialog"
:side="side"
:side-flip="sideFlip"
:align="align"
:side-offset="sideOffset"
:class="[$style.popoverContent, contentClass, { [$style.enableSlideIn]: enableSlideIn }]"
:style="{ width, zIndex }"
:reference="reference"
@open-auto-focus="handleOpenAutoFocus"
>
<N8nScrollArea
v-if="enableScrolling"
:max-height="props.maxHeight"
:max-height="maxHeight"
:type="scrollType"
:enable-vertical-scroll="true"
:enable-horizontal-scroll="false"
>
<div :style="{ width }">
<slot name="content" :close="() => emit('update:open', false)" />
</div>
</N8nScrollArea>
<div v-else :style="{ width }">
<slot name="content" :close="() => emit('update:open', false)" />
</div>
</N8nScrollArea>
<template v-else>
<slot name="content" :close="() => emit('update:open', false)" />
</template>
</PopoverContent>
</PopoverPortal>
</PopoverRoot>
@@ -77,10 +115,12 @@ const emit = defineEmits<Emits>();
box-shadow:
rgba(0, 0, 0, 0.1) 0 10px 15px -3px,
rgba(0, 0, 0, 0.05) 0 4px 6px -2px;
animation-duration: 400ms;
animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
will-change: transform, opacity;
z-index: 999;
&.enableSlideIn {
animation-duration: 400ms;
animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
}
}
.popoverContent[data-state='open'][data-side='top'] {

View File

@@ -4,13 +4,11 @@ exports[`N8nPopoverReka > should render correctly with default props 1`] = `
"<mock-popover-root>
<mock-popover-trigger><button></button></mock-popover-trigger>
<mock-popover-portal>
<mock-popover-content>
<mock-popover-content role="dialog">
<div dir="ltr" style="position: relative; --reka-scroll-area-corner-width: 0px; --reka-scroll-area-corner-height: 0px;" class="scrollAreaRoot">
<div data-reka-scroll-area-viewport="" style="overflow-x: hidden; overflow-y: hidden;" class="viewport" tabindex="0">
<div>
<div>
<content></content>
</div>
<content></content>
</div>
</div>
<style>

View File

@@ -11,6 +11,7 @@ import type {
INodeTypeDescription,
INodeIssues,
ITaskData,
INodeProperties,
} from 'n8n-workflow';
import { FORM_TRIGGER_NODE_TYPE, NodeConnectionTypes, NodeHelpers, Workflow } from 'n8n-workflow';
import { v4 as uuid } from 'uuid';
@@ -217,6 +218,16 @@ export function createTestNode(node: Partial<INode> = {}): INode {
};
}
export function createTestNodeProperties(data: Partial<INodeProperties> = {}): INodeProperties {
return {
displayName: 'Name',
name: 'name',
type: 'string',
default: '',
...data,
};
}
export function createMockEnterpriseSettings(
overrides: Partial<FrontendSettings['enterprise']> = {},
): FrontendSettings['enterprise'] {

View File

@@ -236,7 +236,6 @@ defineExpose({ focus, select });
:segments="segments"
:is-read-only="isReadOnly"
:virtual-ref="container"
:append-to="isInExperimentalNdv ? 'body' : undefined"
/>
</div>
</template>

View File

@@ -4,7 +4,7 @@ import InlineExpressionEditorOutput from './InlineExpressionEditorOutput.vue';
describe('InlineExpressionEditorOutput.vue', () => {
test('should render duplicate segments correctly', async () => {
const { getByTestId } = renderComponent(InlineExpressionEditorOutput, {
const rendered = renderComponent(InlineExpressionEditorOutput, {
pinia: createTestingPinia(),
props: {
visible: true,
@@ -48,11 +48,13 @@ describe('InlineExpressionEditorOutput.vue', () => {
],
},
});
expect(getByTestId('inline-expression-editor-output')).toHaveTextContent('[1,2]');
const body = await rendered.findByTestId('inline-expression-editor-output');
expect(body).toHaveTextContent('[1,2]');
});
test('should render segments with resolved expressions', () => {
const { getByTestId } = renderComponent(InlineExpressionEditorOutput, {
test('should render segments with resolved expressions', async () => {
const rendered = renderComponent(InlineExpressionEditorOutput, {
pinia: createTestingPinia(),
props: {
visible: true,
@@ -93,8 +95,8 @@ describe('InlineExpressionEditorOutput.vue', () => {
],
},
});
expect(getByTestId('inline-expression-editor-output')).toHaveTextContent(
'before> [Object: "2024-04-18T09:03:26.651-04:00"] <after',
);
const body = await rendered.findByTestId('inline-expression-editor-output');
expect(body).toHaveTextContent('before> [Object: "2024-04-18T09:03:26.651-04:00"] <after');
});
});

View File

@@ -4,13 +4,13 @@ import type { EditorState, SelectionRange } from '@codemirror/state';
import { useI18n } from '@n8n/i18n';
import { useNDVStore } from '@/stores/ndv.store';
import type { Segment } from '@/types/expressions';
import { computed, onBeforeUnmount, useTemplateRef } from 'vue';
import { onBeforeUnmount, useTemplateRef } from 'vue';
import ExpressionOutput from './ExpressionOutput.vue';
import OutputItemSelect from './OutputItemSelect.vue';
import InlineExpressionTip from './InlineExpressionTip.vue';
import { outputTheme } from './theme';
import { useElementSize } from '@vueuse/core';
import { N8nPopover } from '@n8n/design-system';
import { N8nPopoverReka, N8nText } from '@n8n/design-system';
import { useStyles } from '@/composables/useStyles';
interface InlineExpressionEditorOutputProps {
segments: Segment[];
@@ -20,10 +20,9 @@ interface InlineExpressionEditorOutputProps {
isReadOnly?: boolean;
visible: boolean;
virtualRef?: HTMLElement;
appendTo?: string;
}
const props = withDefaults(defineProps<InlineExpressionEditorOutputProps>(), {
withDefaults(defineProps<InlineExpressionEditorOutputProps>(), {
editorState: undefined,
selection: undefined,
isReadOnly: false,
@@ -34,7 +33,7 @@ const i18n = useI18n();
const theme = outputTheme();
const ndvStore = useNDVStore();
const contentRef = useTemplateRef('content');
const virtualRefSize = useElementSize(computed(() => props.virtualRef));
const { APP_Z_INDEXES } = useStyles();
onBeforeUnmount(() => {
ndvStore.expressionOutputItemIndex = 0;
@@ -46,75 +45,60 @@ defineExpose({
</script>
<template>
<N8nPopover
:visible="visible"
placement="bottom"
:show-arrow="false"
:offset="0"
:persistent="false"
:virtual-triggering="virtualRef !== undefined"
:virtual-ref="virtualRef"
:width="virtualRefSize.width.value"
:popper-class="`${$style.popper} ignore-key-press-canvas`"
:popper-options="{
modifiers: [
{ name: 'flip', enabled: false },
{
// Ensures that the popover is re-positioned when the reference element is resized
name: 'custom modifier',
options: {
width: virtualRefSize.width.value,
height: virtualRefSize.height.value,
},
},
],
}"
:append-to="appendTo"
<N8nPopoverReka
:open="visible"
side="bottom"
:side-flip="false"
:side-offset="0"
align="start"
:reference="virtualRef"
width="var(--reka-popper-anchor-width)"
:content-class="$style.popover"
:enable-slide-in="false"
:enable-scrolling="false"
:suppress-auto-focus="true"
:z-index="APP_Z_INDEXES.NDV + 1"
>
<div ref="content" :class="$style.dropdown">
<div :class="$style.header">
<n8n-text bold size="small" compact>
{{ i18n.baseText('parameterInput.result') }}
</n8n-text>
<template #content>
<div ref="content" :class="[$style.dropdown, 'ignore-key-press-canvas']">
<div :class="$style.header">
<N8nText bold size="small" compact>
{{ i18n.baseText('parameterInput.result') }}
</N8nText>
<OutputItemSelect />
<OutputItemSelect />
</div>
<N8nText :class="$style.body">
<ExpressionOutput
data-test-id="inline-expression-editor-output"
:segments="segments"
:extensions="theme"
>
</ExpressionOutput>
</N8nText>
<div v-if="!isReadOnly" :class="$style.footer">
<InlineExpressionTip
:editor-state="editorState"
:selection="selection"
:unresolved-expression="unresolvedExpression"
/>
</div>
</div>
<n8n-text :class="$style.body">
<ExpressionOutput
data-test-id="inline-expression-editor-output"
:segments="segments"
:extensions="theme"
>
</ExpressionOutput>
</n8n-text>
<div v-if="!isReadOnly" :class="$style.footer">
<InlineExpressionTip
:editor-state="editorState"
:selection="selection"
:unresolved-expression="unresolvedExpression"
/>
</div>
</div>
</N8nPopover>
</template>
</N8nPopoverReka>
</template>
<style lang="scss" module>
.popper {
background-color: transparent !important;
padding: 0 !important;
border: none !important;
/* Override styles set for el-popper */
word-break: normal;
text-align: unset;
.popover {
border-top: none;
border-top-left-radius: 0;
border-top-right-radius: 0;
}
.dropdown {
display: flex;
flex-direction: column;
background: var(--color-code-background);
border: var(--border-base);
border-top: none;
width: 100%;
box-shadow: 0 2px 6px 0 rgba(#441c17, 0.1);
border-bottom-left-radius: 4px;

View File

@@ -4,7 +4,7 @@ import type { useNDVStore } from '@/stores/ndv.store';
import type { CompletionResult } from '@codemirror/autocomplete';
import { createTestingPinia } from '@pinia/testing';
import { faker } from '@faker-js/faker';
import { waitFor, within } from '@testing-library/vue';
import { fireEvent, waitFor, within } from '@testing-library/vue';
import userEvent from '@testing-library/user-event';
import type { useNodeTypesStore } from '@/stores/nodeTypes.store';
import { useSettingsStore } from '@/stores/settings.store';
@@ -15,6 +15,7 @@ import {
createMockEnterpriseSettings,
createTestNode,
createTestWorkflowObject,
createTestNodeProperties,
} from '@/__tests__/mocks';
import { useWorkflowsStore } from '@/stores/workflows.store';
import { NodeConnectionTypes, type INodeParameterResourceLocator } from 'n8n-workflow';
@@ -680,11 +681,13 @@ describe('ParameterInput.vue', () => {
global: { provide: { [ExpressionLocalResolveContextSymbol]: ctx } },
props: {
path: 'name',
parameter: { displayName: 'Name', name: 'name', type: 'string' },
parameter: createTestNodeProperties(),
modelValue: '',
},
});
await fireEvent.focusIn(rendered.container.querySelector('.parameter-input')!);
expect(rendered.queryByTestId('ndv-input-panel')).toBeInTheDocument();
});
@@ -693,16 +696,13 @@ describe('ParameterInput.vue', () => {
global: { provide: { [ExpressionLocalResolveContextSymbol]: ctx } },
props: {
path: 'name',
parameter: {
displayName: 'Name',
name: 'name',
type: 'string',
typeOptions: { editor: 'sqlEditor' },
},
parameter: createTestNodeProperties({ typeOptions: { editor: 'sqlEditor' } }),
modelValue: 'SELECT 1;',
},
});
await fireEvent.focusIn(rendered.container.querySelector('.parameter-input')!);
expect(rendered.queryByTestId('ndv-input-panel')).toBeInTheDocument();
});
@@ -711,11 +711,13 @@ describe('ParameterInput.vue', () => {
global: { provide: { [ExpressionLocalResolveContextSymbol]: ctx } },
props: {
path: 'name',
parameter: { displayName: 'Name', name: 'name', type: 'string' },
parameter: createTestNodeProperties(),
modelValue: '={{$today}}',
},
});
await fireEvent.focusIn(rendered.container.querySelector('.parameter-input')!);
expect(rendered.queryByTestId('ndv-input-panel')).toBeInTheDocument();
});
@@ -724,11 +726,13 @@ describe('ParameterInput.vue', () => {
global: { provide: { [ExpressionLocalResolveContextSymbol]: ctx } },
props: {
path: 'name',
parameter: { displayName: 'Name', name: 'name', type: 'string', isNodeSetting: true },
parameter: createTestNodeProperties({ isNodeSetting: true }),
modelValue: '',
},
});
await fireEvent.focusIn(rendered.container.querySelector('.parameter-input')!);
expect(rendered.queryByTestId('ndv-input-panel')).not.toBeInTheDocument();
});
@@ -737,11 +741,13 @@ describe('ParameterInput.vue', () => {
global: { provide: { [ExpressionLocalResolveContextSymbol]: ctx } },
props: {
path: 'name',
parameter: { displayName: 'Name', name: 'name', type: 'dateTime' },
parameter: createTestNodeProperties({ type: 'dateTime' }),
modelValue: '',
},
});
await fireEvent.focusIn(rendered.container.querySelector('.parameter-input')!);
expect(rendered.queryByTestId('ndv-input-panel')).not.toBeInTheDocument();
});
});

View File

@@ -194,21 +194,10 @@ describe('SqlEditor.vue', () => {
await focusEditor(container);
await userEvent.click(getByTestId(EXPRESSION_OUTPUT_TEST_ID));
await waitFor(() =>
expect(
queryByTestId(EXPRESSION_OUTPUT_TEST_ID)?.closest('[aria-hidden=false]'),
).toBeInTheDocument(),
);
await waitFor(() => expect(queryByTestId(EXPRESSION_OUTPUT_TEST_ID)).toBeInTheDocument());
// Does hide output when clicking outside the container
await userEvent.click(baseElement);
// NOTE: in testing, popover persists regardless of persist option.
// See https://github.com/element-plus/element-plus/blob/2.4.3/packages/components/tooltip/src/content.vue#L83-L90
await waitFor(() =>
expect(
queryByTestId(EXPRESSION_OUTPUT_TEST_ID)?.closest('[aria-hidden=true]'),
).toBeInTheDocument(),
);
await waitFor(() => expect(queryByTestId(EXPRESSION_OUTPUT_TEST_ID)).not.toBeInTheDocument());
});
});

View File

@@ -375,379 +375,3 @@ exports[`WhatsNewModal > should not render update button when no version updates
</div>
</div>
`;
exports[`WhatsNewModal > should render with update button disabled 1`] = `
<div
class="article"
data-test-id="whats-new-item-1"
>
<h2
class="n8n-heading size-xlarge bold"
>
Convert to sub-workflow
</h2>
<div
class="n8n-markdown markdown"
>
<!-- Needed to support YouTube player embeds. HTML rendered here is sanitized. -->
<!-- eslint-disable vue/no-v-html -->
<div
class="markdown"
>
<p>
Large, monolithic workflows can slow things down. Theyre harder to maintain, tougher to debug, and more difficult to scale. With sub-workflows, you can take a more modular approach, breaking up big workflows into smaller, manageable parts that are easier to reuse, test, understand, and explain.
</p>
<p>
Until now, creating sub-workflows required copying and pasting nodes manually, setting up a new workflow from scratch, and reconnecting everything by hand.
<strong>
Convert to sub-workflow
</strong>
allows you to simplify this process into a single action, so you can spend more time building and less time restructuring.
</p>
<h3>
How it works
</h3>
<ol>
<li>
Highlight the nodes you want to convert to a sub-workflow. These must:
<ul>
<li>
Be fully connected, meaning no missing steps in between them
</li>
<li>
Start from a single starting node
</li>
<li>
End with a single node
</li>
</ul>
</li>
<li>
Right-click to open the context menu and select
<strong>
Convert to sub-workflow
</strong>
<ul>
<li>
Or use the shortcut:
<code>
Alt + X
</code>
</li>
</ul>
</li>
<li>
n8n will:
<ul>
<li>
Open a new tab containing the selected nodes
</li>
<li>
Preserve all node parameters as-is
</li>
<li>
Replace the selected nodes in the original workflow with a
<strong>
Call My Sub-workflow
</strong>
node
</li>
</ul>
</li>
</ol>
<p>
<em>
Note:
</em>
You will need to manually adjust the field types in the Start and Return nodes in the new sub-workflow.
</p>
<p>
This makes it easier to keep workflows modular, performant, and easier to maintain.
</p>
<p>
Learn more about
<a
href="https://docs.n8n.io/flow-logic/subworkflows/"
target="_blank"
>
sub-workflows
</a>
.
</p>
<p>
This release contains performance improvements and bug fixes.
</p>
<p>
<iframe
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowfullscreen=""
frameborder="0"
height="315"
referrerpolicy="strict-origin-when-cross-origin"
src="https://www.youtube-nocookie.com/embed/ZCuL2e4zC_4"
title="YouTube video player"
width="100%"
/>
</p>
<p>
Fusce malesuada diam eget tincidunt ultrices. Mauris quis mauris mollis, venenatis risus ut.
</p>
<h2>
Second level title
</h2>
<h3>
Third level title
</h3>
<p>
This
<strong>
is bold
</strong>
, this
<em>
in italics
</em>
.
<br />
<s>
Strikethrough is also something we support
</s>
.
</p>
<p>
Heres a peace of code:
</p>
<pre>
<code>
const props = defineProps&lt;{
modalName: string;
data: {
articleId: number;
};
}&gt;();
</code>
</pre>
<p>
Inline
<code>
code also works
</code>
withing text.
</p>
<p>
This is a list:
</p>
<ul>
<li>
first
</li>
<li>
second
</li>
<li>
third
</li>
</ul>
<p>
And this list is ordered
</p>
<ol>
<li>
foo
</li>
<li>
bar
</li>
<li>
qux
</li>
</ol>
<p>
Dividers:
</p>
<p>
Three or more…
</p>
<hr />
<p>
Hyphens
</p>
<hr />
<p>
Asterisks
</p>
<hr />
<p>
Underscores
</p>
<hr />
<details>
<summary>
Fixes (4)
</summary>
<ul>
<li>
<strong>
Credential Storage Issue
</strong>
Resolved an issue where credentials would occasionally become inaccessible after server restarts
</li>
<li>
<strong>
Webhook Timeout Handling
</strong>
Fixed timeout issues with long-running webhook requests
</li>
<li>
<strong>
Node Connection Validation
</strong>
Improved validation for node connections to prevent invalid workflow configurations
</li>
<li>
<strong>
Memory Leak in Execution Engine
</strong>
Fixed memory leak that could occur during long-running workflow executions
</li>
</ul>
</details>
</div>
</div>
</div>
`;

View File

@@ -3,13 +3,12 @@ import InputPanel from '@/components/InputPanel.vue';
import { CanvasKey } from '@/constants';
import type { INodeUi } from '@/Interface';
import { useNDVStore } from '@/stores/ndv.store';
import { ElPopover } from 'element-plus';
import { useVueFlow } from '@vue-flow/core';
import { useCanvasStore } from '@/stores/canvas.store';
import { onBeforeUnmount, watch } from 'vue';
import type { Workflow } from 'n8n-workflow';
import { computed, inject, ref, useTemplateRef } from 'vue';
import { useElementBounding, useElementSize } from '@vueuse/core';
import { computed, inject, useTemplateRef } from 'vue';
import { N8nPopoverReka } from '@n8n/design-system';
import { useStyles } from '@/composables/useStyles';
const { node, inputNodeName, visible, virtualRef } = defineProps<{
workflow: Workflow;
@@ -21,19 +20,15 @@ const { node, inputNodeName, visible, virtualRef } = defineProps<{
const contentRef = useTemplateRef('content');
const ndvStore = useNDVStore();
const vf = useVueFlow();
const canvas = inject(CanvasKey, undefined);
const isVisible = computed(() => visible && !canvas?.isPaneMoving.value);
const isOnceVisible = ref(isVisible.value);
const canvasStore = useCanvasStore();
const contentElRef = computed(() => contentRef.value?.$el ?? null);
const contentSize = useElementSize(contentElRef);
const refBounding = useElementBounding(virtualRef);
const { APP_Z_INDEXES } = useStyles();
watch(
isVisible,
(value) => {
isOnceVisible.value = isOnceVisible.value || value;
canvasStore.setSuppressInteraction(value);
},
{ immediate: true },
@@ -49,74 +44,41 @@ defineExpose({
</script>
<template>
<ElPopover
:visible="isVisible"
placement="left-start"
:show-arrow="false"
:popper-class="`${$style.component} ignore-key-press-canvas`"
:width="360"
:offset="8"
append-to="body"
:popper-options="{
modifiers: [
{ name: 'flip', enabled: false },
{
// Ensures that the popover is re-positioned when the reference element is resized
name: 'custom modifier',
options: {
refX: refBounding.x.value,
refY: refBounding.y.value,
width: contentSize.width.value,
height: contentSize?.height.value,
},
},
],
}"
:persistent="isOnceVisible /* works like lazy initialization */"
virtual-triggering
:virtual-ref="virtualRef"
<N8nPopoverReka
:open="isVisible"
side="left"
:side-flip="false"
align="start"
width="360px"
:max-height="`calc(100vh - var(--spacing-s) * 2)`"
:reference="virtualRef"
:suppress-auto-focus="true"
:z-index="APP_Z_INDEXES.NDV + 1"
>
<InputPanel
ref="content"
:tabindex="-1"
:class="$style.inputPanel"
:style="{
maxHeight: `calc(${vf.viewportRef.value?.offsetHeight ?? 0}px - var(--spacing-s) * 2)`,
}"
:workflow-object="workflow"
:run-index="0"
compact
push-ref=""
display-mode="schema"
disable-display-mode-selection
:active-node-name="node.name"
:current-node-name="inputNodeName"
:is-mapping-onboarded="ndvStore.isMappingOnboarded"
:focused-mappable-input="ndvStore.focusedMappableInput"
node-not-run-message-variant="simple"
/>
</ElPopover>
<template #content>
<InputPanel
ref="content"
:tabindex="-1"
:class="[$style.inputPanel, 'ignore-key-press-canvas']"
:workflow-object="workflow"
:run-index="0"
compact
push-ref=""
display-mode="schema"
disable-display-mode-selection
:active-node-name="node.name"
:current-node-name="inputNodeName"
:is-mapping-onboarded="ndvStore.isMappingOnboarded"
:focused-mappable-input="ndvStore.focusedMappableInput"
node-not-run-message-variant="simple"
/>
</template>
</N8nPopoverReka>
</template>
<style lang="scss" module>
.component {
background-color: transparent !important;
padding: 0 !important;
border: none !important;
box-shadow: none !important;
margin-top: -2px;
/* Override styles set for el-popper */
word-break: normal;
text-align: unset;
}
.inputPanel {
border: var(--border-base);
border-width: 1px;
background-color: var(--color-background-light);
border-radius: var(--border-radius-large);
box-shadow: 0 2px 16px rgba(0, 0, 0, 0.05);
background-color: transparent;
padding: var(--spacing-2xs);
height: 100%;
overflow: auto;

View File

@@ -19,6 +19,6 @@ export class FocusPanel {
getMapper(): Locator {
// find from the entire page because the mapper is rendered as portal
return this.root.page().getByRole('tooltip').getByTestId('ndv-input-panel');
return this.root.page().getByRole('dialog').getByTestId('ndv-input-panel');
}
}