refactor(editor): Update RunDataAi helpers to use connections instead of Workflow object (no-changelog) (#17630)

This commit is contained in:
Alex Grozav
2025-07-24 18:29:25 +03:00
committed by GitHub
parent 4b626e5282
commit 3f2e43e919
3 changed files with 76 additions and 32 deletions

View File

@@ -31,11 +31,20 @@ const selectedRun: Ref<IAiData[]> = ref([]);
const i18n = useI18n();
const aiData = computed<AIResult[]>(() =>
createAiData(props.node.name, props.workflow, workflowsStore.getWorkflowResultDataByNodeName),
createAiData(
props.node.name,
props.workflow.connectionsBySourceNode,
workflowsStore.getWorkflowResultDataByNodeName,
),
);
const executionTree = computed<TreeNode[]>(() =>
getTreeNodeData(props.node.name, props.workflow, aiData.value, props.runIndex),
getTreeNodeData(
props.node.name,
props.workflow.connectionsBySourceNode,
aiData.value,
props.runIndex,
),
);
function isTreeNodeSelected(node: TreeNode) {

View File

@@ -89,8 +89,12 @@ describe(getTreeNodeData, () => {
expect(
getTreeNodeData(
'A',
workflow,
createAiData('A', workflow, (name) => taskDataByNodeName[name] ?? null),
workflow.connectionsBySourceNode,
createAiData(
'A',
workflow.connectionsBySourceNode,
(name) => taskDataByNodeName[name] ?? null,
),
0,
),
).toEqual([
@@ -215,10 +219,14 @@ describe(getTreeNodeData, () => {
}),
],
};
const aiData = createAiData('A', workflow, (name) => taskDataByNodeName[name] ?? null);
const aiData = createAiData(
'A',
workflow.connectionsBySourceNode,
(name) => taskDataByNodeName[name] ?? null,
);
const treeRun0 = getTreeNodeData('A', workflow, aiData, 0);
const treeRun1 = getTreeNodeData('A', workflow, aiData, 1);
const treeRun0 = getTreeNodeData('A', workflow.connectionsBySourceNode, aiData, 0);
const treeRun1 = getTreeNodeData('A', workflow.connectionsBySourceNode, aiData, 1);
expect(treeRun0).toHaveLength(1);
expect(treeRun0[0].node).toBe('A');
@@ -293,13 +301,23 @@ describe(getTreeNodeData, () => {
const aiData = [sharedSubNodeData1, sharedSubNodeData2];
// Test for RootNode1 - should only show SharedSubNode with source RootNode1
const rootNode1Tree = getTreeNodeData('RootNode1', workflowWithSharedSubNode, aiData, 0);
const rootNode1Tree = getTreeNodeData(
'RootNode1',
workflowWithSharedSubNode.connectionsBySourceNode,
aiData,
0,
);
expect(rootNode1Tree[0].children.length).toBe(1);
expect(rootNode1Tree[0].children[0].node).toBe('SharedSubNode');
expect(rootNode1Tree[0].children[0].runIndex).toBe(0);
// Test for RootNode2 - should only show SharedSubNode with source RootNode2
const rootNode2Tree = getTreeNodeData('RootNode2', workflowWithSharedSubNode, aiData, 0);
const rootNode2Tree = getTreeNodeData(
'RootNode2',
workflowWithSharedSubNode.connectionsBySourceNode,
aiData,
0,
);
expect(rootNode2Tree[0].children.length).toBe(1);
expect(rootNode2Tree[0].children[0].node).toBe('SharedSubNode');
expect(rootNode2Tree[0].children[0].runIndex).toBe(1);
@@ -352,13 +370,13 @@ describe(getTreeNodeData, () => {
const aiData = [subNodeData1, subNodeData2];
// Test for run #1 of RootNode - should only show SubNode with source run index 0
const rootNode1Tree = getTreeNodeData('RootNode', workflow, aiData, 0);
const rootNode1Tree = getTreeNodeData('RootNode', workflow.connectionsBySourceNode, aiData, 0);
expect(rootNode1Tree[0].children.length).toBe(1);
expect(rootNode1Tree[0].children[0].node).toBe('SubNode');
expect(rootNode1Tree[0].children[0].runIndex).toBe(0);
// Test for run #2 of RootNode - should only show SubNode with source run index 1
const rootNode2Tree = getTreeNodeData('RootNode', workflow, aiData, 1);
const rootNode2Tree = getTreeNodeData('RootNode', workflow.connectionsBySourceNode, aiData, 1);
expect(rootNode2Tree[0].children.length).toBe(1);
expect(rootNode2Tree[0].children[0].node).toBe('SubNode');
expect(rootNode2Tree[0].children[0].runIndex).toBe(1);
@@ -395,7 +413,7 @@ describe(getTreeNodeData, () => {
const aiData = [subNodeData];
// Test for RootNode - should still show SubNode even without source info
const rootNodeTree = getTreeNodeData('RootNode', workflow, aiData, 0);
const rootNodeTree = getTreeNodeData('RootNode', workflow.connectionsBySourceNode, aiData, 0);
expect(rootNodeTree[0].children.length).toBe(1);
expect(rootNodeTree[0].children[0].node).toBe('SubNode');
expect(rootNodeTree[0].children[0].runIndex).toBe(0);
@@ -432,7 +450,7 @@ describe(getTreeNodeData, () => {
const aiData = [subNodeData];
// Test for RootNode - should still show SubNode even with empty source array
const rootNodeTree = getTreeNodeData('RootNode', workflow, aiData, 0);
const rootNodeTree = getTreeNodeData('RootNode', workflow.connectionsBySourceNode, aiData, 0);
expect(rootNodeTree[0].children.length).toBe(1);
expect(rootNodeTree[0].children[0].node).toBe('SubNode');
expect(rootNodeTree[0].children[0].runIndex).toBe(0);
@@ -468,7 +486,7 @@ describe(getTreeNodeData, () => {
// Create test AI data array
const aiData = [subNodeData];
const rootNodeTree = getTreeNodeData('RootNode', workflow, aiData, 0);
const rootNodeTree = getTreeNodeData('RootNode', workflow.connectionsBySourceNode, aiData, 0);
expect(rootNodeTree[0].children.length).toBe(1);
expect(rootNodeTree[0].children[0].node).toBe('SubNode');
@@ -564,7 +582,7 @@ describe(getTreeNodeData, () => {
const aiData = [sharedSubNodeData1, sharedSubNodeData2, deepSubNodeData1, deepSubNodeData2];
// Test filtering for RootNode1
const rootNode1Tree = getTreeNodeData('RootNode1', workflow, aiData, 0);
const rootNode1Tree = getTreeNodeData('RootNode1', workflow.connectionsBySourceNode, aiData, 0);
expect(rootNode1Tree[0].children.length).toBe(1);
expect(rootNode1Tree[0].children[0].node).toBe('SharedSubNode');
expect(rootNode1Tree[0].children[0].runIndex).toBe(0);
@@ -573,7 +591,7 @@ describe(getTreeNodeData, () => {
expect(rootNode1Tree[0].children[0].children[0].runIndex).toBe(0);
// Test filtering for RootNode2
const rootNode2Tree = getTreeNodeData('RootNode2', workflow, aiData, 0);
const rootNode2Tree = getTreeNodeData('RootNode2', workflow.connectionsBySourceNode, aiData, 0);
expect(rootNode2Tree[0].children.length).toBe(1);
expect(rootNode2Tree[0].children[0].node).toBe('SharedSubNode');
expect(rootNode2Tree[0].children[0].runIndex).toBe(1);

View File

@@ -1,16 +1,17 @@
import { type LlmTokenUsageData, type IAiDataContent } from '@/Interface';
import { addTokenUsageData, emptyTokenUsageData } from '@/utils/aiUtils';
import {
type INodeExecutionData,
type ITaskData,
type ITaskDataConnections,
type NodeConnectionType,
type Workflow,
import type {
IConnections,
INodeExecutionData,
ITaskData,
ITaskDataConnections,
NodeConnectionType,
} from 'n8n-workflow';
import { splitTextBySearch } from '@/utils/stringUtils';
import { escapeHtml } from 'xss';
import type MarkdownIt from 'markdown-it';
import { unescapeAll } from 'markdown-it/lib/common/utils';
import * as workflowUtils from 'n8n-workflow/common';
export interface AIResult {
node: string;
@@ -51,26 +52,28 @@ function createNode(
export function getTreeNodeData(
nodeName: string,
workflow: Workflow,
connectionsBySourceNode: IConnections,
aiData: AIResult[] | undefined,
runIndex: number,
): TreeNode[] {
return getTreeNodeDataRec(undefined, nodeName, 0, workflow, aiData, runIndex);
return getTreeNodeDataRec(undefined, nodeName, 0, connectionsBySourceNode, aiData, runIndex);
}
function getTreeNodeDataRec(
parent: TreeNode | undefined,
nodeName: string,
currentDepth: number,
workflow: Workflow,
connectionsBySourceNode: IConnections,
aiData: AIResult[] | undefined,
runIndex: number,
): TreeNode[] {
const connections = workflow.connectionsByDestinationNode[nodeName];
const connectionsByDestinationNode =
workflowUtils.mapConnectionsByDestination(connectionsBySourceNode);
const nodeConnections = connectionsByDestinationNode[nodeName];
const resultData =
aiData?.filter((data) => data.node === nodeName && runIndex === data.runIndex) ?? [];
if (!connections) {
if (!nodeConnections) {
return resultData.map((d) => createNode(parent, nodeName, currentDepth, d.runIndex, d));
}
@@ -88,14 +91,26 @@ function getTreeNodeDataRec(
});
// Get the first level of children
const connectedSubNodes = workflow.getParentNodes(nodeName, 'ALL_NON_MAIN', 1);
const connectedSubNodes = workflowUtils.getParentNodes(
connectionsByDestinationNode,
nodeName,
'ALL_NON_MAIN',
1,
);
const treeNode = createNode(parent, nodeName, currentDepth, runIndex);
// Only include sub-nodes which have data
const children = (filteredAiData ?? []).flatMap((data) =>
connectedSubNodes.includes(data.node)
? getTreeNodeDataRec(treeNode, data.node, currentDepth + 1, workflow, aiData, data.runIndex)
? getTreeNodeDataRec(
treeNode,
data.node,
currentDepth + 1,
connectionsBySourceNode,
aiData,
data.runIndex,
)
: [],
);
@@ -112,11 +127,13 @@ function getTreeNodeDataRec(
export function createAiData(
nodeName: string,
workflow: Workflow,
connectionsBySourceNode: IConnections,
getWorkflowResultDataByNodeName: (nodeName: string) => ITaskData[] | null,
): AIResult[] {
return workflow
.getParentNodes(nodeName, 'ALL_NON_MAIN')
const connectionsByDestinationNode =
workflowUtils.mapConnectionsByDestination(connectionsBySourceNode);
return workflowUtils
.getParentNodes(connectionsByDestinationNode, nodeName, 'ALL_NON_MAIN')
.flatMap((node) =>
(getWorkflowResultDataByNodeName(node) ?? []).map((task, index) => ({ node, task, index })),
)