fix(editor): Data in input/output panel incorrectly mapped (#14878)

This commit is contained in:
Suguru Inoue
2025-04-28 14:30:44 +02:00
committed by GitHub
parent 2212aeba30
commit 0a2b740063
16 changed files with 358 additions and 68 deletions

View File

@@ -34,6 +34,7 @@ describe('LogDetailsPanel', () => {
executionStatus: 'success',
executionTime: 10,
data: { main: [[{ json: { response: 'Hello!' } }]] },
source: [{ previousNode: 'Chat Trigger' }],
});
function render(props: Partial<InstanceType<typeof LogDetailsPanel>['$props']>) {
@@ -116,7 +117,7 @@ describe('LogDetailsPanel', () => {
it('should toggle input and output panel when the button is clicked', async () => {
const rendered = render({
isOpen: true,
logEntry: createTestLogEntry({ node: aiNode, runIndex: 0 }),
logEntry: createTestLogEntry({ node: aiNode, runIndex: 0, runData: aiNodeRunData }),
});
const header = within(rendered.getByTestId('log-details-header'));
@@ -140,7 +141,7 @@ describe('LogDetailsPanel', () => {
const rendered = render({
isOpen: true,
logEntry: createTestLogEntry({ node: aiNode, runIndex: 0 }),
logEntry: createTestLogEntry({ node: aiNode, runIndex: 0, runData: aiNodeRunData }),
});
await fireEvent.mouseDown(rendered.getByTestId('resize-handle'));
@@ -159,7 +160,7 @@ describe('LogDetailsPanel', () => {
const rendered = render({
isOpen: true,
logEntry: createTestLogEntry({ node: aiNode, runIndex: 0 }),
logEntry: createTestLogEntry({ node: aiNode, runIndex: 0, runData: aiNodeRunData }),
});
await fireEvent.mouseDown(rendered.getByTestId('resize-handle'));

View File

@@ -19,6 +19,7 @@ import {
type LogEntry,
} from '@/components/RunDataAi/utils';
import { useVirtualList } from '@vueuse/core';
import { ndvEventBus } from '@/event-bus';
const { isOpen, isReadOnly, selected, isCompact, execution, latestNodeInfo, scrollToSelection } =
defineProps<{
@@ -88,7 +89,14 @@ function handleToggleExpanded(treeNode: LogEntry) {
async function handleOpenNdv(treeNode: LogEntry) {
ndvStore.setActiveNodeName(treeNode.node.name);
await nextTick(() => ndvStore.setOutputRunIndex(treeNode.runIndex));
await nextTick(() => {
const source = treeNode.runData.source[0];
const inputBranch = source?.previousNodeOutput ?? 0;
ndvEventBus.emit('updateInputNodeName', source?.previousNode);
ndvEventBus.emit('setInputBranchIndex', inputBranch);
ndvStore.setOutputRunIndex(treeNode.runIndex);
});
}
async function handleTriggerPartialExecution(treeNode: LogEntry) {
@@ -190,7 +198,7 @@ watch(
</div>
</div>
<N8nRadioButtons
size="small"
size="small-medium"
:class="$style.switchViewButtons"
:model-value="selected ? 'details' : 'overview'"
:options="switchViewOptions"
@@ -257,7 +265,7 @@ watch(
z-index: 10; /* higher than log entry rows background */
right: 0;
top: 0;
margin: var(--spacing-2xs);
margin: var(--spacing-4xs) var(--spacing-2xs);
visibility: hidden;
opacity: 0;
transition: opacity 0.3s $ease-out-expo;

View File

@@ -5,7 +5,6 @@ import { useI18n } from '@/composables/useI18n';
import { type IExecutionResponse, type NodePanelType } from '@/Interface';
import { useNDVStore } from '@/stores/ndv.store';
import { N8nLink, N8nText } from '@n8n/design-system';
import { uniq } from 'lodash-es';
import { type Workflow } from 'n8n-workflow';
import { computed } from 'vue';
import { I18nT } from 'vue-i18n';
@@ -20,17 +19,27 @@ const { title, logEntry, paneType, workflow, execution } = defineProps<{
const locale = useI18n();
const ndvStore = useNDVStore();
const parentNodeNames = computed(() =>
uniq(workflow.getParentNodesByDepth(logEntry.node.name, 1)).map((c) => c.name),
);
const node = computed(() => {
const isMultipleInput = computed(() => paneType === 'input' && logEntry.runData.source.length > 1);
const runDataProps = computed<
Pick<InstanceType<typeof RunData>['$props'], 'node' | 'runIndex' | 'overrideOutputs'> | undefined
>(() => {
if (logEntry.depth > 0 || paneType === 'output') {
return logEntry.node;
return { node: logEntry.node, runIndex: logEntry.runIndex };
}
return parentNodeNames.value.length > 0 ? workflow.getNode(parentNodeNames.value[0]) : undefined;
const source = logEntry.runData.source[0];
const node = source && workflow.getNode(source.previousNode);
if (!source || !node) {
return undefined;
}
return {
node,
runIndex: source.previousNodeRun ?? 0,
overrideOutputs: [source.previousNodeOutput ?? 0],
};
});
const isMultipleInput = computed(() => paneType === 'input' && parentNodeNames.value.length > 1);
function handleClickOpenNdv() {
ndvStore.setActiveNodeName(logEntry.node.name);
@@ -39,11 +48,10 @@ function handleClickOpenNdv() {
<template>
<RunData
v-if="node"
:node="node"
v-if="runDataProps"
v-bind="runDataProps"
:workflow="workflow"
:workflow-execution="execution"
:run-index="logEntry.runIndex"
:too-much-data-title="locale.baseText('ndv.output.tooMuchData.title')"
:no-data-in-branch-message="locale.baseText('ndv.output.noOutputDataInBranch')"
:executing-message="locale.baseText('ndv.output.executing')"

View File

@@ -22,7 +22,7 @@ import {
} from '@/constants';
import { useWorkflowActivate } from '@/composables/useWorkflowActivate';
import type { DataPinningDiscoveryEvent } from '@/event-bus';
import { dataPinningEventBus } from '@/event-bus';
import { dataPinningEventBus, ndvEventBus } from '@/event-bus';
import { useWorkflowsStore } from '@/stores/workflows.store';
import { useNDVStore } from '@/stores/ndv.store';
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
@@ -80,8 +80,8 @@ const settingsEventBus = createEventBus();
const redrawRequired = ref(false);
const runInputIndex = ref(-1);
const runOutputIndex = computed(() => ndvStore.output.run ?? -1);
const isLinkingEnabled = ref(true);
const selectedInput = ref<string | undefined>();
const isLinkingEnabled = ref(true);
const triggerWaitingWarningEnabled = ref(false);
const isDragging = ref(false);
const mainPanelPosition = ref(0);
@@ -615,6 +615,10 @@ const unregisterKeyboardListener = () => {
document.removeEventListener('keydown', onKeyDown, true);
};
const setSelectedInput = (value: string | undefined) => {
selectedInput.value = value;
};
//watchers
watch(
@@ -702,10 +706,12 @@ watch(inputRun, (inputRun) => {
onMounted(() => {
dataPinningEventBus.on('data-pinning-discovery', setIsTooltipVisible);
ndvEventBus.on('updateInputNodeName', setSelectedInput);
});
onBeforeUnmount(() => {
dataPinningEventBus.off('data-pinning-discovery', setIsTooltipVisible);
ndvEventBus.off('updateInputNodeName', setSelectedInput);
unregisterKeyboardListener();
});
</script>

View File

@@ -60,7 +60,7 @@ import type { PinDataSource, UnpinDataSource } from '@/composables/usePinnedData
import { usePinnedData } from '@/composables/usePinnedData';
import { useTelemetry } from '@/composables/useTelemetry';
import { useToast } from '@/composables/useToast';
import { dataPinningEventBus } from '@/event-bus';
import { dataPinningEventBus, ndvEventBus } from '@/event-bus';
import { useNDVStore } from '@/stores/ndv.store';
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import { useRootStore } from '@/stores/root.store';
@@ -617,6 +617,12 @@ const itemsCountProps = computed<InstanceType<typeof RunDataItemCount>['$props']
subExecutionsCount: activeTaskMetadata.value?.subExecutionsCount,
}));
function setInputBranchIndex(value: number) {
if (props.paneType === 'input') {
outputIndex.value = value;
}
}
watch(node, (newNode, prevNode) => {
if (newNode?.id === prevNode?.id) return;
init();
@@ -674,6 +680,8 @@ watch(search, (newSearch) => {
onMounted(() => {
init();
ndvEventBus.on('setInputBranchIndex', setInputBranchIndex);
if (!isPaneTypeInput.value) {
showPinDataDiscoveryTooltip(jsonData.value);
}
@@ -710,6 +718,7 @@ onMounted(() => {
onBeforeUnmount(() => {
hidePinDataDiscoveryTooltip();
ndvEventBus.off('setInputBranchIndex', setInputBranchIndex);
});
function getResolvedNodeOutputs() {
@@ -1551,6 +1560,7 @@ defineExpose({ enterEditMode });
<div :class="$style.tabs">
<N8nTabs
size="small"
:model-value="currentOutputIndex"
:options="branches"
@update:model-value="onBranchChange"
@@ -2049,6 +2059,13 @@ defineExpose({ enterEditMode });
padding-left: var(--spacing-s);
padding-right: var(--spacing-s);
padding-bottom: var(--spacing-s);
.compact & {
padding-left: var(--spacing-2xs);
padding-right: var(--spacing-2xs);
padding-bottom: var(--spacing-2xs);
font-size: var(--font-size-2xs);
}
}
.tabs {

View File

@@ -39,32 +39,17 @@ const options = computed(() => {
:model-value="value"
:options="options"
data-test-id="ndv-run-data-display-mode"
:size="compact ? 'small' : 'medium'"
:size="compact ? 'small-medium' : 'medium'"
:square-buttons="compact"
@update:model-value="(selected) => emit('change', selected)"
>
<template v-if="compact" #option="option">
<N8nIcon v-if="option.value === 'table'" icon="table" size="small" :class="$style.icon" />
<N8nIcon v-else-if="option.value === 'json'" icon="json" size="small" :class="$style.icon" />
<N8nIcon
v-else-if="option.value === 'binary'"
icon="binary"
size="small"
:class="$style.icon"
/>
<N8nIcon
v-else-if="option.value === 'schema'"
icon="schema"
size="small"
:class="$style.icon"
/>
<N8nIcon v-else-if="option.value === 'html'" icon="html" size="small" :class="$style.icon" />
<N8nIcon v-if="option.value === 'table'" icon="table" size="small" />
<N8nIcon v-else-if="option.value === 'json'" icon="json" size="small" />
<N8nIcon v-else-if="option.value === 'binary'" icon="binary" size="small" />
<N8nIcon v-else-if="option.value === 'schema'" icon="schema" size="small" />
<N8nIcon v-else-if="option.value === 'html'" icon="html" size="small" />
<span v-else>{{ option.label }}</span>
</template>
</N8nRadioButtons>
</template>
<style lang="scss" module>
.icon {
padding-inline: var(--spacing-4xs);
}
</style>