mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-16 09:36:44 +00:00
refactor(editor): Migrate mapper popover to ruka UI (#19564)
This commit is contained in:
@@ -244,6 +244,9 @@ describe('AI Assistant::enabled', () => {
|
|||||||
|
|
||||||
ndv.getters.nodeExecuteButton().click();
|
ndv.getters.nodeExecuteButton().click();
|
||||||
|
|
||||||
|
// Wait for a message from AI to be shown
|
||||||
|
aiAssistant.getters.chatMessagesAssistant().should('have.length', 3);
|
||||||
|
|
||||||
getEditor()
|
getEditor()
|
||||||
.type('{selectall}')
|
.type('{selectall}')
|
||||||
.paste(
|
.paste(
|
||||||
|
|||||||
@@ -113,4 +113,32 @@ describe('N8nPopoverReka', () => {
|
|||||||
|
|
||||||
expect(wrapper.props('maxHeight')).toBeUndefined();
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,14 +1,31 @@
|
|||||||
<script setup lang="ts">
|
<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';
|
import N8nScrollArea from '../N8nScrollArea/N8nScrollArea.vue';
|
||||||
|
|
||||||
interface Props {
|
interface Props
|
||||||
open?: boolean;
|
extends Pick<PopoverContentProps, 'side' | 'align' | 'sideFlip' | 'sideOffset' | 'reference'>,
|
||||||
|
Pick<PopoverRootProps, 'open'> {
|
||||||
/**
|
/**
|
||||||
* Whether to enable scrolling in the popover content
|
* Whether to enable scrolling in the popover content
|
||||||
*/
|
*/
|
||||||
enableScrolling?: boolean;
|
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
|
* Scrollbar visibility behavior
|
||||||
*/
|
*/
|
||||||
@@ -17,14 +34,18 @@ interface Props {
|
|||||||
* Popover width
|
* Popover width
|
||||||
*/
|
*/
|
||||||
width?: string;
|
width?: string;
|
||||||
|
/**
|
||||||
|
* z-index of popover content
|
||||||
|
*/
|
||||||
|
zIndex?: number | CSSProperties['zIndex'];
|
||||||
/**
|
/**
|
||||||
* Popover max height
|
* Popover max height
|
||||||
*/
|
*/
|
||||||
maxHeight?: string;
|
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 {
|
interface Emits {
|
||||||
@@ -32,15 +53,24 @@ interface Emits {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
open: undefined,
|
|
||||||
maxHeight: undefined,
|
maxHeight: undefined,
|
||||||
width: undefined,
|
width: undefined,
|
||||||
enableScrolling: true,
|
enableScrolling: true,
|
||||||
|
enableSlideIn: true,
|
||||||
scrollType: 'hover',
|
scrollType: 'hover',
|
||||||
align: undefined,
|
sideOffset: 5,
|
||||||
|
sideFlip: undefined,
|
||||||
|
suppressAutoFocus: false,
|
||||||
|
zIndex: 999,
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits<Emits>();
|
const emit = defineEmits<Emits>();
|
||||||
|
|
||||||
|
function handleOpenAutoFocus(e: Event) {
|
||||||
|
if (props.suppressAutoFocus) {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -49,21 +79,29 @@ const emit = defineEmits<Emits>();
|
|||||||
<slot name="trigger"></slot>
|
<slot name="trigger"></slot>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverPortal>
|
<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
|
<N8nScrollArea
|
||||||
v-if="enableScrolling"
|
v-if="enableScrolling"
|
||||||
:max-height="props.maxHeight"
|
:max-height="maxHeight"
|
||||||
:type="scrollType"
|
:type="scrollType"
|
||||||
:enable-vertical-scroll="true"
|
:enable-vertical-scroll="true"
|
||||||
:enable-horizontal-scroll="false"
|
: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)" />
|
<slot name="content" :close="() => emit('update:open', false)" />
|
||||||
</div>
|
</N8nScrollArea>
|
||||||
|
<template v-else>
|
||||||
|
<slot name="content" :close="() => emit('update:open', false)" />
|
||||||
|
</template>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</PopoverPortal>
|
</PopoverPortal>
|
||||||
</PopoverRoot>
|
</PopoverRoot>
|
||||||
@@ -77,10 +115,12 @@ const emit = defineEmits<Emits>();
|
|||||||
box-shadow:
|
box-shadow:
|
||||||
rgba(0, 0, 0, 0.1) 0 10px 15px -3px,
|
rgba(0, 0, 0, 0.1) 0 10px 15px -3px,
|
||||||
rgba(0, 0, 0, 0.05) 0 4px 6px -2px;
|
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;
|
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'] {
|
.popoverContent[data-state='open'][data-side='top'] {
|
||||||
|
|||||||
@@ -4,13 +4,11 @@ exports[`N8nPopoverReka > should render correctly with default props 1`] = `
|
|||||||
"<mock-popover-root>
|
"<mock-popover-root>
|
||||||
<mock-popover-trigger><button></button></mock-popover-trigger>
|
<mock-popover-trigger><button></button></mock-popover-trigger>
|
||||||
<mock-popover-portal>
|
<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 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 data-reka-scroll-area-viewport="" style="overflow-x: hidden; overflow-y: hidden;" class="viewport" tabindex="0">
|
||||||
<div>
|
<div>
|
||||||
<div>
|
<content></content>
|
||||||
<content></content>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import type {
|
|||||||
INodeTypeDescription,
|
INodeTypeDescription,
|
||||||
INodeIssues,
|
INodeIssues,
|
||||||
ITaskData,
|
ITaskData,
|
||||||
|
INodeProperties,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import { FORM_TRIGGER_NODE_TYPE, NodeConnectionTypes, NodeHelpers, Workflow } from 'n8n-workflow';
|
import { FORM_TRIGGER_NODE_TYPE, NodeConnectionTypes, NodeHelpers, Workflow } from 'n8n-workflow';
|
||||||
import { v4 as uuid } from 'uuid';
|
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(
|
export function createMockEnterpriseSettings(
|
||||||
overrides: Partial<FrontendSettings['enterprise']> = {},
|
overrides: Partial<FrontendSettings['enterprise']> = {},
|
||||||
): FrontendSettings['enterprise'] {
|
): FrontendSettings['enterprise'] {
|
||||||
|
|||||||
@@ -236,7 +236,6 @@ defineExpose({ focus, select });
|
|||||||
:segments="segments"
|
:segments="segments"
|
||||||
:is-read-only="isReadOnly"
|
:is-read-only="isReadOnly"
|
||||||
:virtual-ref="container"
|
:virtual-ref="container"
|
||||||
:append-to="isInExperimentalNdv ? 'body' : undefined"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import InlineExpressionEditorOutput from './InlineExpressionEditorOutput.vue';
|
|||||||
|
|
||||||
describe('InlineExpressionEditorOutput.vue', () => {
|
describe('InlineExpressionEditorOutput.vue', () => {
|
||||||
test('should render duplicate segments correctly', async () => {
|
test('should render duplicate segments correctly', async () => {
|
||||||
const { getByTestId } = renderComponent(InlineExpressionEditorOutput, {
|
const rendered = renderComponent(InlineExpressionEditorOutput, {
|
||||||
pinia: createTestingPinia(),
|
pinia: createTestingPinia(),
|
||||||
props: {
|
props: {
|
||||||
visible: true,
|
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', () => {
|
test('should render segments with resolved expressions', async () => {
|
||||||
const { getByTestId } = renderComponent(InlineExpressionEditorOutput, {
|
const rendered = renderComponent(InlineExpressionEditorOutput, {
|
||||||
pinia: createTestingPinia(),
|
pinia: createTestingPinia(),
|
||||||
props: {
|
props: {
|
||||||
visible: true,
|
visible: true,
|
||||||
@@ -93,8 +95,8 @@ describe('InlineExpressionEditorOutput.vue', () => {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(getByTestId('inline-expression-editor-output')).toHaveTextContent(
|
const body = await rendered.findByTestId('inline-expression-editor-output');
|
||||||
'before> [Object: "2024-04-18T09:03:26.651-04:00"] <after',
|
|
||||||
);
|
expect(body).toHaveTextContent('before> [Object: "2024-04-18T09:03:26.651-04:00"] <after');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,13 +4,13 @@ import type { EditorState, SelectionRange } from '@codemirror/state';
|
|||||||
import { useI18n } from '@n8n/i18n';
|
import { useI18n } from '@n8n/i18n';
|
||||||
import { useNDVStore } from '@/stores/ndv.store';
|
import { useNDVStore } from '@/stores/ndv.store';
|
||||||
import type { Segment } from '@/types/expressions';
|
import type { Segment } from '@/types/expressions';
|
||||||
import { computed, onBeforeUnmount, useTemplateRef } from 'vue';
|
import { onBeforeUnmount, useTemplateRef } from 'vue';
|
||||||
import ExpressionOutput from './ExpressionOutput.vue';
|
import ExpressionOutput from './ExpressionOutput.vue';
|
||||||
import OutputItemSelect from './OutputItemSelect.vue';
|
import OutputItemSelect from './OutputItemSelect.vue';
|
||||||
import InlineExpressionTip from './InlineExpressionTip.vue';
|
import InlineExpressionTip from './InlineExpressionTip.vue';
|
||||||
import { outputTheme } from './theme';
|
import { outputTheme } from './theme';
|
||||||
import { useElementSize } from '@vueuse/core';
|
import { N8nPopoverReka, N8nText } from '@n8n/design-system';
|
||||||
import { N8nPopover } from '@n8n/design-system';
|
import { useStyles } from '@/composables/useStyles';
|
||||||
|
|
||||||
interface InlineExpressionEditorOutputProps {
|
interface InlineExpressionEditorOutputProps {
|
||||||
segments: Segment[];
|
segments: Segment[];
|
||||||
@@ -20,10 +20,9 @@ interface InlineExpressionEditorOutputProps {
|
|||||||
isReadOnly?: boolean;
|
isReadOnly?: boolean;
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
virtualRef?: HTMLElement;
|
virtualRef?: HTMLElement;
|
||||||
appendTo?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<InlineExpressionEditorOutputProps>(), {
|
withDefaults(defineProps<InlineExpressionEditorOutputProps>(), {
|
||||||
editorState: undefined,
|
editorState: undefined,
|
||||||
selection: undefined,
|
selection: undefined,
|
||||||
isReadOnly: false,
|
isReadOnly: false,
|
||||||
@@ -34,7 +33,7 @@ const i18n = useI18n();
|
|||||||
const theme = outputTheme();
|
const theme = outputTheme();
|
||||||
const ndvStore = useNDVStore();
|
const ndvStore = useNDVStore();
|
||||||
const contentRef = useTemplateRef('content');
|
const contentRef = useTemplateRef('content');
|
||||||
const virtualRefSize = useElementSize(computed(() => props.virtualRef));
|
const { APP_Z_INDEXES } = useStyles();
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
ndvStore.expressionOutputItemIndex = 0;
|
ndvStore.expressionOutputItemIndex = 0;
|
||||||
@@ -46,75 +45,60 @@ defineExpose({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<N8nPopover
|
<N8nPopoverReka
|
||||||
:visible="visible"
|
:open="visible"
|
||||||
placement="bottom"
|
side="bottom"
|
||||||
:show-arrow="false"
|
:side-flip="false"
|
||||||
:offset="0"
|
:side-offset="0"
|
||||||
:persistent="false"
|
align="start"
|
||||||
:virtual-triggering="virtualRef !== undefined"
|
:reference="virtualRef"
|
||||||
:virtual-ref="virtualRef"
|
width="var(--reka-popper-anchor-width)"
|
||||||
:width="virtualRefSize.width.value"
|
:content-class="$style.popover"
|
||||||
:popper-class="`${$style.popper} ignore-key-press-canvas`"
|
:enable-slide-in="false"
|
||||||
:popper-options="{
|
:enable-scrolling="false"
|
||||||
modifiers: [
|
:suppress-auto-focus="true"
|
||||||
{ name: 'flip', enabled: false },
|
:z-index="APP_Z_INDEXES.NDV + 1"
|
||||||
{
|
|
||||||
// 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"
|
|
||||||
>
|
>
|
||||||
<div ref="content" :class="$style.dropdown">
|
<template #content>
|
||||||
<div :class="$style.header">
|
<div ref="content" :class="[$style.dropdown, 'ignore-key-press-canvas']">
|
||||||
<n8n-text bold size="small" compact>
|
<div :class="$style.header">
|
||||||
{{ i18n.baseText('parameterInput.result') }}
|
<N8nText bold size="small" compact>
|
||||||
</n8n-text>
|
{{ 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>
|
</div>
|
||||||
<n8n-text :class="$style.body">
|
</template>
|
||||||
<ExpressionOutput
|
</N8nPopoverReka>
|
||||||
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>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
.popper {
|
.popover {
|
||||||
background-color: transparent !important;
|
border-top: none;
|
||||||
padding: 0 !important;
|
border-top-left-radius: 0;
|
||||||
border: none !important;
|
border-top-right-radius: 0;
|
||||||
|
|
||||||
/* Override styles set for el-popper */
|
|
||||||
word-break: normal;
|
|
||||||
text-align: unset;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown {
|
.dropdown {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
background: var(--color-code-background);
|
background: var(--color-code-background);
|
||||||
border: var(--border-base);
|
|
||||||
border-top: none;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
box-shadow: 0 2px 6px 0 rgba(#441c17, 0.1);
|
box-shadow: 0 2px 6px 0 rgba(#441c17, 0.1);
|
||||||
border-bottom-left-radius: 4px;
|
border-bottom-left-radius: 4px;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import type { useNDVStore } from '@/stores/ndv.store';
|
|||||||
import type { CompletionResult } from '@codemirror/autocomplete';
|
import type { CompletionResult } from '@codemirror/autocomplete';
|
||||||
import { createTestingPinia } from '@pinia/testing';
|
import { createTestingPinia } from '@pinia/testing';
|
||||||
import { faker } from '@faker-js/faker';
|
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 userEvent from '@testing-library/user-event';
|
||||||
import type { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
import type { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||||
import { useSettingsStore } from '@/stores/settings.store';
|
import { useSettingsStore } from '@/stores/settings.store';
|
||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
createMockEnterpriseSettings,
|
createMockEnterpriseSettings,
|
||||||
createTestNode,
|
createTestNode,
|
||||||
createTestWorkflowObject,
|
createTestWorkflowObject,
|
||||||
|
createTestNodeProperties,
|
||||||
} from '@/__tests__/mocks';
|
} from '@/__tests__/mocks';
|
||||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
import { NodeConnectionTypes, type INodeParameterResourceLocator } from 'n8n-workflow';
|
import { NodeConnectionTypes, type INodeParameterResourceLocator } from 'n8n-workflow';
|
||||||
@@ -680,11 +681,13 @@ describe('ParameterInput.vue', () => {
|
|||||||
global: { provide: { [ExpressionLocalResolveContextSymbol]: ctx } },
|
global: { provide: { [ExpressionLocalResolveContextSymbol]: ctx } },
|
||||||
props: {
|
props: {
|
||||||
path: 'name',
|
path: 'name',
|
||||||
parameter: { displayName: 'Name', name: 'name', type: 'string' },
|
parameter: createTestNodeProperties(),
|
||||||
modelValue: '',
|
modelValue: '',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await fireEvent.focusIn(rendered.container.querySelector('.parameter-input')!);
|
||||||
|
|
||||||
expect(rendered.queryByTestId('ndv-input-panel')).toBeInTheDocument();
|
expect(rendered.queryByTestId('ndv-input-panel')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -693,16 +696,13 @@ describe('ParameterInput.vue', () => {
|
|||||||
global: { provide: { [ExpressionLocalResolveContextSymbol]: ctx } },
|
global: { provide: { [ExpressionLocalResolveContextSymbol]: ctx } },
|
||||||
props: {
|
props: {
|
||||||
path: 'name',
|
path: 'name',
|
||||||
parameter: {
|
parameter: createTestNodeProperties({ typeOptions: { editor: 'sqlEditor' } }),
|
||||||
displayName: 'Name',
|
|
||||||
name: 'name',
|
|
||||||
type: 'string',
|
|
||||||
typeOptions: { editor: 'sqlEditor' },
|
|
||||||
},
|
|
||||||
modelValue: 'SELECT 1;',
|
modelValue: 'SELECT 1;',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await fireEvent.focusIn(rendered.container.querySelector('.parameter-input')!);
|
||||||
|
|
||||||
expect(rendered.queryByTestId('ndv-input-panel')).toBeInTheDocument();
|
expect(rendered.queryByTestId('ndv-input-panel')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -711,11 +711,13 @@ describe('ParameterInput.vue', () => {
|
|||||||
global: { provide: { [ExpressionLocalResolveContextSymbol]: ctx } },
|
global: { provide: { [ExpressionLocalResolveContextSymbol]: ctx } },
|
||||||
props: {
|
props: {
|
||||||
path: 'name',
|
path: 'name',
|
||||||
parameter: { displayName: 'Name', name: 'name', type: 'string' },
|
parameter: createTestNodeProperties(),
|
||||||
modelValue: '={{$today}}',
|
modelValue: '={{$today}}',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await fireEvent.focusIn(rendered.container.querySelector('.parameter-input')!);
|
||||||
|
|
||||||
expect(rendered.queryByTestId('ndv-input-panel')).toBeInTheDocument();
|
expect(rendered.queryByTestId('ndv-input-panel')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -724,11 +726,13 @@ describe('ParameterInput.vue', () => {
|
|||||||
global: { provide: { [ExpressionLocalResolveContextSymbol]: ctx } },
|
global: { provide: { [ExpressionLocalResolveContextSymbol]: ctx } },
|
||||||
props: {
|
props: {
|
||||||
path: 'name',
|
path: 'name',
|
||||||
parameter: { displayName: 'Name', name: 'name', type: 'string', isNodeSetting: true },
|
parameter: createTestNodeProperties({ isNodeSetting: true }),
|
||||||
modelValue: '',
|
modelValue: '',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await fireEvent.focusIn(rendered.container.querySelector('.parameter-input')!);
|
||||||
|
|
||||||
expect(rendered.queryByTestId('ndv-input-panel')).not.toBeInTheDocument();
|
expect(rendered.queryByTestId('ndv-input-panel')).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -737,11 +741,13 @@ describe('ParameterInput.vue', () => {
|
|||||||
global: { provide: { [ExpressionLocalResolveContextSymbol]: ctx } },
|
global: { provide: { [ExpressionLocalResolveContextSymbol]: ctx } },
|
||||||
props: {
|
props: {
|
||||||
path: 'name',
|
path: 'name',
|
||||||
parameter: { displayName: 'Name', name: 'name', type: 'dateTime' },
|
parameter: createTestNodeProperties({ type: 'dateTime' }),
|
||||||
modelValue: '',
|
modelValue: '',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await fireEvent.focusIn(rendered.container.querySelector('.parameter-input')!);
|
||||||
|
|
||||||
expect(rendered.queryByTestId('ndv-input-panel')).not.toBeInTheDocument();
|
expect(rendered.queryByTestId('ndv-input-panel')).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -194,21 +194,10 @@ describe('SqlEditor.vue', () => {
|
|||||||
await focusEditor(container);
|
await focusEditor(container);
|
||||||
await userEvent.click(getByTestId(EXPRESSION_OUTPUT_TEST_ID));
|
await userEvent.click(getByTestId(EXPRESSION_OUTPUT_TEST_ID));
|
||||||
|
|
||||||
await waitFor(() =>
|
await waitFor(() => expect(queryByTestId(EXPRESSION_OUTPUT_TEST_ID)).toBeInTheDocument());
|
||||||
expect(
|
|
||||||
queryByTestId(EXPRESSION_OUTPUT_TEST_ID)?.closest('[aria-hidden=false]'),
|
|
||||||
).toBeInTheDocument(),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Does hide output when clicking outside the container
|
// Does hide output when clicking outside the container
|
||||||
await userEvent.click(baseElement);
|
await userEvent.click(baseElement);
|
||||||
|
await waitFor(() => expect(queryByTestId(EXPRESSION_OUTPUT_TEST_ID)).not.toBeInTheDocument());
|
||||||
// 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(),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -375,379 +375,3 @@ exports[`WhatsNewModal > should not render update button when no version updates
|
|||||||
</div>
|
</div>
|
||||||
</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. They’re 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>
|
|
||||||
Here’s a peace of code:
|
|
||||||
</p>
|
|
||||||
|
|
||||||
|
|
||||||
<pre>
|
|
||||||
<code>
|
|
||||||
const props = defineProps<{
|
|
||||||
modalName: string;
|
|
||||||
data: {
|
|
||||||
articleId: number;
|
|
||||||
};
|
|
||||||
}>();
|
|
||||||
|
|
||||||
</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>
|
|
||||||
`;
|
|
||||||
|
|||||||
@@ -3,13 +3,12 @@ import InputPanel from '@/components/InputPanel.vue';
|
|||||||
import { CanvasKey } from '@/constants';
|
import { CanvasKey } from '@/constants';
|
||||||
import type { INodeUi } from '@/Interface';
|
import type { INodeUi } from '@/Interface';
|
||||||
import { useNDVStore } from '@/stores/ndv.store';
|
import { useNDVStore } from '@/stores/ndv.store';
|
||||||
import { ElPopover } from 'element-plus';
|
|
||||||
import { useVueFlow } from '@vue-flow/core';
|
|
||||||
import { useCanvasStore } from '@/stores/canvas.store';
|
import { useCanvasStore } from '@/stores/canvas.store';
|
||||||
import { onBeforeUnmount, watch } from 'vue';
|
import { onBeforeUnmount, watch } from 'vue';
|
||||||
import type { Workflow } from 'n8n-workflow';
|
import type { Workflow } from 'n8n-workflow';
|
||||||
import { computed, inject, ref, useTemplateRef } from 'vue';
|
import { computed, inject, useTemplateRef } from 'vue';
|
||||||
import { useElementBounding, useElementSize } from '@vueuse/core';
|
import { N8nPopoverReka } from '@n8n/design-system';
|
||||||
|
import { useStyles } from '@/composables/useStyles';
|
||||||
|
|
||||||
const { node, inputNodeName, visible, virtualRef } = defineProps<{
|
const { node, inputNodeName, visible, virtualRef } = defineProps<{
|
||||||
workflow: Workflow;
|
workflow: Workflow;
|
||||||
@@ -21,19 +20,15 @@ const { node, inputNodeName, visible, virtualRef } = defineProps<{
|
|||||||
|
|
||||||
const contentRef = useTemplateRef('content');
|
const contentRef = useTemplateRef('content');
|
||||||
const ndvStore = useNDVStore();
|
const ndvStore = useNDVStore();
|
||||||
const vf = useVueFlow();
|
|
||||||
const canvas = inject(CanvasKey, undefined);
|
const canvas = inject(CanvasKey, undefined);
|
||||||
const isVisible = computed(() => visible && !canvas?.isPaneMoving.value);
|
const isVisible = computed(() => visible && !canvas?.isPaneMoving.value);
|
||||||
const isOnceVisible = ref(isVisible.value);
|
|
||||||
const canvasStore = useCanvasStore();
|
const canvasStore = useCanvasStore();
|
||||||
const contentElRef = computed(() => contentRef.value?.$el ?? null);
|
const contentElRef = computed(() => contentRef.value?.$el ?? null);
|
||||||
const contentSize = useElementSize(contentElRef);
|
const { APP_Z_INDEXES } = useStyles();
|
||||||
const refBounding = useElementBounding(virtualRef);
|
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
isVisible,
|
isVisible,
|
||||||
(value) => {
|
(value) => {
|
||||||
isOnceVisible.value = isOnceVisible.value || value;
|
|
||||||
canvasStore.setSuppressInteraction(value);
|
canvasStore.setSuppressInteraction(value);
|
||||||
},
|
},
|
||||||
{ immediate: true },
|
{ immediate: true },
|
||||||
@@ -49,74 +44,41 @@ defineExpose({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ElPopover
|
<N8nPopoverReka
|
||||||
:visible="isVisible"
|
:open="isVisible"
|
||||||
placement="left-start"
|
side="left"
|
||||||
:show-arrow="false"
|
:side-flip="false"
|
||||||
:popper-class="`${$style.component} ignore-key-press-canvas`"
|
align="start"
|
||||||
:width="360"
|
width="360px"
|
||||||
:offset="8"
|
:max-height="`calc(100vh - var(--spacing-s) * 2)`"
|
||||||
append-to="body"
|
:reference="virtualRef"
|
||||||
:popper-options="{
|
:suppress-auto-focus="true"
|
||||||
modifiers: [
|
:z-index="APP_Z_INDEXES.NDV + 1"
|
||||||
{ 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"
|
|
||||||
>
|
>
|
||||||
<InputPanel
|
<template #content>
|
||||||
ref="content"
|
<InputPanel
|
||||||
:tabindex="-1"
|
ref="content"
|
||||||
:class="$style.inputPanel"
|
:tabindex="-1"
|
||||||
:style="{
|
:class="[$style.inputPanel, 'ignore-key-press-canvas']"
|
||||||
maxHeight: `calc(${vf.viewportRef.value?.offsetHeight ?? 0}px - var(--spacing-s) * 2)`,
|
:workflow-object="workflow"
|
||||||
}"
|
:run-index="0"
|
||||||
:workflow-object="workflow"
|
compact
|
||||||
:run-index="0"
|
push-ref=""
|
||||||
compact
|
display-mode="schema"
|
||||||
push-ref=""
|
disable-display-mode-selection
|
||||||
display-mode="schema"
|
:active-node-name="node.name"
|
||||||
disable-display-mode-selection
|
:current-node-name="inputNodeName"
|
||||||
:active-node-name="node.name"
|
:is-mapping-onboarded="ndvStore.isMappingOnboarded"
|
||||||
:current-node-name="inputNodeName"
|
:focused-mappable-input="ndvStore.focusedMappableInput"
|
||||||
:is-mapping-onboarded="ndvStore.isMappingOnboarded"
|
node-not-run-message-variant="simple"
|
||||||
:focused-mappable-input="ndvStore.focusedMappableInput"
|
/>
|
||||||
node-not-run-message-variant="simple"
|
</template>
|
||||||
/>
|
</N8nPopoverReka>
|
||||||
</ElPopover>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<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 {
|
.inputPanel {
|
||||||
border: var(--border-base);
|
background-color: transparent;
|
||||||
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);
|
|
||||||
padding: var(--spacing-2xs);
|
padding: var(--spacing-2xs);
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
|||||||
@@ -19,6 +19,6 @@ export class FocusPanel {
|
|||||||
|
|
||||||
getMapper(): Locator {
|
getMapper(): Locator {
|
||||||
// find from the entire page because the mapper is rendered as portal
|
// 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');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user