fix(editor): Provide correct node output runData information in new canvas (no-changelog) (#10691)
This commit is contained in:
@@ -2,18 +2,18 @@ import type { Ref } from 'vue';
|
||||
import { ref } from 'vue';
|
||||
import { NodeConnectionType } from 'n8n-workflow';
|
||||
import type { Workflow } from 'n8n-workflow';
|
||||
import { createPinia, setActivePinia } from 'pinia';
|
||||
import { setActivePinia } from 'pinia';
|
||||
|
||||
import { useCanvasMapping } from '@/composables/useCanvasMapping';
|
||||
import type { INodeUi } from '@/Interface';
|
||||
import {
|
||||
createTestNode,
|
||||
createTestWorkflowObject,
|
||||
mockNode,
|
||||
mockNodes,
|
||||
mockNodeTypeDescription,
|
||||
} from '@/__tests__/mocks';
|
||||
import { MANUAL_TRIGGER_NODE_TYPE, SET_NODE_TYPE, STICKY_NODE_TYPE } from '@/constants';
|
||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||
import { MANUAL_TRIGGER_NODE_TYPE, SET_NODE_TYPE, STICKY_NODE_TYPE, STORES } from '@/constants';
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import {
|
||||
createCanvasConnectionHandleString,
|
||||
@@ -21,19 +21,32 @@ import {
|
||||
} from '@/utils/canvasUtilsV2';
|
||||
import { CanvasConnectionMode, CanvasNodeRenderType } from '@/types';
|
||||
import { MarkerType } from '@vue-flow/core';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import { mockedStore } from '@/__tests__/utils';
|
||||
|
||||
beforeEach(() => {
|
||||
const pinia = createPinia();
|
||||
const pinia = createTestingPinia({
|
||||
initialState: {
|
||||
[STORES.WORKFLOWS]: {
|
||||
workflowExecutionData: {},
|
||||
},
|
||||
[STORES.NODE_TYPES]: {
|
||||
nodeTypes: {
|
||||
[MANUAL_TRIGGER_NODE_TYPE]: {
|
||||
1: mockNodeTypeDescription({
|
||||
name: MANUAL_TRIGGER_NODE_TYPE,
|
||||
}),
|
||||
},
|
||||
[SET_NODE_TYPE]: {
|
||||
1: mockNodeTypeDescription({
|
||||
name: SET_NODE_TYPE,
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
setActivePinia(pinia);
|
||||
|
||||
useNodeTypesStore().setNodeTypes([
|
||||
mockNodeTypeDescription({
|
||||
name: MANUAL_TRIGGER_NODE_TYPE,
|
||||
}),
|
||||
mockNodeTypeDescription({
|
||||
name: SET_NODE_TYPE,
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -61,6 +74,7 @@ describe('useCanvasMapping', () => {
|
||||
|
||||
describe('nodes', () => {
|
||||
it('should map nodes to canvas nodes', () => {
|
||||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||||
const manualTriggerNode = mockNode({
|
||||
name: 'Manual Trigger',
|
||||
type: MANUAL_TRIGGER_NODE_TYPE,
|
||||
@@ -79,6 +93,8 @@ describe('useCanvasMapping', () => {
|
||||
workflowObject: ref(workflowObject) as Ref<Workflow>,
|
||||
});
|
||||
|
||||
workflowsStore.isNodeExecuting.mockReturnValue(false);
|
||||
|
||||
expect(mappedNodes.value).toEqual([
|
||||
{
|
||||
id: manualTriggerNode.id,
|
||||
@@ -106,7 +122,8 @@ describe('useCanvasMapping', () => {
|
||||
visible: false,
|
||||
},
|
||||
runData: {
|
||||
count: 0,
|
||||
iterations: 0,
|
||||
outputMap: {},
|
||||
visible: false,
|
||||
},
|
||||
inputs: [
|
||||
@@ -169,6 +186,7 @@ describe('useCanvasMapping', () => {
|
||||
});
|
||||
|
||||
it('should handle execution state', () => {
|
||||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||||
const manualTriggerNode = mockNode({
|
||||
name: 'Manual Trigger',
|
||||
type: MANUAL_TRIGGER_NODE_TYPE,
|
||||
@@ -181,7 +199,7 @@ describe('useCanvasMapping', () => {
|
||||
connections,
|
||||
});
|
||||
|
||||
useWorkflowsStore().addExecutingNode(manualTriggerNode.name);
|
||||
workflowsStore.isNodeExecuting.mockReturnValue(true);
|
||||
|
||||
const { nodes: mappedNodes } = useCanvasMapping({
|
||||
nodes: ref(nodes),
|
||||
@@ -336,6 +354,195 @@ describe('useCanvasMapping', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('runData', () => {
|
||||
describe('nodeExecutionRunDataOutputMapById', () => {
|
||||
it('should return an empty object when there is no run data', () => {
|
||||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||||
const nodes: INodeUi[] = [];
|
||||
const connections = {};
|
||||
const workflowObject = createTestWorkflowObject({
|
||||
nodes,
|
||||
connections,
|
||||
});
|
||||
|
||||
workflowsStore.getWorkflowResultDataByNodeName.mockReturnValue(null);
|
||||
|
||||
const { nodeExecutionRunDataOutputMapById } = useCanvasMapping({
|
||||
nodes: ref(nodes),
|
||||
connections: ref(connections),
|
||||
workflowObject: ref(workflowObject) as Ref<Workflow>,
|
||||
});
|
||||
|
||||
expect(nodeExecutionRunDataOutputMapById.value).toEqual({});
|
||||
});
|
||||
|
||||
it('should calculate iterations and total correctly for single node', () => {
|
||||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||||
const nodes = [createTestNode({ name: 'Node 1' })];
|
||||
const connections = {};
|
||||
const workflowObject = createTestWorkflowObject({
|
||||
nodes,
|
||||
connections,
|
||||
});
|
||||
|
||||
workflowsStore.getWorkflowResultDataByNodeName.mockReturnValue([
|
||||
{
|
||||
startTime: 0,
|
||||
executionTime: 0,
|
||||
source: [],
|
||||
data: {
|
||||
[NodeConnectionType.Main]: [[{ json: {} }, { json: {} }]],
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const { nodeExecutionRunDataOutputMapById } = useCanvasMapping({
|
||||
nodes: ref(nodes),
|
||||
connections: ref(connections),
|
||||
workflowObject: ref(workflowObject) as Ref<Workflow>,
|
||||
});
|
||||
|
||||
expect(nodeExecutionRunDataOutputMapById.value).toEqual({
|
||||
[nodes[0].id]: {
|
||||
[NodeConnectionType.Main]: {
|
||||
0: {
|
||||
iterations: 1,
|
||||
total: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle multiple nodes with different connection types', () => {
|
||||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||||
const nodes = [
|
||||
createTestNode({ id: 'node1', name: 'Node 1' }),
|
||||
createTestNode({ id: 'node2', name: 'Node 2' }),
|
||||
];
|
||||
const connections = {};
|
||||
const workflowObject = createTestWorkflowObject({
|
||||
nodes,
|
||||
connections,
|
||||
});
|
||||
|
||||
workflowsStore.getWorkflowResultDataByNodeName.mockImplementation((nodeName: string) => {
|
||||
if (nodeName === 'Node 1') {
|
||||
return [
|
||||
{
|
||||
startTime: 0,
|
||||
executionTime: 0,
|
||||
source: [],
|
||||
data: {
|
||||
[NodeConnectionType.Main]: [[{ json: {} }]],
|
||||
[NodeConnectionType.AiAgent]: [[{ json: {} }, { json: {} }]],
|
||||
},
|
||||
},
|
||||
];
|
||||
} else if (nodeName === 'Node 2') {
|
||||
return [
|
||||
{
|
||||
startTime: 0,
|
||||
executionTime: 0,
|
||||
source: [],
|
||||
data: {
|
||||
[NodeConnectionType.Main]: [[{ json: {} }, { json: {} }, { json: {} }]],
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
const { nodeExecutionRunDataOutputMapById } = useCanvasMapping({
|
||||
nodes: ref(nodes),
|
||||
connections: ref(connections),
|
||||
workflowObject: ref(workflowObject) as Ref<Workflow>,
|
||||
});
|
||||
|
||||
expect(nodeExecutionRunDataOutputMapById.value).toEqual({
|
||||
node1: {
|
||||
[NodeConnectionType.Main]: {
|
||||
0: {
|
||||
iterations: 1,
|
||||
total: 1,
|
||||
},
|
||||
},
|
||||
[NodeConnectionType.AiAgent]: {
|
||||
0: {
|
||||
iterations: 1,
|
||||
total: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
node2: {
|
||||
[NodeConnectionType.Main]: {
|
||||
0: {
|
||||
iterations: 1,
|
||||
total: 3,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('handles multiple iterations correctly', () => {
|
||||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||||
const nodes = [createTestNode({ name: 'Node 1' })];
|
||||
const connections = {};
|
||||
const workflowObject = createTestWorkflowObject({
|
||||
nodes,
|
||||
connections,
|
||||
});
|
||||
|
||||
workflowsStore.getWorkflowResultDataByNodeName.mockReturnValue([
|
||||
{
|
||||
startTime: 0,
|
||||
executionTime: 0,
|
||||
source: [],
|
||||
data: {
|
||||
[NodeConnectionType.Main]: [[{ json: {} }]],
|
||||
},
|
||||
},
|
||||
{
|
||||
startTime: 0,
|
||||
executionTime: 0,
|
||||
source: [],
|
||||
data: {
|
||||
[NodeConnectionType.Main]: [[{ json: {} }, { json: {} }, { json: {} }]],
|
||||
},
|
||||
},
|
||||
{
|
||||
startTime: 0,
|
||||
executionTime: 0,
|
||||
source: [],
|
||||
data: {
|
||||
[NodeConnectionType.Main]: [[{ json: {} }, { json: {} }]],
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const { nodeExecutionRunDataOutputMapById } = useCanvasMapping({
|
||||
nodes: ref(nodes),
|
||||
connections: ref(connections),
|
||||
workflowObject: ref(workflowObject) as Ref<Workflow>,
|
||||
});
|
||||
|
||||
expect(nodeExecutionRunDataOutputMapById.value).toEqual({
|
||||
[nodes[0].id]: {
|
||||
[NodeConnectionType.Main]: {
|
||||
0: {
|
||||
iterations: 3,
|
||||
total: 6,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('connections', () => {
|
||||
|
||||
@@ -18,11 +18,13 @@ import type {
|
||||
CanvasNodeDefaultRender,
|
||||
CanvasNodeDefaultRenderLabelSize,
|
||||
CanvasNodeStickyNoteRender,
|
||||
ExecutionOutputMap,
|
||||
} from '@/types';
|
||||
import { CanvasConnectionMode, CanvasNodeRenderType } from '@/types';
|
||||
import {
|
||||
mapLegacyConnectionsToCanvasConnections,
|
||||
mapLegacyEndpointsToCanvasConnectionPort,
|
||||
parseCanvasConnectionHandleString,
|
||||
} from '@/utils/canvasUtilsV2';
|
||||
import type {
|
||||
ExecutionStatus,
|
||||
@@ -241,6 +243,39 @@ export function useCanvasMapping({
|
||||
}, {}),
|
||||
);
|
||||
|
||||
const nodeExecutionRunDataOutputMapById = computed(() =>
|
||||
Object.keys(nodeExecutionRunDataById.value).reduce<Record<string, ExecutionOutputMap>>(
|
||||
(acc, nodeId) => {
|
||||
acc[nodeId] = {};
|
||||
|
||||
const outputData = { iterations: 0, total: 0 };
|
||||
for (const runIteration of nodeExecutionRunDataById.value[nodeId] ?? []) {
|
||||
const data = runIteration.data ?? {};
|
||||
|
||||
for (const connectionType of Object.keys(data)) {
|
||||
const connectionTypeData = data[connectionType] ?? {};
|
||||
acc[nodeId][connectionType] = acc[nodeId][connectionType] ?? {};
|
||||
|
||||
for (const outputIndex of Object.keys(connectionTypeData)) {
|
||||
const parsedOutputIndex = parseInt(outputIndex, 10);
|
||||
const connectionTypeOutputIndexData = connectionTypeData[parsedOutputIndex] ?? [];
|
||||
|
||||
acc[nodeId][connectionType][outputIndex] = acc[nodeId][connectionType][
|
||||
outputIndex
|
||||
] ?? { ...outputData };
|
||||
acc[nodeId][connectionType][outputIndex].iterations += 1;
|
||||
acc[nodeId][connectionType][outputIndex].total +=
|
||||
connectionTypeOutputIndexData.length;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
),
|
||||
);
|
||||
|
||||
const nodeIssuesById = computed(() =>
|
||||
nodes.value.reduce<Record<string, string[]>>((acc, node) => {
|
||||
const issues: string[] = [];
|
||||
@@ -359,7 +394,8 @@ export function useCanvasMapping({
|
||||
running: nodeExecutionRunningById.value[node.id],
|
||||
},
|
||||
runData: {
|
||||
count: nodeExecutionRunDataById.value[node.id]?.length ?? 0,
|
||||
outputMap: nodeExecutionRunDataOutputMapById.value[node.id],
|
||||
iterations: nodeExecutionRunDataById.value[node.id]?.length ?? 0,
|
||||
visible: !!nodeExecutionRunDataById.value[node.id],
|
||||
},
|
||||
render: renderTypeByNodeId.value[node.id] ?? { type: 'default', options: {} },
|
||||
@@ -426,7 +462,6 @@ export function useCanvasMapping({
|
||||
|
||||
function getConnectionLabel(connection: CanvasConnection): string {
|
||||
const fromNode = nodes.value.find((node) => node.name === connection.data?.fromNodeName);
|
||||
|
||||
if (!fromNode) {
|
||||
return '';
|
||||
}
|
||||
@@ -438,10 +473,13 @@ export function useCanvasMapping({
|
||||
interpolate: { count: String(pinnedDataCount) },
|
||||
});
|
||||
} else if (nodeExecutionRunDataById.value[fromNode.id]) {
|
||||
const runDataCount = nodeExecutionRunDataById.value[fromNode.id]?.length ?? 0;
|
||||
const { type, index } = parseCanvasConnectionHandleString(connection.sourceHandle);
|
||||
const runDataTotal =
|
||||
nodeExecutionRunDataOutputMapById.value[fromNode.id]?.[type]?.[index]?.total ?? 0;
|
||||
|
||||
return i18n.baseText('ndv.output.items', {
|
||||
adjustToNumber: runDataCount,
|
||||
interpolate: { count: String(runDataCount) },
|
||||
adjustToNumber: runDataTotal,
|
||||
interpolate: { count: String(runDataTotal) },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -449,6 +487,7 @@ export function useCanvasMapping({
|
||||
}
|
||||
|
||||
return {
|
||||
nodeExecutionRunDataOutputMapById,
|
||||
connections: mappedConnections,
|
||||
nodes: mappedNodes,
|
||||
};
|
||||
|
||||
@@ -27,7 +27,8 @@ describe('useCanvasNode', () => {
|
||||
expect(result.isSelected.value).toBeUndefined();
|
||||
expect(result.pinnedDataCount.value).toBe(0);
|
||||
expect(result.hasPinnedData.value).toBe(false);
|
||||
expect(result.runDataCount.value).toBe(0);
|
||||
expect(result.runDataOutputMap.value).toEqual({});
|
||||
expect(result.runDataIterations.value).toBe(0);
|
||||
expect(result.hasRunData.value).toBe(false);
|
||||
expect(result.issues.value).toEqual([]);
|
||||
expect(result.hasIssues.value).toBe(false);
|
||||
@@ -54,7 +55,7 @@ describe('useCanvasNode', () => {
|
||||
},
|
||||
issues: { items: ['issue1'], visible: true },
|
||||
execution: { status: 'running', waiting: 'waiting', running: true },
|
||||
runData: { count: 1, visible: true },
|
||||
runData: { outputMap: {}, iterations: 1, visible: true },
|
||||
pinnedData: { count: 1, visible: true },
|
||||
render: {
|
||||
type: CanvasNodeRenderType.Default,
|
||||
@@ -86,7 +87,8 @@ describe('useCanvasNode', () => {
|
||||
expect(result.isSelected.value).toBe(true);
|
||||
expect(result.pinnedDataCount.value).toBe(1);
|
||||
expect(result.hasPinnedData.value).toBe(true);
|
||||
expect(result.runDataCount.value).toBe(1);
|
||||
expect(result.runDataOutputMap.value).toEqual({});
|
||||
expect(result.runDataIterations.value).toBe(1);
|
||||
expect(result.hasRunData.value).toBe(true);
|
||||
expect(result.issues.value).toEqual(['issue1']);
|
||||
expect(result.hasIssues.value).toBe(true);
|
||||
|
||||
@@ -10,9 +10,10 @@ import { CanvasNodeRenderType, CanvasConnectionMode } from '@/types';
|
||||
|
||||
export function useCanvasNode() {
|
||||
const node = inject(CanvasNodeKey);
|
||||
const data = computed<CanvasNodeData>(
|
||||
const data = computed(
|
||||
() =>
|
||||
node?.data.value ?? {
|
||||
node?.data.value ??
|
||||
({
|
||||
id: '',
|
||||
name: '',
|
||||
subtitle: '',
|
||||
@@ -27,12 +28,12 @@ export function useCanvasNode() {
|
||||
execution: {
|
||||
running: false,
|
||||
},
|
||||
runData: { count: 0, visible: false },
|
||||
runData: { iterations: 0, outputMap: {}, visible: false },
|
||||
render: {
|
||||
type: CanvasNodeRenderType.Default,
|
||||
options: {},
|
||||
},
|
||||
},
|
||||
} satisfies CanvasNodeData),
|
||||
);
|
||||
|
||||
const id = computed(() => node?.id.value ?? '');
|
||||
@@ -58,7 +59,8 @@ export function useCanvasNode() {
|
||||
const executionWaiting = computed(() => data.value.execution.waiting);
|
||||
const executionRunning = computed(() => data.value.execution.running);
|
||||
|
||||
const runDataCount = computed(() => data.value.runData.count);
|
||||
const runDataOutputMap = computed(() => data.value.runData.outputMap);
|
||||
const runDataIterations = computed(() => data.value.runData.iterations);
|
||||
const hasRunData = computed(() => data.value.runData.visible);
|
||||
|
||||
const render = computed(() => data.value.render);
|
||||
@@ -79,7 +81,8 @@ export function useCanvasNode() {
|
||||
isSelected,
|
||||
pinnedDataCount,
|
||||
hasPinnedData,
|
||||
runDataCount,
|
||||
runDataIterations,
|
||||
runDataOutputMap,
|
||||
hasRunData,
|
||||
issues,
|
||||
hasIssues,
|
||||
|
||||
@@ -14,9 +14,11 @@ export function useCanvasNodeHandle() {
|
||||
const label = computed(() => handle?.label.value ?? '');
|
||||
const isConnected = computed(() => handle?.isConnected.value ?? false);
|
||||
const isConnecting = computed(() => handle?.isConnecting.value ?? false);
|
||||
const isReadOnly = computed(() => handle?.isReadOnly.value);
|
||||
const type = computed(() => handle?.type.value ?? NodeConnectionType.Main);
|
||||
const mode = computed(() => handle?.mode.value ?? CanvasConnectionMode.Input);
|
||||
const isReadOnly = computed(() => handle?.isReadOnly.value);
|
||||
const index = computed(() => handle?.index.value ?? 0);
|
||||
const runData = computed(() => handle?.runData.value);
|
||||
|
||||
return {
|
||||
label,
|
||||
@@ -25,5 +27,7 @@ export function useCanvasNodeHandle() {
|
||||
isReadOnly,
|
||||
type,
|
||||
mode,
|
||||
index,
|
||||
runData,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user