mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 01:56:46 +00:00
feat(editor): Show input panel for mapping in embedded NDV (no-changelog) (#17227)
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
import { createTestNode, createTestWorkflow, createTestWorkflowObject } from '@/__tests__/mocks';
|
||||
import { createComponentRenderer } from '@/__tests__/render';
|
||||
import InputPanel, { type Props } from '@/components/InputPanel.vue';
|
||||
import { STORES } from '@n8n/stores';
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import { waitFor } from '@testing-library/vue';
|
||||
@@ -50,7 +49,6 @@ const render = (props: Partial<Props> = {}, pinData?: INodeExecutionData[], runD
|
||||
|
||||
const pinia = createTestingPinia({
|
||||
stubActions: false,
|
||||
initialState: { [STORES.NDV]: { activeNodeName: props.currentNodeName ?? nodes[1].name } },
|
||||
});
|
||||
setActivePinia(pinia);
|
||||
|
||||
@@ -97,9 +95,12 @@ const render = (props: Partial<Props> = {}, pinData?: INodeExecutionData[], runD
|
||||
props: {
|
||||
pushRef: 'pushRef',
|
||||
runIndex: 0,
|
||||
currentNodeName: nodes[1].name,
|
||||
currentNodeName: nodes[0].name,
|
||||
activeNodeName: nodes[1].name,
|
||||
workflow: workflowObject,
|
||||
displayMode: 'schema',
|
||||
focusedMappableInput: '',
|
||||
isMappingOnboarded: false,
|
||||
},
|
||||
global: {
|
||||
stubs: {
|
||||
@@ -118,14 +119,14 @@ describe('InputPanel', () => {
|
||||
});
|
||||
|
||||
it("opens mapping tab by default if the node hasn't run yet", async () => {
|
||||
const { findByTestId } = render({ currentNodeName: 'Tool' });
|
||||
const { findByTestId } = render({ activeNodeName: 'Tool' });
|
||||
|
||||
expect((await findByTestId('radio-button-mapping')).parentNode).toBeChecked();
|
||||
expect((await findByTestId('radio-button-debugging')).parentNode).not.toBeChecked();
|
||||
});
|
||||
|
||||
it('opens debugging tab by default if the node has already run', async () => {
|
||||
const { findByTestId } = render({ currentNodeName: 'Tool' }, undefined, {
|
||||
const { findByTestId } = render({ activeNodeName: 'Tool' }, undefined, {
|
||||
Tool: [
|
||||
{
|
||||
startTime: 0,
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
} from '@/constants';
|
||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import { useNDVStore } from '@/stores/ndv.store';
|
||||
import { waitingNodeTooltip } from '@/utils/executionUtils';
|
||||
import uniqBy from 'lodash/uniqBy';
|
||||
import { N8nIcon, N8nRadioButtons, N8nText, N8nTooltip } from '@n8n/design-system';
|
||||
@@ -22,7 +21,6 @@ import {
|
||||
NodeConnectionTypes,
|
||||
NodeHelpers,
|
||||
} from 'n8n-workflow';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import InputNodeSelect from './InputNodeSelect.vue';
|
||||
import NodeExecuteButton from './NodeExecuteButton.vue';
|
||||
@@ -38,6 +36,7 @@ export type Props = {
|
||||
runIndex: number;
|
||||
workflow: Workflow;
|
||||
pushRef: string;
|
||||
activeNodeName: string;
|
||||
currentNodeName?: string;
|
||||
canLinkRuns?: boolean;
|
||||
linkedRuns?: boolean;
|
||||
@@ -45,6 +44,10 @@ export type Props = {
|
||||
isProductionExecutionPreview?: boolean;
|
||||
isPaneActive?: boolean;
|
||||
displayMode: IRunDataDisplayMode;
|
||||
compact?: boolean;
|
||||
disableDisplayModeSelection?: boolean;
|
||||
focusedMappableInput: string;
|
||||
isMappingOnboarded: boolean;
|
||||
};
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
@@ -91,15 +94,10 @@ const inputModes = [
|
||||
];
|
||||
|
||||
const nodeTypesStore = useNodeTypesStore();
|
||||
const ndvStore = useNDVStore();
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const posthogStore = usePostHog();
|
||||
|
||||
const {
|
||||
activeNode,
|
||||
focusedMappableInput,
|
||||
isMappingOnboarded: isUserOnboarded,
|
||||
} = storeToRefs(ndvStore);
|
||||
const activeNode = computed(() => workflowsStore.getNodeByName(props.activeNodeName));
|
||||
|
||||
const rootNode = computed(() => {
|
||||
if (!activeNode.value) return null;
|
||||
@@ -128,7 +126,7 @@ const showDraggableHint = computed(() => {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !!focusedMappableInput.value && !isUserOnboarded.value;
|
||||
return !!props.focusedMappableInput && !props.isMappingOnboarded;
|
||||
});
|
||||
|
||||
const isActiveNodeConfig = computed(() => {
|
||||
@@ -396,6 +394,8 @@ function handleChangeCollapsingColumn(columnName: string | null) {
|
||||
data-test-id="ndv-input-panel"
|
||||
:disable-ai-content="true"
|
||||
:collapsing-table-column-name="collapsingColumnName"
|
||||
:compact="compact"
|
||||
:disable-display-mode-selection="disableDisplayModeSelection"
|
||||
@activate-pane="activatePane"
|
||||
@item-hover="onItemHover"
|
||||
@link-run="onLinkRun"
|
||||
@@ -408,9 +408,14 @@ function handleChangeCollapsingColumn(columnName: string | null) {
|
||||
>
|
||||
<template #header>
|
||||
<div :class="[$style.titleSection, { [$style.titleSectionV2]: isNDVV2 }]">
|
||||
<span :class="[$style.title, { [$style.titleV2]: isNDVV2 }]">{{
|
||||
i18n.baseText('ndv.input')
|
||||
}}</span>
|
||||
<N8nText
|
||||
:bold="true"
|
||||
color="text-light"
|
||||
:size="compact ? 'small' : 'medium'"
|
||||
:class="[$style.title, { [$style.titleV2]: isNDVV2 }]"
|
||||
>
|
||||
{{ i18n.baseText('ndv.input') }}
|
||||
</N8nText>
|
||||
<N8nRadioButtons
|
||||
v-if="isActiveNodeConfig && !readOnly"
|
||||
data-test-id="input-panel-mode"
|
||||
@@ -686,10 +691,7 @@ function handleChangeCollapsingColumn(columnName: string | null) {
|
||||
|
||||
.title {
|
||||
text-transform: uppercase;
|
||||
color: var(--color-text-light);
|
||||
letter-spacing: 3px;
|
||||
font-size: var(--font-size-s);
|
||||
font-weight: var(--font-weight-bold);
|
||||
}
|
||||
|
||||
.titleV2 {
|
||||
|
||||
@@ -783,12 +783,15 @@ onBeforeUnmount(() => {
|
||||
:can-link-runs="canLinkRuns"
|
||||
:run-index="inputRun"
|
||||
:linked-runs="linked"
|
||||
:active-node-name="activeNode.name"
|
||||
:current-node-name="inputNodeName"
|
||||
:push-ref="pushRef"
|
||||
:read-only="readOnly || hasForeignCredential"
|
||||
:is-production-execution-preview="isProductionExecutionPreview"
|
||||
:is-pane-active="isInputPaneActive"
|
||||
:display-mode="inputPanelDisplayMode"
|
||||
:is-mapping-onboarded="ndvStore.isMappingOnboarded"
|
||||
:focused-mappable-input="ndvStore.focusedMappableInput"
|
||||
@activate-pane="activateInputPane"
|
||||
@link-run="onLinkRunToInput"
|
||||
@unlink-run="() => onUnlinkRun('input')"
|
||||
|
||||
@@ -759,6 +759,7 @@ onBeforeUnmount(() => {
|
||||
:can-link-runs="canLinkRuns"
|
||||
:run-index="inputRun"
|
||||
:linked-runs="linked"
|
||||
:active-node-name="activeNode.name"
|
||||
:current-node-name="inputNodeName"
|
||||
:push-ref="pushRef"
|
||||
:read-only="readOnly || hasForeignCredential"
|
||||
@@ -766,6 +767,8 @@ onBeforeUnmount(() => {
|
||||
:is-pane-active="isInputPaneActive"
|
||||
:display-mode="inputPanelDisplayMode"
|
||||
:class="$style.input"
|
||||
:is-mapping-onboarded="ndvStore.isMappingOnboarded"
|
||||
:focused-mappable-input="ndvStore.focusedMappableInput"
|
||||
@activate-pane="activateInputPane"
|
||||
@link-run="onLinkRunToInput"
|
||||
@unlink-run="() => onUnlinkRun('input')"
|
||||
|
||||
@@ -1126,7 +1126,7 @@ function handleWheelEvent(event: WheelEvent) {
|
||||
&::-webkit-scrollbar-thumb {
|
||||
border-radius: var(--spacing-2xs);
|
||||
background: var(--color-foreground-dark);
|
||||
border: var(--spacing-5xs) solid white;
|
||||
border: var(--spacing-5xs) solid var(--color-background-xlight);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,6 +146,7 @@ type Props = {
|
||||
hidePagination?: boolean;
|
||||
calloutMessage?: string;
|
||||
disableRunIndexSelection?: boolean;
|
||||
disableDisplayModeSelection?: boolean;
|
||||
disableEdit?: boolean;
|
||||
disablePin?: boolean;
|
||||
compact?: boolean;
|
||||
@@ -168,6 +169,7 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
hidePagination: false,
|
||||
calloutMessage: undefined,
|
||||
disableRunIndexSelection: false,
|
||||
disableDisplayModeSelection: false,
|
||||
disableEdit: false,
|
||||
disablePin: false,
|
||||
disableHoverHighlight: false,
|
||||
@@ -1447,6 +1449,7 @@ defineExpose({ enterEditMode });
|
||||
/>
|
||||
|
||||
<RunDataDisplayModeSelect
|
||||
v-if="!disableDisplayModeSelection"
|
||||
v-show="
|
||||
hasPreviewSchema ||
|
||||
(hasNodeRun &&
|
||||
|
||||
@@ -22,9 +22,11 @@ exports[`InputPanel > should render 1`] = `
|
||||
class="titleSection"
|
||||
>
|
||||
<span
|
||||
class="title"
|
||||
class="n8n-text text-light size-medium bold title title"
|
||||
>
|
||||
|
||||
Input
|
||||
|
||||
</span>
|
||||
<div
|
||||
class="n8n-radio-buttons radioGroup"
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
import ExperimentalCanvasNodeSettings from './ExperimentalCanvasNodeSettings.vue';
|
||||
import { onBeforeUnmount, ref, computed, provide } from 'vue';
|
||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import { useExperimentalNdvStore } from '../experimentalNdv.store';
|
||||
import InputPanel from '@/components/InputPanel.vue';
|
||||
import NodeTitle from '@/components/NodeTitle.vue';
|
||||
import { N8nIcon, N8nIconButton } from '@n8n/design-system';
|
||||
import { useVueFlow } from '@vue-flow/core';
|
||||
import { watchOnce } from '@vueuse/core';
|
||||
import { ExpressionLocalResolveContextSymbol } from '@/constants';
|
||||
import { useEnvironmentsStore } from '@/stores/environments.ee.store';
|
||||
import { useNDVStore } from '@/stores/ndv.store';
|
||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import type { ExpressionLocalResolveContext } from '@/types/expressions';
|
||||
import { N8nIcon, N8nIconButton } from '@n8n/design-system';
|
||||
import { useVueFlow } from '@vue-flow/core';
|
||||
import { useActiveElement, watchOnce } from '@vueuse/core';
|
||||
import { computed, onBeforeUnmount, provide, ref, useTemplateRef, watch } from 'vue';
|
||||
import { useExperimentalNdvStore } from '../experimentalNdv.store';
|
||||
import ExperimentalCanvasNodeSettings from './ExperimentalCanvasNodeSettings.vue';
|
||||
|
||||
const { nodeId, isReadOnly, isConfigurable } = defineProps<{
|
||||
nodeId: string;
|
||||
@@ -18,6 +20,7 @@ const { nodeId, isReadOnly, isConfigurable } = defineProps<{
|
||||
isConfigurable: boolean;
|
||||
}>();
|
||||
|
||||
const ndvStore = useNDVStore();
|
||||
const experimentalNdvStore = useExperimentalNdvStore();
|
||||
const isExpanded = computed(() => !experimentalNdvStore.collapsedNodes[nodeId]);
|
||||
const nodeTypesStore = useNodeTypesStore();
|
||||
@@ -58,63 +61,81 @@ const isVisible = computed(() =>
|
||||
),
|
||||
);
|
||||
const isOnceVisible = ref(isVisible.value);
|
||||
const shouldShowInputPanel = ref(false);
|
||||
|
||||
provide(
|
||||
ExpressionLocalResolveContextSymbol,
|
||||
computed<ExpressionLocalResolveContext | undefined>(() => {
|
||||
if (!node.value) {
|
||||
return undefined;
|
||||
const containerRef = useTemplateRef('container');
|
||||
const inputPanelContainerRef = useTemplateRef('inputPanelContainer');
|
||||
const activeElement = useActiveElement();
|
||||
|
||||
const expressionResolveCtx = computed<ExpressionLocalResolveContext | undefined>(() => {
|
||||
if (!node.value) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const runIndex = 0; // not changeable for now
|
||||
const execution = workflowsStore.workflowExecutionData;
|
||||
const nodeName = node.value.name;
|
||||
|
||||
function findInputNode(): ExpressionLocalResolveContext['inputNode'] {
|
||||
const taskData = (execution?.data?.resultData.runData[nodeName] ?? [])[runIndex];
|
||||
const source = taskData?.source[0];
|
||||
|
||||
if (source) {
|
||||
return {
|
||||
name: source.previousNode,
|
||||
branchIndex: source.previousNodeOutput ?? 0,
|
||||
runIndex: source.previousNodeRun ?? 0,
|
||||
};
|
||||
}
|
||||
|
||||
const workflow = workflowsStore.getCurrentWorkflow();
|
||||
const runIndex = 0; // not changeable for now
|
||||
const execution = workflowsStore.workflowExecutionData;
|
||||
const nodeName = node.value.name;
|
||||
const inputs = workflow.value.getParentNodesByDepth(nodeName, 1);
|
||||
|
||||
function findInputNode(): ExpressionLocalResolveContext['inputNode'] {
|
||||
const taskData = (execution?.data?.resultData.runData[nodeName] ?? [])[runIndex];
|
||||
const source = taskData?.source[0];
|
||||
|
||||
if (source) {
|
||||
return {
|
||||
name: source.previousNode,
|
||||
branchIndex: source.previousNodeOutput ?? 0,
|
||||
runIndex: source.previousNodeRun ?? 0,
|
||||
};
|
||||
}
|
||||
|
||||
const inputs = workflow.getParentNodesByDepth(nodeName, 1);
|
||||
|
||||
if (inputs.length > 0) {
|
||||
return {
|
||||
name: inputs[0].name,
|
||||
branchIndex: inputs[0].indicies[0] ?? 0,
|
||||
runIndex: 0,
|
||||
};
|
||||
}
|
||||
|
||||
return undefined;
|
||||
if (inputs.length > 0) {
|
||||
return {
|
||||
name: inputs[0].name,
|
||||
branchIndex: inputs[0].indicies[0] ?? 0,
|
||||
runIndex: 0,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
localResolve: true,
|
||||
envVars: useEnvironmentsStore().variablesAsObject,
|
||||
workflow,
|
||||
execution,
|
||||
nodeName,
|
||||
additionalKeys: {},
|
||||
inputNode: findInputNode(),
|
||||
};
|
||||
}),
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
localResolve: true,
|
||||
envVars: useEnvironmentsStore().variablesAsObject,
|
||||
workflow: workflow.value,
|
||||
execution,
|
||||
nodeName,
|
||||
additionalKeys: {},
|
||||
inputNode: findInputNode(),
|
||||
};
|
||||
});
|
||||
|
||||
const workflow = computed(() => workflowsStore.getCurrentWorkflow());
|
||||
|
||||
function handleToggleExpand() {
|
||||
experimentalNdvStore.setNodeExpanded(nodeId);
|
||||
}
|
||||
|
||||
provide(ExpressionLocalResolveContextSymbol, expressionResolveCtx);
|
||||
|
||||
watchOnce(isVisible, (visible) => {
|
||||
isOnceVisible.value = isOnceVisible.value || visible;
|
||||
});
|
||||
|
||||
function handleToggleExpand() {
|
||||
experimentalNdvStore.setNodeExpanded(nodeId);
|
||||
}
|
||||
watch([activeElement, vf.getSelectedNodes], ([active, selected]) => {
|
||||
if (active && containerRef.value?.contains(active)) {
|
||||
// TODO: find a way to implement this without depending on test ID
|
||||
shouldShowInputPanel.value =
|
||||
!!active.closest('[data-test-id=inline-expression-editor-input]') ||
|
||||
!!inputPanelContainerRef.value?.contains(active);
|
||||
}
|
||||
|
||||
if (selected.every((sel) => sel.id !== node.value?.id)) {
|
||||
shouldShowInputPanel.value = false;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -129,6 +150,7 @@ function handleToggleExpand() {
|
||||
<template v-if="isOnceVisible">
|
||||
<ExperimentalCanvasNodeSettings
|
||||
v-if="isExpanded"
|
||||
tabindex="-1"
|
||||
:node-id="nodeId"
|
||||
:class="$style.settingsView"
|
||||
:no-wheel="
|
||||
@@ -158,6 +180,34 @@ function handleToggleExpand() {
|
||||
/>
|
||||
<N8nIcon icon="maximize-2" size="large" />
|
||||
</div>
|
||||
<Transition name="input">
|
||||
<div
|
||||
v-if="shouldShowInputPanel && node"
|
||||
ref="inputPanelContainer"
|
||||
:class="$style.inputPanelContainer"
|
||||
:tabindex="-1"
|
||||
>
|
||||
<InputPanel
|
||||
:class="$style.inputPanel"
|
||||
:workflow="workflow"
|
||||
:run-index="0"
|
||||
compact
|
||||
push-ref=""
|
||||
display-mode="schema"
|
||||
disable-display-mode-selection
|
||||
:active-node-name="node.name"
|
||||
:current-node-name="expressionResolveCtx?.inputNode?.name"
|
||||
:is-mapping-onboarded="ndvStore.isMappingOnboarded"
|
||||
:focused-mappable-input="ndvStore.focusedMappableInput"
|
||||
>
|
||||
<template #header>
|
||||
<N8nText :class="$style.inputPanelTitle" :bold="true" color="text-light" size="small">
|
||||
Input
|
||||
</N8nText>
|
||||
</template>
|
||||
</InputPanel>
|
||||
</div>
|
||||
</Transition>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
@@ -167,7 +217,6 @@ function handleToggleExpand() {
|
||||
position: relative;
|
||||
align-items: flex-start;
|
||||
justify-content: stretch;
|
||||
overflow: hidden;
|
||||
border-width: 1px !important;
|
||||
border-radius: var(--border-radius-base) !important;
|
||||
width: calc(var(--canvas-node--width) * var(--node-width-scaler));
|
||||
@@ -175,9 +224,12 @@ function handleToggleExpand() {
|
||||
&.expanded {
|
||||
cursor: default;
|
||||
height: auto;
|
||||
max-height: min(calc(var(--canvas-node--height) * 2), 300px);
|
||||
min-height: var(--spacing-3xl);
|
||||
}
|
||||
&.collapsed {
|
||||
height: calc(16px * 4);
|
||||
overflow: hidden;
|
||||
height: var(--spacing-3xl);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -193,9 +245,10 @@ function handleToggleExpand() {
|
||||
:root .settingsView {
|
||||
z-index: 1000;
|
||||
width: 100%;
|
||||
border-radius: var(--border-radius-base);
|
||||
|
||||
height: auto;
|
||||
max-height: min(calc(var(--canvas-node--height) * 2), 300px);
|
||||
max-height: calc(min(calc(var(--canvas-node--height) * 2), 300px) - var(--border-width-base) * 2);
|
||||
min-height: var(--spacing-3xl); // should be multiple of GRID_SIZE
|
||||
}
|
||||
|
||||
@@ -221,4 +274,43 @@ function handleToggleExpand() {
|
||||
zoom: var(--zoom);
|
||||
}
|
||||
}
|
||||
|
||||
.inputPanelContainer {
|
||||
position: absolute;
|
||||
right: 100%;
|
||||
top: 0;
|
||||
padding-right: var(--spacing-4xs);
|
||||
margin-top: calc(-1 * var(--border-width-base));
|
||||
width: 180px;
|
||||
z-index: 2000;
|
||||
max-height: 80vh;
|
||||
}
|
||||
|
||||
.inputPanel {
|
||||
border: var(--border-base);
|
||||
border-width: 1px;
|
||||
background-color: var(--color-background-light);
|
||||
border-radius: var(--border-radius-large);
|
||||
zoom: var(--zoom);
|
||||
box-shadow: 0 2px 16px rgba(0, 0, 0, 0.05);
|
||||
padding: var(--spacing-2xs);
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.inputPanelTitle {
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 3px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.input-enter-active,
|
||||
.input-leave-active {
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.input-enter-from,
|
||||
.input-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user