mirror of
https://github.com/n8n-io/n8n.git
synced 2026-09-24 23:22:38 +08:00
fix(editor): Fix sub-nodes connection labels counters (#21549)
Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>
This commit is contained in:
@@ -221,6 +221,12 @@ export type CanvasNodeMoveEvent = { id: string; position: CanvasNode['position']
|
||||
export type ExecutionOutputMapData = {
|
||||
total: number;
|
||||
iterations: number;
|
||||
byTarget?: {
|
||||
[targetNodeId: string]: {
|
||||
total: number;
|
||||
iterations: number;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
export type ExecutionOutputMap = {
|
||||
|
||||
+483
-1
@@ -549,6 +549,7 @@ describe('useCanvasMapping', () => {
|
||||
0: {
|
||||
iterations: 1,
|
||||
total: 2,
|
||||
byTarget: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -729,6 +730,175 @@ describe('useCanvasMapping', () => {
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should populate byTarget field for non-main connections with per-target counts', () => {
|
||||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||||
const modelNode = createTestNode({ name: 'OpenAI Chat Model' });
|
||||
const agent1Node = createTestNode({ name: 'AI Agent 1' });
|
||||
const agent2Node = createTestNode({ name: 'AI Agent 2' });
|
||||
const nodes = [modelNode, agent1Node, agent2Node];
|
||||
const connections = {
|
||||
[modelNode.name]: {
|
||||
[NodeConnectionTypes.AiLanguageModel]: [
|
||||
[
|
||||
{ node: agent1Node.name, type: NodeConnectionTypes.AiLanguageModel, index: 0 },
|
||||
{ node: agent2Node.name, type: NodeConnectionTypes.AiLanguageModel, index: 0 },
|
||||
],
|
||||
],
|
||||
},
|
||||
};
|
||||
const workflowObject = createTestWorkflowObject({
|
||||
nodes,
|
||||
connections,
|
||||
});
|
||||
|
||||
// Model node has multiple executions from different sources
|
||||
workflowsStore.getWorkflowResultDataByNodeName.mockImplementation((nodeName: string) => {
|
||||
if (nodeName === modelNode.name) {
|
||||
return [
|
||||
{
|
||||
startTime: 0,
|
||||
executionTime: 0,
|
||||
executionIndex: 0,
|
||||
source: [{ previousNode: agent1Node.name }],
|
||||
executionStatus: 'success',
|
||||
data: {
|
||||
[NodeConnectionTypes.AiLanguageModel]: [[{ json: {} }, { json: {} }]],
|
||||
},
|
||||
},
|
||||
{
|
||||
startTime: 0,
|
||||
executionTime: 0,
|
||||
executionIndex: 1,
|
||||
source: [{ previousNode: agent2Node.name }],
|
||||
executionStatus: 'success',
|
||||
data: {
|
||||
[NodeConnectionTypes.AiLanguageModel]: [[{ json: {} }]],
|
||||
},
|
||||
},
|
||||
{
|
||||
startTime: 0,
|
||||
executionTime: 0,
|
||||
executionIndex: 2,
|
||||
source: [{ previousNode: agent1Node.name }],
|
||||
executionStatus: 'success',
|
||||
data: {
|
||||
[NodeConnectionTypes.AiLanguageModel]: [
|
||||
[{ json: {} }, { json: {} }, { json: {} }],
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
const { nodeExecutionRunDataOutputMapById } = useCanvasMapping({
|
||||
nodes: ref(nodes),
|
||||
connections: ref(connections),
|
||||
workflowObject: ref(workflowObject) as Ref<Workflow>,
|
||||
});
|
||||
|
||||
// Should have byTarget field for non-main connections
|
||||
const modelOutputData = nodeExecutionRunDataOutputMapById.value[modelNode.id];
|
||||
expect(modelOutputData).toBeDefined();
|
||||
expect(modelOutputData[NodeConnectionTypes.AiLanguageModel]).toBeDefined();
|
||||
expect(modelOutputData[NodeConnectionTypes.AiLanguageModel][0]).toBeDefined();
|
||||
|
||||
const outputData = modelOutputData[NodeConnectionTypes.AiLanguageModel][0];
|
||||
|
||||
// Check aggregated totals
|
||||
expect(outputData.iterations).toBe(3);
|
||||
expect(outputData.total).toBe(6);
|
||||
|
||||
// Check per-target tracking
|
||||
expect(outputData.byTarget).toBeDefined();
|
||||
assert(outputData.byTarget);
|
||||
|
||||
expect(outputData.byTarget[agent1Node.id]).toBeDefined();
|
||||
expect(outputData.byTarget[agent2Node.id]).toBeDefined();
|
||||
|
||||
// Agent 1 was called twice with 2 + 3 = 5 items total
|
||||
expect(outputData.byTarget[agent1Node.id].iterations).toBe(2);
|
||||
expect(outputData.byTarget[agent1Node.id].total).toBe(5);
|
||||
|
||||
// Agent 2 was called once with 1 item
|
||||
expect(outputData.byTarget[agent2Node.id].iterations).toBe(1);
|
||||
expect(outputData.byTarget[agent2Node.id].total).toBe(1);
|
||||
});
|
||||
|
||||
it('should count items inside response field when aggregating for non-main connections', () => {
|
||||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||||
const embeddingNode = createTestNode({ name: 'Embeddings OpenAI' });
|
||||
const vectorStoreNode = createTestNode({ name: 'Vector Store' });
|
||||
const nodes = [embeddingNode, vectorStoreNode];
|
||||
const connections = {
|
||||
[embeddingNode.name]: {
|
||||
[NodeConnectionTypes.AiEmbedding]: [
|
||||
[{ node: vectorStoreNode.name, type: NodeConnectionTypes.AiEmbedding, index: 0 }],
|
||||
],
|
||||
},
|
||||
};
|
||||
const workflowObject = createTestWorkflowObject({
|
||||
nodes,
|
||||
connections,
|
||||
});
|
||||
|
||||
// Embedding node returns data wrapped in response field
|
||||
workflowsStore.getWorkflowResultDataByNodeName.mockImplementation((nodeName: string) => {
|
||||
if (nodeName === embeddingNode.name) {
|
||||
return [
|
||||
{
|
||||
startTime: 0,
|
||||
executionTime: 0,
|
||||
executionIndex: 0,
|
||||
source: [{ previousNode: vectorStoreNode.name }],
|
||||
executionStatus: 'success',
|
||||
data: {
|
||||
[NodeConnectionTypes.AiEmbedding]: [
|
||||
[
|
||||
{
|
||||
json: {
|
||||
response: [
|
||||
{ embedding: [0.1, 0.2] },
|
||||
{ embedding: [0.3, 0.4] },
|
||||
{ embedding: [0.5, 0.6] },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
const { nodeExecutionRunDataOutputMapById } = useCanvasMapping({
|
||||
nodes: ref(nodes),
|
||||
connections: ref(connections),
|
||||
workflowObject: ref(workflowObject) as Ref<Workflow>,
|
||||
});
|
||||
|
||||
const embeddingOutputData = nodeExecutionRunDataOutputMapById.value[embeddingNode.id];
|
||||
expect(embeddingOutputData).toBeDefined();
|
||||
expect(embeddingOutputData[NodeConnectionTypes.AiEmbedding]).toBeDefined();
|
||||
expect(embeddingOutputData[NodeConnectionTypes.AiEmbedding][0]).toBeDefined();
|
||||
|
||||
const outputData = embeddingOutputData[NodeConnectionTypes.AiEmbedding][0];
|
||||
|
||||
// Should count the 3 items inside response, not just 1 wrapper
|
||||
expect(outputData.iterations).toBe(1);
|
||||
expect(outputData.total).toBe(3);
|
||||
|
||||
// Should also apply to per-target counts
|
||||
expect(outputData.byTarget).toBeDefined();
|
||||
assert(outputData.byTarget);
|
||||
|
||||
expect(outputData.byTarget[vectorStoreNode.id]).toBeDefined();
|
||||
expect(outputData.byTarget[vectorStoreNode.id].total).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('additionalNodePropertiesById', () => {
|
||||
@@ -2603,13 +2773,28 @@ describe('useCanvasMapping', () => {
|
||||
startTime: 0,
|
||||
executionTime: 0,
|
||||
executionIndex: 0,
|
||||
source: [],
|
||||
source: [{ previousNode: setNode.name }],
|
||||
data: {
|
||||
[NodeConnectionTypes.AiTool]: [[{ json: {} }, { json: {} }]],
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
// Add execution data for target node so connection shows as executed
|
||||
if (nodeName === setNode.name) {
|
||||
return [
|
||||
{
|
||||
startTime: 0,
|
||||
executionTime: 0,
|
||||
executionIndex: 0,
|
||||
source: [],
|
||||
executionStatus: 'success',
|
||||
data: {
|
||||
[NodeConnectionTypes.Main]: [[{ json: {} }]],
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
@@ -3173,6 +3358,21 @@ describe('useCanvasMapping', () => {
|
||||
},
|
||||
];
|
||||
}
|
||||
// Add execution data for target node so connection shows as executed
|
||||
if (nodeName === setNode.name) {
|
||||
return [
|
||||
{
|
||||
startTime: 0,
|
||||
executionTime: 0,
|
||||
executionIndex: 0,
|
||||
source: [{ previousNode: manualTriggerNode.name }],
|
||||
executionStatus: 'success',
|
||||
data: {
|
||||
[NodeConnectionTypes.Main]: [[{ json: {} }]],
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
@@ -3184,6 +3384,288 @@ describe('useCanvasMapping', () => {
|
||||
|
||||
expect(mappedConnections.value[0]?.data?.status).toEqual('success');
|
||||
});
|
||||
|
||||
it('should not mark non-main connections as executed when only source has run data', () => {
|
||||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||||
const embeddingNode = createTestNode({ name: 'Embeddings OpenAI' });
|
||||
const vectorStoreNode = createTestNode({ name: 'Vector Store' });
|
||||
const nodes = [embeddingNode, vectorStoreNode];
|
||||
const connections = {
|
||||
[embeddingNode.name]: {
|
||||
[NodeConnectionTypes.AiEmbedding]: [
|
||||
[{ node: vectorStoreNode.name, type: NodeConnectionTypes.AiEmbedding, index: 0 }],
|
||||
],
|
||||
},
|
||||
};
|
||||
const workflowObject = createTestWorkflowObject({
|
||||
nodes,
|
||||
connections,
|
||||
});
|
||||
|
||||
// Only source node has execution data, target does not
|
||||
workflowsStore.getWorkflowResultDataByNodeName.mockImplementation((nodeName: string) => {
|
||||
if (nodeName === embeddingNode.name) {
|
||||
return [
|
||||
{
|
||||
startTime: 0,
|
||||
executionTime: 0,
|
||||
executionIndex: 0,
|
||||
source: [],
|
||||
executionStatus: 'success',
|
||||
data: {
|
||||
[NodeConnectionTypes.AiEmbedding]: [[{ json: {} }]],
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
const { connections: mappedConnections } = useCanvasMapping({
|
||||
nodes: ref(nodes),
|
||||
connections: ref(connections),
|
||||
workflowObject: ref(workflowObject) as Ref<Workflow>,
|
||||
});
|
||||
|
||||
// Non-main connection should not be marked as success when target hasn't executed
|
||||
expect(mappedConnections.value[0]?.data?.status).toBeUndefined();
|
||||
expect(mappedConnections.value[0]?.label).toBe('');
|
||||
});
|
||||
|
||||
it('should mark non-main connections as executed when both source and target have run data', () => {
|
||||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||||
const embeddingNode = createTestNode({ name: 'Embeddings OpenAI' });
|
||||
const vectorStoreNode = createTestNode({ name: 'Vector Store' });
|
||||
const nodes = [embeddingNode, vectorStoreNode];
|
||||
const connections = {
|
||||
[embeddingNode.name]: {
|
||||
[NodeConnectionTypes.AiEmbedding]: [
|
||||
[{ node: vectorStoreNode.name, type: NodeConnectionTypes.AiEmbedding, index: 0 }],
|
||||
],
|
||||
},
|
||||
};
|
||||
const workflowObject = createTestWorkflowObject({
|
||||
nodes,
|
||||
connections,
|
||||
});
|
||||
|
||||
// Both source and target have execution data
|
||||
workflowsStore.getWorkflowResultDataByNodeName.mockImplementation((nodeName: string) => {
|
||||
if (nodeName === embeddingNode.name) {
|
||||
return [
|
||||
{
|
||||
startTime: 0,
|
||||
executionTime: 0,
|
||||
executionIndex: 0,
|
||||
source: [{ previousNode: vectorStoreNode.name }],
|
||||
executionStatus: 'success',
|
||||
data: {
|
||||
[NodeConnectionTypes.AiEmbedding]: [[{ json: {} }, { json: {} }]],
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
if (nodeName === vectorStoreNode.name) {
|
||||
return [
|
||||
{
|
||||
startTime: 0,
|
||||
executionTime: 0,
|
||||
executionIndex: 0,
|
||||
source: [],
|
||||
executionStatus: 'success',
|
||||
data: {
|
||||
[NodeConnectionTypes.Main]: [[{ json: {} }]],
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
const { connections: mappedConnections } = useCanvasMapping({
|
||||
nodes: ref(nodes),
|
||||
connections: ref(connections),
|
||||
workflowObject: ref(workflowObject) as Ref<Workflow>,
|
||||
});
|
||||
|
||||
// Non-main connection should be marked as success when both source and target have executed
|
||||
expect(mappedConnections.value[0]?.data?.status).toEqual('success');
|
||||
expect(mappedConnections.value[0]?.label).toBe('2 items');
|
||||
});
|
||||
|
||||
it('should show per-target counts when multiple agents share a model', () => {
|
||||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||||
const modelNode = createTestNode({ name: 'OpenAI Chat Model' });
|
||||
const agent1Node = createTestNode({ name: 'AI Agent 1' });
|
||||
const agent2Node = createTestNode({ name: 'AI Agent 2' });
|
||||
const nodes = [modelNode, agent1Node, agent2Node];
|
||||
const connections = {
|
||||
[modelNode.name]: {
|
||||
[NodeConnectionTypes.AiLanguageModel]: [
|
||||
[
|
||||
{ node: agent1Node.name, type: NodeConnectionTypes.AiLanguageModel, index: 0 },
|
||||
{ node: agent2Node.name, type: NodeConnectionTypes.AiLanguageModel, index: 0 },
|
||||
],
|
||||
],
|
||||
},
|
||||
};
|
||||
const workflowObject = createTestWorkflowObject({
|
||||
nodes,
|
||||
connections,
|
||||
});
|
||||
|
||||
// Model node has execution data with source tracking
|
||||
workflowsStore.getWorkflowResultDataByNodeName.mockImplementation((nodeName: string) => {
|
||||
if (nodeName === modelNode.name) {
|
||||
return [
|
||||
{
|
||||
startTime: 0,
|
||||
executionTime: 0,
|
||||
executionIndex: 0,
|
||||
source: [{ previousNode: agent1Node.name }],
|
||||
executionStatus: 'success',
|
||||
data: {
|
||||
[NodeConnectionTypes.AiLanguageModel]: [[{ json: {} }, { json: {} }]],
|
||||
},
|
||||
},
|
||||
{
|
||||
startTime: 0,
|
||||
executionTime: 0,
|
||||
executionIndex: 1,
|
||||
source: [{ previousNode: agent2Node.name }],
|
||||
executionStatus: 'success',
|
||||
data: {
|
||||
[NodeConnectionTypes.AiLanguageModel]: [[{ json: {} }]],
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
if (nodeName === agent1Node.name) {
|
||||
return [
|
||||
{
|
||||
startTime: 0,
|
||||
executionTime: 0,
|
||||
executionIndex: 0,
|
||||
source: [],
|
||||
executionStatus: 'success',
|
||||
data: {
|
||||
[NodeConnectionTypes.Main]: [[{ json: {} }]],
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
if (nodeName === agent2Node.name) {
|
||||
return [
|
||||
{
|
||||
startTime: 0,
|
||||
executionTime: 0,
|
||||
executionIndex: 0,
|
||||
source: [],
|
||||
executionStatus: 'success',
|
||||
data: {
|
||||
[NodeConnectionTypes.Main]: [[{ json: {} }]],
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
const { connections: mappedConnections } = useCanvasMapping({
|
||||
nodes: ref(nodes),
|
||||
connections: ref(connections),
|
||||
workflowObject: ref(workflowObject) as Ref<Workflow>,
|
||||
});
|
||||
|
||||
// Should have two connections from the model node
|
||||
expect(mappedConnections.value).toHaveLength(2);
|
||||
|
||||
// Find connections for each agent
|
||||
const agent1Connection = mappedConnections.value.find((c) => c.target === agent1Node.id);
|
||||
const agent2Connection = mappedConnections.value.find((c) => c.target === agent2Node.id);
|
||||
|
||||
// Each connection should show its specific count
|
||||
expect(agent1Connection?.label).toBe('2 items');
|
||||
expect(agent2Connection?.label).toBe('1 item');
|
||||
expect(agent1Connection?.data?.status).toEqual('success');
|
||||
expect(agent2Connection?.data?.status).toEqual('success');
|
||||
});
|
||||
|
||||
it('should count items inside response field for non-main connections', () => {
|
||||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||||
const embeddingNode = createTestNode({ name: 'Embeddings OpenAI' });
|
||||
const vectorStoreNode = createTestNode({ name: 'Vector Store' });
|
||||
const nodes = [embeddingNode, vectorStoreNode];
|
||||
const connections = {
|
||||
[embeddingNode.name]: {
|
||||
[NodeConnectionTypes.AiEmbedding]: [
|
||||
[{ node: vectorStoreNode.name, type: NodeConnectionTypes.AiEmbedding, index: 0 }],
|
||||
],
|
||||
},
|
||||
};
|
||||
const workflowObject = createTestWorkflowObject({
|
||||
nodes,
|
||||
connections,
|
||||
});
|
||||
|
||||
// Embedding node returns data with response array containing 6 items
|
||||
workflowsStore.getWorkflowResultDataByNodeName.mockImplementation((nodeName: string) => {
|
||||
if (nodeName === embeddingNode.name) {
|
||||
return [
|
||||
{
|
||||
startTime: 0,
|
||||
executionTime: 0,
|
||||
executionIndex: 0,
|
||||
source: [{ previousNode: vectorStoreNode.name }],
|
||||
executionStatus: 'success',
|
||||
data: {
|
||||
[NodeConnectionTypes.AiEmbedding]: [
|
||||
[
|
||||
{
|
||||
json: {
|
||||
response: [
|
||||
{ embedding: [0.1, 0.2] },
|
||||
{ embedding: [0.3, 0.4] },
|
||||
{ embedding: [0.5, 0.6] },
|
||||
{ embedding: [0.7, 0.8] },
|
||||
{ embedding: [0.9, 1.0] },
|
||||
{ embedding: [1.1, 1.2] },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
if (nodeName === vectorStoreNode.name) {
|
||||
return [
|
||||
{
|
||||
startTime: 0,
|
||||
executionTime: 0,
|
||||
executionIndex: 0,
|
||||
source: [],
|
||||
executionStatus: 'success',
|
||||
data: {
|
||||
[NodeConnectionTypes.Main]: [[{ json: {} }]],
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
const { connections: mappedConnections } = useCanvasMapping({
|
||||
nodes: ref(nodes),
|
||||
connections: ref(connections),
|
||||
workflowObject: ref(workflowObject) as Ref<Workflow>,
|
||||
});
|
||||
|
||||
// Should count the 6 items inside response, not just 1 wrapper object
|
||||
expect(mappedConnections.value[0]?.data?.status).toEqual('success');
|
||||
expect(mappedConnections.value[0]?.label).toBe('6 items');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+92
-10
@@ -364,6 +364,9 @@ export function useCanvasMapping({
|
||||
}, {}),
|
||||
);
|
||||
|
||||
// Create a map for O(1) node lookups by name
|
||||
const nodesByName = computed(() => new Map(nodes.value.map((n) => [n.name, n])));
|
||||
|
||||
const nodeExecutionRunDataOutputMapById = ref<Record<string, ExecutionOutputMap>>({});
|
||||
|
||||
throttledWatch(
|
||||
@@ -388,12 +391,61 @@ export function useCanvasMapping({
|
||||
|
||||
acc[nodeId][connectionType][outputIndex] = acc[nodeId][connectionType][
|
||||
outputIndex
|
||||
] ?? { ...outputData };
|
||||
] ?? {
|
||||
...outputData,
|
||||
...(connectionType !== NodeConnectionTypes.Main ? { byTarget: {} } : {}),
|
||||
};
|
||||
// For non-main connections, check if items are wrapped in a response field
|
||||
// (common for AI nodes like embeddings, tools, etc.)
|
||||
// Note: We check only the first item assuming uniform structure across all items
|
||||
let itemCount = connectionTypeOutputIndexData.length;
|
||||
if (
|
||||
connectionType !== NodeConnectionTypes.Main &&
|
||||
connectionTypeOutputIndexData.length > 0
|
||||
) {
|
||||
const firstItem = connectionTypeOutputIndexData[0];
|
||||
// AI nodes typically wrap all items uniformly in response field
|
||||
if (
|
||||
firstItem?.json &&
|
||||
typeof firstItem.json === 'object' &&
|
||||
'response' in firstItem.json &&
|
||||
Array.isArray(firstItem.json.response)
|
||||
) {
|
||||
// Use response array length for all items (assuming uniform structure)
|
||||
itemCount = firstItem.json.response.length;
|
||||
}
|
||||
}
|
||||
|
||||
if (runIteration.executionStatus !== 'canceled') {
|
||||
acc[nodeId][connectionType][outputIndex].iterations += 1;
|
||||
}
|
||||
acc[nodeId][connectionType][outputIndex].total +=
|
||||
connectionTypeOutputIndexData.length;
|
||||
acc[nodeId][connectionType][outputIndex].total += itemCount;
|
||||
|
||||
// For non-main connections, track per-target execution counts
|
||||
if (connectionType !== NodeConnectionTypes.Main) {
|
||||
const callingNodeName = runIteration.source?.[0]?.previousNode;
|
||||
if (callingNodeName) {
|
||||
const callingNode = nodesByName.value.get(callingNodeName);
|
||||
if (callingNode) {
|
||||
const targetId = callingNode.id;
|
||||
const outputEntry = acc[nodeId][connectionType][outputIndex];
|
||||
|
||||
if (outputEntry.byTarget) {
|
||||
if (!outputEntry.byTarget[targetId]) {
|
||||
outputEntry.byTarget[targetId] = {
|
||||
total: 0,
|
||||
iterations: 0,
|
||||
};
|
||||
}
|
||||
|
||||
if (runIteration.executionStatus !== 'canceled') {
|
||||
outputEntry.byTarget[targetId].iterations += 1;
|
||||
}
|
||||
outputEntry.byTarget[targetId].total += itemCount;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -711,7 +763,14 @@ export function useCanvasMapping({
|
||||
} else if (nodeHasIssuesById.value[connection.source]) {
|
||||
status = 'error';
|
||||
} else if (runDataTotal > 0 && lastSourceTask?.executionStatus !== 'canceled') {
|
||||
status = 'success';
|
||||
// For non-main connections (model, memory, tool, etc.), only mark as executed
|
||||
// if the target node also executed, since these are passive connections
|
||||
const isMainConnection = type === NodeConnectionTypes.Main;
|
||||
const targetNodeHasAnyExecution = nodeExecutionRunDataById.value[connection.target];
|
||||
|
||||
if (isMainConnection || targetNodeHasAnyExecution) {
|
||||
status = 'success';
|
||||
}
|
||||
}
|
||||
|
||||
const maxConnections = [
|
||||
@@ -754,13 +813,36 @@ export function useCanvasMapping({
|
||||
: '';
|
||||
} else if (nodeExecutionRunDataById.value[fromNode.id]) {
|
||||
const { type, index } = parseCanvasConnectionHandleString(connection.sourceHandle);
|
||||
const runDataTotal =
|
||||
nodeExecutionRunDataOutputMapById.value[fromNode.id]?.[type]?.[index]?.total ?? 0;
|
||||
const hasMultipleRunDataIterations =
|
||||
(nodeExecutionRunDataOutputMapById.value[fromNode.id]?.[type]?.[index]?.iterations ?? 1) >
|
||||
1;
|
||||
const outputData = nodeExecutionRunDataOutputMapById.value[fromNode.id]?.[type]?.[index];
|
||||
|
||||
return runDataTotal > 0
|
||||
// For non-main connections, use per-target data if available
|
||||
const isMainConnection = type === NodeConnectionTypes.Main;
|
||||
const targetHasExecutionData = nodeExecutionRunDataById.value[connection.target];
|
||||
|
||||
if (!isMainConnection && outputData?.byTarget) {
|
||||
// Look up the target node to get per-connection counts
|
||||
const targetNodeId = connection.target;
|
||||
const targetData = outputData.byTarget[targetNodeId];
|
||||
|
||||
if (targetData && targetData.total > 0 && targetHasExecutionData) {
|
||||
return i18n.baseText(
|
||||
targetData.iterations > 1 ? 'ndv.output.itemsTotal' : 'ndv.output.items',
|
||||
{
|
||||
adjustToNumber: targetData.total,
|
||||
interpolate: { count: String(targetData.total) },
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Target hasn't executed, show no label
|
||||
return '';
|
||||
}
|
||||
|
||||
// For main connections, use aggregate counts
|
||||
const runDataTotal = outputData?.total ?? 0;
|
||||
const hasMultipleRunDataIterations = (outputData?.iterations ?? 1) > 1;
|
||||
|
||||
return runDataTotal > 0 && (isMainConnection || targetHasExecutionData)
|
||||
? i18n.baseText(
|
||||
hasMultipleRunDataIterations ? 'ndv.output.itemsTotal' : 'ndv.output.items',
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user