mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-20 11:22:15 +00:00
fix(editor): Handle Loop node execution data preview correctly when inserting a node (#15351)
This commit is contained in:
@@ -1,4 +1,9 @@
|
||||
import type { IWorkflowData, IWorkflowDataUpdate, IWorkflowDb } from '@/Interface';
|
||||
import type {
|
||||
IExecutionResponse,
|
||||
IWorkflowData,
|
||||
IWorkflowDataUpdate,
|
||||
IWorkflowDb,
|
||||
} from '@/Interface';
|
||||
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
|
||||
import router from '@/router';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
@@ -10,6 +15,7 @@ import { useUIStore } from '@/stores/ui.store';
|
||||
import { createTestWorkflow } from '@/__tests__/mocks';
|
||||
import { WEBHOOK_NODE_TYPE, type AssignmentCollectionValue } from 'n8n-workflow';
|
||||
import * as apiWebhooks from '../api/webhooks';
|
||||
import { mockedStore } from '@/__tests__/utils';
|
||||
|
||||
const getDuplicateTestWorkflow = (): IWorkflowDataUpdate => ({
|
||||
name: 'Duplicate webhook test',
|
||||
@@ -58,14 +64,14 @@ const getDuplicateTestWorkflow = (): IWorkflowDataUpdate => ({
|
||||
});
|
||||
|
||||
describe('useWorkflowHelpers', () => {
|
||||
let workflowsStore: ReturnType<typeof useWorkflowsStore>;
|
||||
let workflowsStore: ReturnType<typeof mockedStore<typeof useWorkflowsStore>>;
|
||||
let workflowsEEStore: ReturnType<typeof useWorkflowsEEStore>;
|
||||
let tagsStore: ReturnType<typeof useTagsStore>;
|
||||
let uiStore: ReturnType<typeof useUIStore>;
|
||||
|
||||
beforeAll(() => {
|
||||
setActivePinia(createTestingPinia());
|
||||
workflowsStore = useWorkflowsStore();
|
||||
workflowsStore = mockedStore(useWorkflowsStore);
|
||||
workflowsEEStore = useWorkflowsEEStore();
|
||||
tagsStore = useTagsStore();
|
||||
uiStore = useUIStore();
|
||||
@@ -454,4 +460,374 @@ describe('useWorkflowHelpers', () => {
|
||||
expect(await workflowHelpers.checkConflictingWebhooks('12345')).toEqual(null);
|
||||
});
|
||||
});
|
||||
describe('executeData', () => {
|
||||
it('should return empty execute data if no parent nodes', () => {
|
||||
const { executeData } = useWorkflowHelpers({ router });
|
||||
|
||||
const parentNodes: string[] = [];
|
||||
const currentNode = 'Set';
|
||||
const inputName = 'main';
|
||||
const runIndex = 0;
|
||||
|
||||
const result = executeData(parentNodes, currentNode, inputName, runIndex);
|
||||
|
||||
expect(result).toEqual({
|
||||
node: {},
|
||||
data: {},
|
||||
source: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return the correct execution data with one parent node', () => {
|
||||
const { executeData } = useWorkflowHelpers({ router });
|
||||
|
||||
const parentNodes = ['Start'];
|
||||
const currentNode = 'Set';
|
||||
const inputName = 'main';
|
||||
const runIndex = 0;
|
||||
const jsonData = {
|
||||
name: 'Test',
|
||||
};
|
||||
workflowsStore.getCurrentWorkflow.mockReturnValue({
|
||||
connectionsByDestinationNode: {
|
||||
Set: {
|
||||
main: [
|
||||
[
|
||||
{ node: 'Start', index: 0, type: 'main' },
|
||||
{ node: 'Set', index: 0, type: 'main' },
|
||||
],
|
||||
],
|
||||
},
|
||||
},
|
||||
} as never);
|
||||
|
||||
workflowsStore.workflowExecutionData = {
|
||||
data: {
|
||||
resultData: {
|
||||
runData: {
|
||||
[parentNodes[0]]: [
|
||||
{
|
||||
startTime: 0,
|
||||
executionTime: 0,
|
||||
data: {
|
||||
main: [
|
||||
{
|
||||
json: jsonData,
|
||||
index: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
source: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as IExecutionResponse;
|
||||
|
||||
const result = executeData(parentNodes, currentNode, inputName, runIndex);
|
||||
|
||||
expect(result).toEqual({
|
||||
node: {},
|
||||
data: {
|
||||
main: [
|
||||
{
|
||||
index: 0,
|
||||
json: jsonData,
|
||||
},
|
||||
],
|
||||
},
|
||||
source: {
|
||||
main: [
|
||||
{
|
||||
previousNode: parentNodes[0],
|
||||
previousNodeOutput: 0,
|
||||
previousNodeRun: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should return the correct execution data with multiple parent nodes, only one with execution data', () => {
|
||||
const { executeData } = useWorkflowHelpers({ router });
|
||||
|
||||
const parentNodes = ['Parent A', 'Parent B'];
|
||||
const currentNode = 'Set';
|
||||
const inputName = 'main';
|
||||
const runIndex = 0;
|
||||
const jsonData = {
|
||||
name: 'Test',
|
||||
};
|
||||
workflowsStore.getCurrentWorkflow.mockReturnValue({
|
||||
connectionsByDestinationNode: {
|
||||
Set: {
|
||||
main: [
|
||||
[
|
||||
{ node: 'Start', index: 0, type: 'main' },
|
||||
{ node: 'Set', index: 0, type: 'main' },
|
||||
],
|
||||
],
|
||||
},
|
||||
},
|
||||
} as never);
|
||||
|
||||
workflowsStore.workflowExecutionData = {
|
||||
data: {
|
||||
resultData: {
|
||||
runData: {
|
||||
[parentNodes[1]]: [
|
||||
{
|
||||
startTime: 0,
|
||||
executionTime: 0,
|
||||
data: {
|
||||
main: [
|
||||
{
|
||||
json: jsonData,
|
||||
index: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
source: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as IExecutionResponse;
|
||||
|
||||
const result = executeData(parentNodes, currentNode, inputName, runIndex);
|
||||
|
||||
expect(result).toEqual({
|
||||
node: {},
|
||||
data: {
|
||||
main: [
|
||||
{
|
||||
index: 0,
|
||||
json: jsonData,
|
||||
},
|
||||
],
|
||||
},
|
||||
source: {
|
||||
main: [
|
||||
{
|
||||
previousNode: parentNodes[1],
|
||||
previousNodeOutput: undefined,
|
||||
previousNodeRun: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should return the correct execution data with multiple parent nodes, all with execution data', () => {
|
||||
const { executeData } = useWorkflowHelpers({ router });
|
||||
|
||||
const parentNodes = ['Parent A', 'Parent B'];
|
||||
const currentNode = 'Set';
|
||||
const inputName = 'main';
|
||||
const runIndex = 0;
|
||||
|
||||
const jsonDataA = {
|
||||
name: 'Test A',
|
||||
};
|
||||
|
||||
const jsonDataB = {
|
||||
name: 'Test B',
|
||||
};
|
||||
|
||||
workflowsStore.getCurrentWorkflow.mockReturnValue({
|
||||
connectionsByDestinationNode: {
|
||||
Set: {
|
||||
main: [
|
||||
[
|
||||
{ node: 'Parent A', index: 0, type: 'main' },
|
||||
{ node: 'Set', index: 0, type: 'main' },
|
||||
],
|
||||
[
|
||||
{ node: 'Parent B', index: 0, type: 'main' },
|
||||
{ node: 'Set', index: 0, type: 'main' },
|
||||
],
|
||||
],
|
||||
},
|
||||
},
|
||||
} as never);
|
||||
|
||||
workflowsStore.workflowExecutionData = {
|
||||
data: {
|
||||
resultData: {
|
||||
runData: {
|
||||
[parentNodes[0]]: [
|
||||
{
|
||||
startTime: 0,
|
||||
executionTime: 0,
|
||||
data: {
|
||||
main: [
|
||||
{
|
||||
json: jsonDataA,
|
||||
index: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
source: [],
|
||||
},
|
||||
],
|
||||
[parentNodes[1]]: [
|
||||
{
|
||||
startTime: 0,
|
||||
executionTime: 0,
|
||||
data: {
|
||||
main: [
|
||||
{
|
||||
json: jsonDataB,
|
||||
index: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
source: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as IExecutionResponse;
|
||||
|
||||
const result = executeData(parentNodes, currentNode, inputName, runIndex);
|
||||
|
||||
expect(result).toEqual({
|
||||
node: {},
|
||||
data: {
|
||||
main: [
|
||||
{
|
||||
index: 0,
|
||||
json: jsonDataA,
|
||||
},
|
||||
],
|
||||
},
|
||||
source: {
|
||||
main: [
|
||||
{
|
||||
previousNode: parentNodes[0],
|
||||
previousNodeOutput: 0,
|
||||
previousNodeRun: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should return data from pinnedWorkflowData if available', () => {
|
||||
const { executeData } = useWorkflowHelpers({ router });
|
||||
|
||||
const parentNodes = ['ParentNode'];
|
||||
const currentNode = 'CurrentNode';
|
||||
const inputName = 'main';
|
||||
const runIndex = 0;
|
||||
|
||||
workflowsStore.pinnedWorkflowData = {
|
||||
ParentNode: [{ json: { key: 'value' } }],
|
||||
};
|
||||
workflowsStore.shouldReplaceInputDataWithPinData = true;
|
||||
|
||||
const result = executeData(parentNodes, currentNode, inputName, runIndex);
|
||||
|
||||
expect(result.data).toEqual({ main: [[{ json: { key: 'value' } }]] });
|
||||
expect(result.source).toEqual({ main: [{ previousNode: 'ParentNode' }] });
|
||||
});
|
||||
|
||||
it('should return data from getWorkflowRunData if pinnedWorkflowData is not available', () => {
|
||||
const { executeData } = useWorkflowHelpers({ router });
|
||||
|
||||
const parentNodes = ['ParentNode'];
|
||||
const currentNode = 'CurrentNode';
|
||||
const inputName = 'main';
|
||||
const runIndex = 0;
|
||||
|
||||
workflowsStore.pinnedWorkflowData = undefined;
|
||||
workflowsStore.shouldReplaceInputDataWithPinData = false;
|
||||
workflowsStore.getWorkflowRunData = {
|
||||
ParentNode: [
|
||||
{
|
||||
data: { main: [[{ json: { key: 'valueFromRunData' } }]] },
|
||||
} as never,
|
||||
],
|
||||
};
|
||||
workflowsStore.getCurrentWorkflow.mockReturnValue({
|
||||
connectionsByDestinationNode: {
|
||||
CurrentNode: {
|
||||
main: [
|
||||
[
|
||||
{ node: 'ParentNode', index: 0, type: 'main' },
|
||||
{ node: 'CurrentNode', index: 0, type: 'main' },
|
||||
],
|
||||
],
|
||||
},
|
||||
},
|
||||
} as never);
|
||||
|
||||
const result = executeData(parentNodes, currentNode, inputName, runIndex);
|
||||
|
||||
expect(result.data).toEqual({ main: [[{ json: { key: 'valueFromRunData' } }]] });
|
||||
expect(result.source).toEqual({
|
||||
main: [{ previousNode: 'ParentNode', previousNodeOutput: 0, previousNodeRun: 0 }],
|
||||
});
|
||||
});
|
||||
it('should use provided parentRunIndex ', () => {
|
||||
const { executeData } = useWorkflowHelpers({ router });
|
||||
|
||||
const parentNodes = ['ParentNode'];
|
||||
const currentNode = 'CurrentNode';
|
||||
const inputName = 'main';
|
||||
const runIndex = 0;
|
||||
const parentRunIndex = 1;
|
||||
|
||||
workflowsStore.pinnedWorkflowData = undefined;
|
||||
workflowsStore.shouldReplaceInputDataWithPinData = false;
|
||||
workflowsStore.getWorkflowRunData = {
|
||||
ParentNode: [
|
||||
{ data: {} } as never,
|
||||
{
|
||||
data: { main: [[{ json: { key: 'valueFromRunData' } }]] },
|
||||
} as never,
|
||||
],
|
||||
};
|
||||
workflowsStore.getCurrentWorkflow.mockReturnValue({
|
||||
connectionsByDestinationNode: {
|
||||
CurrentNode: {
|
||||
main: [
|
||||
[
|
||||
{ node: 'ParentNode', index: 1, type: 'main' },
|
||||
{ node: 'CurrentNode', index: 0, type: 'main' },
|
||||
],
|
||||
],
|
||||
},
|
||||
},
|
||||
} as never);
|
||||
|
||||
const result = executeData(parentNodes, currentNode, inputName, runIndex, parentRunIndex);
|
||||
|
||||
expect(result.data).toEqual({ main: [[{ json: { key: 'valueFromRunData' } }]] });
|
||||
expect(result.source).toEqual({
|
||||
main: [{ previousNode: 'ParentNode', previousNodeOutput: 1, previousNodeRun: 1 }],
|
||||
});
|
||||
});
|
||||
|
||||
it('should return empty data if neither pinnedWorkflowData nor getWorkflowRunData is available', () => {
|
||||
const { executeData } = useWorkflowHelpers({ router });
|
||||
|
||||
const parentNodes = ['ParentNode'];
|
||||
const currentNode = 'CurrentNode';
|
||||
const inputName = 'main';
|
||||
const runIndex = 0;
|
||||
|
||||
workflowsStore.pinnedWorkflowData = undefined;
|
||||
workflowsStore.shouldReplaceInputDataWithPinData = false;
|
||||
workflowsStore.getWorkflowRunData = null;
|
||||
|
||||
const result = executeData(parentNodes, currentNode, inputName, runIndex);
|
||||
|
||||
expect(result.data).toEqual({});
|
||||
expect(result.source).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -216,7 +216,13 @@ export function resolveParameter<T = IDataObject>(
|
||||
) {
|
||||
runIndexCurrent = workflowRunData[contextNode!.name].length - 1;
|
||||
}
|
||||
let _executeData = executeData(parentNode, contextNode!.name, inputName, runIndexCurrent);
|
||||
let _executeData = executeData(
|
||||
parentNode,
|
||||
contextNode!.name,
|
||||
inputName,
|
||||
runIndexCurrent,
|
||||
runIndexParent,
|
||||
);
|
||||
|
||||
if (!_executeData.source) {
|
||||
// fallback to parent's run index for multi-output case
|
||||
@@ -354,6 +360,7 @@ export function executeData(
|
||||
currentNode: string,
|
||||
inputName: string,
|
||||
runIndex: number,
|
||||
parentRunIndex?: number,
|
||||
): IExecuteData {
|
||||
const executeData = {
|
||||
node: {},
|
||||
@@ -361,6 +368,8 @@ export function executeData(
|
||||
source: null,
|
||||
} as IExecuteData;
|
||||
|
||||
parentRunIndex = parentRunIndex ?? runIndex;
|
||||
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
|
||||
// Find the parent node which has data
|
||||
@@ -386,15 +395,15 @@ export function executeData(
|
||||
|
||||
if (
|
||||
!workflowRunData[parentNodeName] ||
|
||||
workflowRunData[parentNodeName].length <= runIndex ||
|
||||
!workflowRunData[parentNodeName][runIndex] ||
|
||||
!workflowRunData[parentNodeName][runIndex].hasOwnProperty('data') ||
|
||||
workflowRunData[parentNodeName][runIndex].data === undefined ||
|
||||
!workflowRunData[parentNodeName][runIndex].data.hasOwnProperty(inputName)
|
||||
workflowRunData[parentNodeName].length <= parentRunIndex ||
|
||||
!workflowRunData[parentNodeName][parentRunIndex] ||
|
||||
!workflowRunData[parentNodeName][parentRunIndex].hasOwnProperty('data') ||
|
||||
workflowRunData[parentNodeName][parentRunIndex].data === undefined ||
|
||||
!workflowRunData[parentNodeName][parentRunIndex].data?.hasOwnProperty(inputName)
|
||||
) {
|
||||
executeData.data = {};
|
||||
} else {
|
||||
executeData.data = workflowRunData[parentNodeName][runIndex].data!;
|
||||
executeData.data = workflowRunData[parentNodeName][parentRunIndex].data!;
|
||||
if (workflowRunData[currentNode] && workflowRunData[currentNode][runIndex]) {
|
||||
executeData.source = {
|
||||
[inputName]: workflowRunData[currentNode][runIndex].source,
|
||||
@@ -427,6 +436,7 @@ export function executeData(
|
||||
{
|
||||
previousNode: parentNodeName,
|
||||
previousNodeOutput,
|
||||
previousNodeRun: parentRunIndex,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -1,233 +0,0 @@
|
||||
import { createPinia, setActivePinia } from 'pinia';
|
||||
import { useSettingsStore } from '@/stores/settings.store';
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import { setupServer } from '@/__tests__/server';
|
||||
import { executeData } from '@/composables/useWorkflowHelpers';
|
||||
import type { IExecutionResponse } from '@/Interface';
|
||||
|
||||
describe('workflowHelpers', () => {
|
||||
let server: ReturnType<typeof setupServer>;
|
||||
let pinia: ReturnType<typeof createPinia>;
|
||||
let workflowsStore: ReturnType<typeof useWorkflowsStore>;
|
||||
let settingsStore: ReturnType<typeof useSettingsStore>;
|
||||
|
||||
beforeAll(() => {
|
||||
server = setupServer();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
pinia = createPinia();
|
||||
setActivePinia(pinia);
|
||||
|
||||
workflowsStore = useWorkflowsStore();
|
||||
settingsStore = useSettingsStore();
|
||||
|
||||
await settingsStore.getSettings();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
server.shutdown();
|
||||
});
|
||||
|
||||
describe('executeData()', () => {
|
||||
it('should return empty execute data if no parent nodes', () => {
|
||||
const parentNodes: string[] = [];
|
||||
const currentNode = 'Set';
|
||||
const inputName = 'main';
|
||||
const runIndex = 0;
|
||||
|
||||
const result = executeData(parentNodes, currentNode, inputName, runIndex);
|
||||
|
||||
expect(result).toEqual({
|
||||
node: {},
|
||||
data: {},
|
||||
source: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return the correct execution data with one parent node', () => {
|
||||
const parentNodes = ['Start'];
|
||||
const currentNode = 'Set';
|
||||
const inputName = 'main';
|
||||
const runIndex = 0;
|
||||
const jsonData = {
|
||||
name: 'Test',
|
||||
};
|
||||
|
||||
workflowsStore.workflowExecutionData = {
|
||||
data: {
|
||||
resultData: {
|
||||
runData: {
|
||||
[parentNodes[0]]: [
|
||||
{
|
||||
startTime: 0,
|
||||
executionTime: 0,
|
||||
data: {
|
||||
main: [
|
||||
{
|
||||
json: jsonData,
|
||||
index: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
source: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as IExecutionResponse;
|
||||
|
||||
const result = executeData(parentNodes, currentNode, inputName, runIndex);
|
||||
|
||||
expect(result).toEqual({
|
||||
node: {},
|
||||
data: {
|
||||
main: [
|
||||
{
|
||||
index: 0,
|
||||
json: jsonData,
|
||||
},
|
||||
],
|
||||
},
|
||||
source: {
|
||||
main: [
|
||||
{
|
||||
previousNode: parentNodes[0],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should return the correct execution data with multiple parent nodes, only one with execution data', () => {
|
||||
const parentNodes = ['Parent A', 'Parent B'];
|
||||
const currentNode = 'Set';
|
||||
const inputName = 'main';
|
||||
const runIndex = 0;
|
||||
const jsonData = {
|
||||
name: 'Test',
|
||||
};
|
||||
|
||||
workflowsStore.workflowExecutionData = {
|
||||
data: {
|
||||
resultData: {
|
||||
runData: {
|
||||
[parentNodes[1]]: [
|
||||
{
|
||||
startTime: 0,
|
||||
executionTime: 0,
|
||||
data: {
|
||||
main: [
|
||||
{
|
||||
json: jsonData,
|
||||
index: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
source: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as IExecutionResponse;
|
||||
|
||||
const result = executeData(parentNodes, currentNode, inputName, runIndex);
|
||||
|
||||
expect(result).toEqual({
|
||||
node: {},
|
||||
data: {
|
||||
main: [
|
||||
{
|
||||
index: 0,
|
||||
json: jsonData,
|
||||
},
|
||||
],
|
||||
},
|
||||
source: {
|
||||
main: [
|
||||
{
|
||||
previousNode: parentNodes[1],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should return the correct execution data with multiple parent nodes, all with execution data', () => {
|
||||
const parentNodes = ['Parent A', 'Parent B'];
|
||||
const currentNode = 'Set';
|
||||
const inputName = 'main';
|
||||
const runIndex = 0;
|
||||
|
||||
const jsonDataA = {
|
||||
name: 'Test A',
|
||||
};
|
||||
|
||||
const jsonDataB = {
|
||||
name: 'Test B',
|
||||
};
|
||||
|
||||
workflowsStore.workflowExecutionData = {
|
||||
data: {
|
||||
resultData: {
|
||||
runData: {
|
||||
[parentNodes[0]]: [
|
||||
{
|
||||
startTime: 0,
|
||||
executionTime: 0,
|
||||
data: {
|
||||
main: [
|
||||
{
|
||||
json: jsonDataA,
|
||||
index: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
source: [],
|
||||
},
|
||||
],
|
||||
[parentNodes[1]]: [
|
||||
{
|
||||
startTime: 0,
|
||||
executionTime: 0,
|
||||
data: {
|
||||
main: [
|
||||
{
|
||||
json: jsonDataB,
|
||||
index: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
source: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as IExecutionResponse;
|
||||
|
||||
const result = executeData(parentNodes, currentNode, inputName, runIndex);
|
||||
|
||||
expect(result).toEqual({
|
||||
node: {},
|
||||
data: {
|
||||
main: [
|
||||
{
|
||||
index: 0,
|
||||
json: jsonDataA,
|
||||
},
|
||||
],
|
||||
},
|
||||
source: {
|
||||
main: [
|
||||
{
|
||||
previousNode: parentNodes[0],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user