mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 01:56:46 +00:00
fix: Show correct "canceled" node status for chat model nodes (#19366)
This commit is contained in:
@@ -44,6 +44,7 @@ import {
|
|||||||
NodeOperationError,
|
NodeOperationError,
|
||||||
Workflow,
|
Workflow,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
import assert from 'node:assert';
|
||||||
|
|
||||||
import * as Helpers from '@test/helpers';
|
import * as Helpers from '@test/helpers';
|
||||||
import { legacyWorkflowExecuteTests, v1WorkflowExecuteTests } from '@test/helpers/constants';
|
import { legacyWorkflowExecuteTests, v1WorkflowExecuteTests } from '@test/helpers/constants';
|
||||||
@@ -2485,4 +2486,87 @@ describe('WorkflowExecute', () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Cancellation', () => {
|
||||||
|
test('should update only running task statuses to cancelled when workflow is cancelled', () => {
|
||||||
|
// Arrange - create a workflow with some nodes
|
||||||
|
const startNode = createNodeData({ name: 'Start' });
|
||||||
|
const processingNode = createNodeData({ name: 'Processing' });
|
||||||
|
const completedNode = createNodeData({ name: 'Completed' });
|
||||||
|
|
||||||
|
const workflow = new Workflow({
|
||||||
|
id: 'test-workflow',
|
||||||
|
nodes: [startNode, processingNode, completedNode],
|
||||||
|
connections: {},
|
||||||
|
active: false,
|
||||||
|
nodeTypes,
|
||||||
|
});
|
||||||
|
|
||||||
|
const waitPromise = createDeferredPromise<IRun>();
|
||||||
|
const additionalData = Helpers.WorkflowExecuteAdditionalData(waitPromise);
|
||||||
|
const workflowExecute = new WorkflowExecute(additionalData, 'manual');
|
||||||
|
|
||||||
|
// Create run execution data with tasks in various statuses
|
||||||
|
const runExecutionData: IRunExecutionData = {
|
||||||
|
startData: { startNodes: [{ name: 'Start', sourceData: null }] },
|
||||||
|
resultData: {
|
||||||
|
runData: {
|
||||||
|
Start: [toITaskData([{ data: { test: 'data' } }], { executionStatus: 'success' })],
|
||||||
|
Processing: [toITaskData([{ data: { test: 'data' } }], { executionStatus: 'running' })],
|
||||||
|
Completed: [
|
||||||
|
toITaskData([{ data: { test: 'data1' } }], { executionStatus: 'error' }),
|
||||||
|
toITaskData([{ data: { test: 'data2' } }], { executionStatus: 'running' }),
|
||||||
|
toITaskData([{ data: { test: 'data3' } }], { executionStatus: 'waiting' }),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
lastNodeExecuted: 'Processing',
|
||||||
|
},
|
||||||
|
executionData: mock<IRunExecutionData['executionData']>({
|
||||||
|
nodeExecutionStack: [],
|
||||||
|
metadata: {},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Set the run execution data on the workflow execute instance
|
||||||
|
// @ts-expect-error private data
|
||||||
|
workflowExecute.runExecutionData = runExecutionData;
|
||||||
|
|
||||||
|
assert(additionalData.hooks);
|
||||||
|
const runHook = jest.fn();
|
||||||
|
additionalData.hooks.runHook = runHook;
|
||||||
|
|
||||||
|
const promise = workflowExecute.processRunExecutionData(workflow);
|
||||||
|
promise.cancel('reason');
|
||||||
|
|
||||||
|
const updatedExecutionData = {
|
||||||
|
data: {
|
||||||
|
startData: { startNodes: [{ name: 'Start', sourceData: null }] },
|
||||||
|
resultData: {
|
||||||
|
runData: {
|
||||||
|
Start: [toITaskData([{ data: { test: 'data' } }], { executionStatus: 'success' })],
|
||||||
|
Processing: [
|
||||||
|
toITaskData([{ data: { test: 'data' } }], { executionStatus: 'canceled' }),
|
||||||
|
],
|
||||||
|
Completed: [
|
||||||
|
toITaskData([{ data: { test: 'data1' } }], { executionStatus: 'error' }),
|
||||||
|
toITaskData([{ data: { test: 'data2' } }], { executionStatus: 'canceled' }),
|
||||||
|
toITaskData([{ data: { test: 'data3' } }], { executionStatus: 'waiting' }),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
lastNodeExecuted: 'Processing',
|
||||||
|
},
|
||||||
|
executionData: mock<IRunExecutionData['executionData']>({
|
||||||
|
nodeExecutionStack: [],
|
||||||
|
metadata: {},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(runHook.mock.lastCall[0]).toEqual('workflowExecuteAfter');
|
||||||
|
expect(JSON.stringify(runHook.mock.lastCall[1][0].data)).toBe(
|
||||||
|
JSON.stringify(updatedExecutionData.data),
|
||||||
|
);
|
||||||
|
expect(runHook.mock.lastCall[1][0].status).toEqual('canceled');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import type {
|
|||||||
NodeConnectionType,
|
NodeConnectionType,
|
||||||
IRunData,
|
IRunData,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import { ApplicationError, NodeConnectionTypes } from 'n8n-workflow';
|
import { ApplicationError, ExecutionCancelledError, NodeConnectionTypes } from 'n8n-workflow';
|
||||||
|
|
||||||
import { describeCommonTests } from './shared-tests';
|
import { describeCommonTests } from './shared-tests';
|
||||||
import { SupplyDataContext } from '../supply-data-context';
|
import { SupplyDataContext } from '../supply-data-context';
|
||||||
@@ -219,4 +219,70 @@ describe('SupplyDataContext', () => {
|
|||||||
expect(latestRunIndex).toBe(2);
|
expect(latestRunIndex).toBe(2);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('addExecutionDataFunctions', () => {
|
||||||
|
it('should preserve canceled status when execution is aborted and output has error', async () => {
|
||||||
|
const errorData = new ExecutionCancelledError('Execution was aborted');
|
||||||
|
const abortedSignal = mock<AbortSignal>({ aborted: true });
|
||||||
|
const mockHooks = {
|
||||||
|
runHook: jest.fn().mockResolvedValue(undefined),
|
||||||
|
};
|
||||||
|
const testAdditionalData = mock<IWorkflowExecuteAdditionalData>({
|
||||||
|
credentialsHelper,
|
||||||
|
hooks: mockHooks,
|
||||||
|
currentNodeExecutionIndex: 0,
|
||||||
|
});
|
||||||
|
const testRunExecutionData = mock<IRunExecutionData>({
|
||||||
|
resultData: {
|
||||||
|
runData: {
|
||||||
|
[node.name]: [
|
||||||
|
{
|
||||||
|
executionStatus: 'canceled',
|
||||||
|
startTime: Date.now(),
|
||||||
|
executionTime: 0,
|
||||||
|
executionIndex: 0,
|
||||||
|
error: undefined,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
error: undefined,
|
||||||
|
},
|
||||||
|
executionData: { metadata: {} },
|
||||||
|
});
|
||||||
|
|
||||||
|
const contextWithAbort = new SupplyDataContext(
|
||||||
|
workflow,
|
||||||
|
node,
|
||||||
|
testAdditionalData,
|
||||||
|
mode,
|
||||||
|
testRunExecutionData,
|
||||||
|
runIndex,
|
||||||
|
connectionInputData,
|
||||||
|
inputData,
|
||||||
|
'ai_agent',
|
||||||
|
executeData,
|
||||||
|
[closeFn],
|
||||||
|
abortedSignal,
|
||||||
|
);
|
||||||
|
|
||||||
|
await contextWithAbort.addExecutionDataFunctions(
|
||||||
|
'output',
|
||||||
|
errorData,
|
||||||
|
'ai_agent',
|
||||||
|
node.name,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
|
||||||
|
const taskData = testRunExecutionData.resultData.runData[node.name][0];
|
||||||
|
expect(taskData.executionStatus).toBe('canceled');
|
||||||
|
expect(taskData.error).toBeUndefined();
|
||||||
|
|
||||||
|
// Verify nodeExecuteAfter hook was called correctly
|
||||||
|
expect(mockHooks.runHook).toHaveBeenCalledWith('nodeExecuteAfter', [
|
||||||
|
node.name,
|
||||||
|
taskData,
|
||||||
|
testRunExecutionData,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -280,8 +280,14 @@ export class SupplyDataContext extends BaseExecuteContext implements ISupplyData
|
|||||||
taskData = taskData!;
|
taskData = taskData!;
|
||||||
|
|
||||||
if (data instanceof Error) {
|
if (data instanceof Error) {
|
||||||
|
// if running node was already marked as "canceled" because execution was aborted
|
||||||
|
// leave as "canceled" instead of showing "This operation was aborted" error
|
||||||
|
if (
|
||||||
|
!(type === 'output' && this.abortSignal?.aborted && taskData.executionStatus === 'canceled')
|
||||||
|
) {
|
||||||
taskData.executionStatus = 'error';
|
taskData.executionStatus = 'error';
|
||||||
taskData.error = data;
|
taskData.error = data;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
if (type === 'output') {
|
if (type === 'output') {
|
||||||
taskData.executionStatus = 'success';
|
taskData.executionStatus = 'success';
|
||||||
|
|||||||
@@ -1584,6 +1584,7 @@ export class WorkflowExecute {
|
|||||||
onCancel.shouldReject = false;
|
onCancel.shouldReject = false;
|
||||||
onCancel(() => {
|
onCancel(() => {
|
||||||
this.status = 'canceled';
|
this.status = 'canceled';
|
||||||
|
this.updateTaskStatusesToCancelled();
|
||||||
this.abortController.abort();
|
this.abortController.abort();
|
||||||
const fullRunData = this.getFullRunData(startedAt);
|
const fullRunData = this.getFullRunData(startedAt);
|
||||||
void hooks.runHook('workflowExecuteAfter', [fullRunData]);
|
void hooks.runHook('workflowExecuteAfter', [fullRunData]);
|
||||||
@@ -2654,6 +2655,17 @@ export class WorkflowExecute {
|
|||||||
return nodeSuccessData;
|
return nodeSuccessData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private updateTaskStatusesToCancelled(): void {
|
||||||
|
Object.keys(this.runExecutionData.resultData.runData).forEach((nodeName) => {
|
||||||
|
const taskDataArray = this.runExecutionData.resultData.runData[nodeName];
|
||||||
|
taskDataArray.forEach((taskData) => {
|
||||||
|
if (taskData.executionStatus === 'running') {
|
||||||
|
taskData.executionStatus = 'canceled';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private get isCancelled() {
|
private get isCancelled() {
|
||||||
return this.abortController.signal.aborted;
|
return this.abortController.signal.aborted;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1883,6 +1883,7 @@
|
|||||||
"runData.editValue": "Edit Value",
|
"runData.editValue": "Edit Value",
|
||||||
"runData.executionStatus.success": "Executed successfully",
|
"runData.executionStatus.success": "Executed successfully",
|
||||||
"runData.executionStatus.failed": "Execution failed",
|
"runData.executionStatus.failed": "Execution failed",
|
||||||
|
"runData.executionStatus.canceled": "Execution canceled",
|
||||||
"runData.downloadBinaryData": "Download",
|
"runData.downloadBinaryData": "Download",
|
||||||
"runData.executeNode": "Test Node",
|
"runData.executeNode": "Test Node",
|
||||||
"runData.executionTime": "Execution Time",
|
"runData.executionTime": "Execution Time",
|
||||||
|
|||||||
155
packages/frontend/editor-ui/src/components/RunInfo.test.ts
Normal file
155
packages/frontend/editor-ui/src/components/RunInfo.test.ts
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
import { describe, it, expect, vi } from 'vitest';
|
||||||
|
import type { ITaskData } from 'n8n-workflow';
|
||||||
|
import RunInfo from './RunInfo.vue';
|
||||||
|
import { createComponentRenderer } from '@/__tests__/render';
|
||||||
|
import { mock } from 'vitest-mock-extended';
|
||||||
|
|
||||||
|
vi.mock('@/utils/formatters/dateFormatter', () => ({
|
||||||
|
convertToDisplayDateComponents: vi.fn(() => ({
|
||||||
|
date: 'Jan 15',
|
||||||
|
time: '10:30:00',
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@n8n/i18n', async (importOriginal) => {
|
||||||
|
return {
|
||||||
|
...(await importOriginal()),
|
||||||
|
useI18n: () => ({
|
||||||
|
baseText: vi.fn((key: string) => {
|
||||||
|
const translations: Record<string, string> = {
|
||||||
|
'runData.executionStatus.success': 'Success',
|
||||||
|
'runData.executionStatus.canceled': 'Canceled',
|
||||||
|
'runData.executionStatus.failed': 'Failed',
|
||||||
|
'runData.startTime': 'Start time',
|
||||||
|
'runData.executionTime': 'Execution time',
|
||||||
|
'runData.ms': 'ms',
|
||||||
|
};
|
||||||
|
return translations[key] || key;
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const renderComponent = createComponentRenderer(RunInfo);
|
||||||
|
|
||||||
|
describe('RunInfo', () => {
|
||||||
|
it('should display success status when execution status is success', () => {
|
||||||
|
const successTaskData: ITaskData = mock<ITaskData>({
|
||||||
|
startTime: Date.now(),
|
||||||
|
executionTime: 1500,
|
||||||
|
executionStatus: 'success',
|
||||||
|
data: {},
|
||||||
|
error: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { getByTestId, container } = renderComponent({
|
||||||
|
props: {
|
||||||
|
taskData: successTaskData,
|
||||||
|
hasStaleData: false,
|
||||||
|
hasPinData: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(getByTestId('node-run-status-success')).toBeInTheDocument();
|
||||||
|
expect(getByTestId('node-run-info')).toBeInTheDocument();
|
||||||
|
|
||||||
|
const tooltipDiv = container.querySelector('.tooltipRow');
|
||||||
|
expect(tooltipDiv).toBeInTheDocument();
|
||||||
|
|
||||||
|
const statusIcon = getByTestId('node-run-status-success');
|
||||||
|
expect(statusIcon).toHaveClass('success');
|
||||||
|
|
||||||
|
const infoIcon = getByTestId('node-run-info');
|
||||||
|
expect(infoIcon).toBeInTheDocument();
|
||||||
|
|
||||||
|
// Verify the component renders with success status
|
||||||
|
expect(statusIcon).toHaveAttribute('data-test-id', 'node-run-status-success');
|
||||||
|
|
||||||
|
// Check tooltip content exists in the DOM (even if hidden)
|
||||||
|
expect(document.body).toHaveTextContent('Success');
|
||||||
|
expect(document.body).toHaveTextContent('Start time:');
|
||||||
|
expect(document.body).toHaveTextContent('Jan 15 at 10:30:00');
|
||||||
|
expect(document.body).toHaveTextContent('Execution time:');
|
||||||
|
expect(document.body).toHaveTextContent('1500 ms');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display cancelled status when execution status is canceled', () => {
|
||||||
|
const cancelledTaskData: ITaskData = mock<ITaskData>({
|
||||||
|
startTime: 1757506978099,
|
||||||
|
executionTime: 800,
|
||||||
|
executionStatus: 'canceled',
|
||||||
|
data: {},
|
||||||
|
error: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { getByTestId, container, queryByTestId } = renderComponent({
|
||||||
|
props: {
|
||||||
|
taskData: cancelledTaskData,
|
||||||
|
hasStaleData: false,
|
||||||
|
hasPinData: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(queryByTestId('node-run-status-success')).not.toBeInTheDocument();
|
||||||
|
expect(getByTestId('node-run-info')).toBeInTheDocument();
|
||||||
|
|
||||||
|
const tooltipDiv = container.querySelector('.tooltipRow');
|
||||||
|
expect(tooltipDiv).toBeInTheDocument();
|
||||||
|
|
||||||
|
const infoIcon = getByTestId('node-run-info');
|
||||||
|
expect(infoIcon).toBeInTheDocument();
|
||||||
|
|
||||||
|
// For cancelled status, only info tooltip is shown (no status icon)
|
||||||
|
expect(infoIcon).toHaveAttribute('data-test-id', 'node-run-info');
|
||||||
|
|
||||||
|
// Check tooltip content exists in the DOM (even if hidden)
|
||||||
|
expect(document.body).toHaveTextContent('Canceled');
|
||||||
|
expect(document.body).toHaveTextContent('Start time:');
|
||||||
|
expect(document.body).toHaveTextContent('Jan 15 at 10:30:00');
|
||||||
|
expect(document.body).toHaveTextContent('Execution time:');
|
||||||
|
expect(document.body).toHaveTextContent('800 ms');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display error status when there is an error', () => {
|
||||||
|
const errorTaskData: ITaskData = mock<ITaskData>({
|
||||||
|
startTime: 1757506978099,
|
||||||
|
executionTime: 1200,
|
||||||
|
executionStatus: 'success',
|
||||||
|
data: {},
|
||||||
|
error: {
|
||||||
|
message: 'Something went wrong',
|
||||||
|
name: 'Error',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { getByTestId, container } = renderComponent({
|
||||||
|
props: {
|
||||||
|
taskData: errorTaskData,
|
||||||
|
hasStaleData: false,
|
||||||
|
hasPinData: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(getByTestId('node-run-status-danger')).toBeInTheDocument();
|
||||||
|
expect(getByTestId('node-run-info')).toBeInTheDocument();
|
||||||
|
|
||||||
|
const tooltipDiv = container.querySelector('.tooltipRow');
|
||||||
|
expect(tooltipDiv).toBeInTheDocument();
|
||||||
|
|
||||||
|
const statusIcon = getByTestId('node-run-status-danger');
|
||||||
|
expect(statusIcon).toHaveClass('danger');
|
||||||
|
|
||||||
|
const infoIcon = getByTestId('node-run-info');
|
||||||
|
expect(infoIcon).toBeInTheDocument();
|
||||||
|
|
||||||
|
// Verify the component renders with error status
|
||||||
|
expect(statusIcon).toHaveAttribute('data-test-id', 'node-run-status-danger');
|
||||||
|
|
||||||
|
// Check tooltip content exists in the DOM (even if hidden)
|
||||||
|
expect(document.body).toHaveTextContent('Failed');
|
||||||
|
expect(document.body).toHaveTextContent('Start time:');
|
||||||
|
expect(document.body).toHaveTextContent('Jan 15 at 10:30:00');
|
||||||
|
expect(document.body).toHaveTextContent('Execution time:');
|
||||||
|
expect(document.body).toHaveTextContent('1200 ms');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -53,6 +53,7 @@ const runMetadata = computed(() => {
|
|||||||
</N8nInfoTip>
|
</N8nInfoTip>
|
||||||
<div v-else-if="runMetadata" :class="$style.tooltipRow">
|
<div v-else-if="runMetadata" :class="$style.tooltipRow">
|
||||||
<N8nInfoTip
|
<N8nInfoTip
|
||||||
|
v-if="taskData?.executionStatus !== 'canceled'"
|
||||||
type="note"
|
type="note"
|
||||||
:theme="theme"
|
:theme="theme"
|
||||||
:data-test-id="`node-run-status-${theme}`"
|
:data-test-id="`node-run-status-${theme}`"
|
||||||
@@ -69,6 +70,8 @@ const runMetadata = computed(() => {
|
|||||||
>{{
|
>{{
|
||||||
runTaskData?.error
|
runTaskData?.error
|
||||||
? i18n.baseText('runData.executionStatus.failed')
|
? i18n.baseText('runData.executionStatus.failed')
|
||||||
|
: runTaskData?.executionStatus === 'canceled'
|
||||||
|
? i18n.baseText('runData.executionStatus.canceled')
|
||||||
: i18n.baseText('runData.executionStatus.success')
|
: i18n.baseText('runData.executionStatus.success')
|
||||||
}} </n8n-text
|
}} </n8n-text
|
||||||
><br />
|
><br />
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ const classes = computed(() => {
|
|||||||
[$style.node]: true,
|
[$style.node]: true,
|
||||||
[$style.selected]: isSelected.value,
|
[$style.selected]: isSelected.value,
|
||||||
[$style.disabled]: isDisabled.value,
|
[$style.disabled]: isDisabled.value,
|
||||||
[$style.success]: hasRunData.value,
|
[$style.success]: hasRunData.value && executionStatus.value === 'success',
|
||||||
[$style.error]: hasExecutionErrors.value,
|
[$style.error]: hasExecutionErrors.value,
|
||||||
[$style.pinned]: hasPinnedData.value,
|
[$style.pinned]: hasPinnedData.value,
|
||||||
[$style.waiting]: executionWaiting.value ?? executionStatus.value === 'waiting',
|
[$style.waiting]: executionWaiting.value ?? executionStatus.value === 'waiting',
|
||||||
|
|||||||
@@ -75,7 +75,10 @@ describe('CanvasNodeStatusIcons', () => {
|
|||||||
provide: {
|
provide: {
|
||||||
...createCanvasProvide(),
|
...createCanvasProvide(),
|
||||||
...createCanvasNodeProvide({
|
...createCanvasNodeProvide({
|
||||||
data: { runData: { outputMap: {}, iterations: 15, visible: true } },
|
data: {
|
||||||
|
execution: { status: 'success', running: false },
|
||||||
|
runData: { outputMap: {}, iterations: 15, visible: true },
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -84,6 +87,24 @@ describe('CanvasNodeStatusIcons', () => {
|
|||||||
expect(getByTestId('canvas-node-status-success')).toHaveTextContent('15');
|
expect(getByTestId('canvas-node-status-success')).toHaveTextContent('15');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not render success icon for a node that was canceled', () => {
|
||||||
|
const { queryByTestId } = renderComponent({
|
||||||
|
global: {
|
||||||
|
provide: {
|
||||||
|
...createCanvasProvide(),
|
||||||
|
...createCanvasNodeProvide({
|
||||||
|
data: {
|
||||||
|
execution: { status: 'canceled', running: false },
|
||||||
|
runData: { outputMap: {}, iterations: 15, visible: true },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(queryByTestId('canvas-node-status-success')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
it('should render correctly for a dirty node that has run successfully', () => {
|
it('should render correctly for a dirty node that has run successfully', () => {
|
||||||
const { getByTestId } = renderComponent({
|
const { getByTestId } = renderComponent({
|
||||||
global: {
|
global: {
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ const commonClasses = computed(() => [
|
|||||||
</N8nTooltip>
|
</N8nTooltip>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else-if="hasRunData"
|
v-else-if="hasRunData && executionStatus === 'success'"
|
||||||
data-test-id="canvas-node-status-success"
|
data-test-id="canvas-node-status-success"
|
||||||
:class="[...commonClasses, $style.runData]"
|
:class="[...commonClasses, $style.runData]"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -567,6 +567,116 @@ describe('useCanvasMapping', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not count canceled iterations but still count their data', () => {
|
||||||
|
const workflowsStore = mockedStore(useWorkflowsStore);
|
||||||
|
const nodes = [createTestNode({ name: 'Node 1' })];
|
||||||
|
const connections = {};
|
||||||
|
const workflowObject = createTestWorkflowObject({
|
||||||
|
nodes,
|
||||||
|
connections,
|
||||||
|
});
|
||||||
|
|
||||||
|
workflowsStore.getWorkflowResultDataByNodeName.mockReturnValue([
|
||||||
|
{
|
||||||
|
startTime: 0,
|
||||||
|
executionTime: 0,
|
||||||
|
executionIndex: 0,
|
||||||
|
source: [],
|
||||||
|
executionStatus: 'success',
|
||||||
|
data: {
|
||||||
|
[NodeConnectionTypes.Main]: [[{ json: {} }, { json: {} }]],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
startTime: 0,
|
||||||
|
executionTime: 0,
|
||||||
|
executionIndex: 1,
|
||||||
|
source: [],
|
||||||
|
executionStatus: 'canceled',
|
||||||
|
data: {
|
||||||
|
[NodeConnectionTypes.Main]: [[{ json: {} }, { json: {} }, { json: {} }]],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
startTime: 0,
|
||||||
|
executionTime: 0,
|
||||||
|
executionIndex: 2,
|
||||||
|
source: [],
|
||||||
|
executionStatus: 'success',
|
||||||
|
data: {
|
||||||
|
[NodeConnectionTypes.Main]: [[{ json: {} }]],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const { nodeExecutionRunDataOutputMapById } = useCanvasMapping({
|
||||||
|
nodes: ref(nodes),
|
||||||
|
connections: ref(connections),
|
||||||
|
workflowObject: ref(workflowObject) as Ref<Workflow>,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(nodeExecutionRunDataOutputMapById.value).toEqual({
|
||||||
|
[nodes[0].id]: {
|
||||||
|
[NodeConnectionTypes.Main]: {
|
||||||
|
0: {
|
||||||
|
iterations: 2, // Only 2 iterations counted (not the canceled one)
|
||||||
|
total: 6, // All data items still counted
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle all canceled 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,
|
||||||
|
executionIndex: 0,
|
||||||
|
source: [],
|
||||||
|
executionStatus: 'canceled',
|
||||||
|
data: {
|
||||||
|
[NodeConnectionTypes.Main]: [[{ json: {} }, { json: {} }]],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
startTime: 0,
|
||||||
|
executionTime: 0,
|
||||||
|
executionIndex: 1,
|
||||||
|
source: [],
|
||||||
|
executionStatus: 'canceled',
|
||||||
|
data: {
|
||||||
|
[NodeConnectionTypes.Main]: [[{ json: {} }]],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const { nodeExecutionRunDataOutputMapById } = useCanvasMapping({
|
||||||
|
nodes: ref(nodes),
|
||||||
|
connections: ref(connections),
|
||||||
|
workflowObject: ref(workflowObject) as Ref<Workflow>,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(nodeExecutionRunDataOutputMapById.value).toEqual({
|
||||||
|
[nodes[0].id]: {
|
||||||
|
[NodeConnectionTypes.Main]: {
|
||||||
|
0: {
|
||||||
|
iterations: 0, // No iterations counted since all are canceled
|
||||||
|
total: 3, // But data items still counted
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('additionalNodePropertiesById', () => {
|
describe('additionalNodePropertiesById', () => {
|
||||||
@@ -961,6 +1071,262 @@ describe('useCanvasMapping', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('filterOutCanceled helper function', () => {
|
||||||
|
it('should return null for null input', () => {
|
||||||
|
const workflowsStore = mockedStore(useWorkflowsStore);
|
||||||
|
const nodes = [createTestNode({ name: 'Node 1' })];
|
||||||
|
const connections = {};
|
||||||
|
const workflowObject = createTestWorkflowObject({ nodes, connections });
|
||||||
|
|
||||||
|
workflowsStore.getWorkflowResultDataByNodeName.mockReturnValue(null);
|
||||||
|
|
||||||
|
const { nodes: mappedNodes } = useCanvasMapping({
|
||||||
|
nodes: ref(nodes),
|
||||||
|
connections: ref(connections),
|
||||||
|
workflowObject: ref(workflowObject) as Ref<Workflow>,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mappedNodes.value[0]?.data?.runData?.iterations).toEqual(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should filter out canceled tasks and return correct iteration count', () => {
|
||||||
|
const workflowsStore = mockedStore(useWorkflowsStore);
|
||||||
|
const nodes = [createTestNode({ name: 'Node 1' })];
|
||||||
|
const connections = {};
|
||||||
|
const workflowObject = createTestWorkflowObject({ nodes, connections });
|
||||||
|
|
||||||
|
workflowsStore.getWorkflowResultDataByNodeName.mockReturnValue([
|
||||||
|
{
|
||||||
|
startTime: 0,
|
||||||
|
executionTime: 0,
|
||||||
|
executionIndex: 0,
|
||||||
|
source: [],
|
||||||
|
executionStatus: 'success',
|
||||||
|
data: {
|
||||||
|
[NodeConnectionTypes.Main]: [[{ json: {} }]],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
startTime: 0,
|
||||||
|
executionTime: 0,
|
||||||
|
executionIndex: 1,
|
||||||
|
source: [],
|
||||||
|
executionStatus: 'canceled',
|
||||||
|
data: {
|
||||||
|
[NodeConnectionTypes.Main]: [[{ json: {} }]],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
startTime: 0,
|
||||||
|
executionTime: 0,
|
||||||
|
executionIndex: 2,
|
||||||
|
source: [],
|
||||||
|
executionStatus: 'error',
|
||||||
|
data: {
|
||||||
|
[NodeConnectionTypes.Main]: [[{ json: {} }]],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const { nodes: mappedNodes } = useCanvasMapping({
|
||||||
|
nodes: ref(nodes),
|
||||||
|
connections: ref(connections),
|
||||||
|
workflowObject: ref(workflowObject) as Ref<Workflow>,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mappedNodes.value[0]?.data?.runData?.iterations).toEqual(2);
|
||||||
|
expect(mappedNodes.value[0]?.data?.runData?.visible).toEqual(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 0 iterations when all tasks are canceled', () => {
|
||||||
|
const workflowsStore = mockedStore(useWorkflowsStore);
|
||||||
|
const nodes = [createTestNode({ name: 'Node 1' })];
|
||||||
|
const connections = {};
|
||||||
|
const workflowObject = createTestWorkflowObject({ nodes, connections });
|
||||||
|
|
||||||
|
workflowsStore.getWorkflowResultDataByNodeName.mockReturnValue([
|
||||||
|
{
|
||||||
|
startTime: 0,
|
||||||
|
executionTime: 0,
|
||||||
|
executionIndex: 0,
|
||||||
|
source: [],
|
||||||
|
executionStatus: 'canceled',
|
||||||
|
data: {
|
||||||
|
[NodeConnectionTypes.Main]: [[{ json: {} }]],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
startTime: 0,
|
||||||
|
executionTime: 0,
|
||||||
|
executionIndex: 1,
|
||||||
|
source: [],
|
||||||
|
executionStatus: 'canceled',
|
||||||
|
data: {
|
||||||
|
[NodeConnectionTypes.Main]: [[{ json: {} }]],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const { nodes: mappedNodes } = useCanvasMapping({
|
||||||
|
nodes: ref(nodes),
|
||||||
|
connections: ref(connections),
|
||||||
|
workflowObject: ref(workflowObject) as Ref<Workflow>,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mappedNodes.value[0]?.data?.runData?.iterations).toEqual(0);
|
||||||
|
expect(mappedNodes.value[0]?.data?.runData?.visible).toEqual(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return correct count when no canceled tasks', () => {
|
||||||
|
const workflowsStore = mockedStore(useWorkflowsStore);
|
||||||
|
const nodes = [createTestNode({ name: 'Node 1' })];
|
||||||
|
const connections = {};
|
||||||
|
const workflowObject = createTestWorkflowObject({ nodes, connections });
|
||||||
|
|
||||||
|
workflowsStore.getWorkflowResultDataByNodeName.mockReturnValue([
|
||||||
|
{
|
||||||
|
startTime: 0,
|
||||||
|
executionTime: 0,
|
||||||
|
executionIndex: 0,
|
||||||
|
source: [],
|
||||||
|
executionStatus: 'success',
|
||||||
|
data: {
|
||||||
|
[NodeConnectionTypes.Main]: [[{ json: {} }]],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
startTime: 0,
|
||||||
|
executionTime: 0,
|
||||||
|
executionIndex: 1,
|
||||||
|
source: [],
|
||||||
|
executionStatus: 'error',
|
||||||
|
data: {
|
||||||
|
[NodeConnectionTypes.Main]: [[{ json: {} }]],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const { nodes: mappedNodes } = useCanvasMapping({
|
||||||
|
nodes: ref(nodes),
|
||||||
|
connections: ref(connections),
|
||||||
|
workflowObject: ref(workflowObject) as Ref<Workflow>,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mappedNodes.value[0]?.data?.runData?.iterations).toEqual(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('nodeExecutionStatusById', () => {
|
||||||
|
it('should return last execution status when not canceled', () => {
|
||||||
|
const workflowsStore = mockedStore(useWorkflowsStore);
|
||||||
|
const node = createTestNode({ name: 'Test Node' });
|
||||||
|
const nodes = [node];
|
||||||
|
const connections = {};
|
||||||
|
const workflowObject = createTestWorkflowObject({ nodes, connections });
|
||||||
|
|
||||||
|
workflowsStore.getWorkflowRunData = {
|
||||||
|
'Test Node': [
|
||||||
|
{
|
||||||
|
startTime: 0,
|
||||||
|
executionTime: 0,
|
||||||
|
executionIndex: 0,
|
||||||
|
source: [],
|
||||||
|
executionStatus: 'success',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const { nodes: mappedNodes } = useCanvasMapping({
|
||||||
|
nodes: ref(nodes),
|
||||||
|
connections: ref(connections),
|
||||||
|
workflowObject: ref(workflowObject) as Ref<Workflow>,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mappedNodes.value[0]?.data?.execution?.status).toEqual('success');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return second-to-last status when last execution is canceled and multiple tasks exist', () => {
|
||||||
|
const workflowsStore = mockedStore(useWorkflowsStore);
|
||||||
|
const node = createTestNode({ name: 'Test Node' });
|
||||||
|
const nodes = [node];
|
||||||
|
const connections = {};
|
||||||
|
const workflowObject = createTestWorkflowObject({ nodes, connections });
|
||||||
|
|
||||||
|
workflowsStore.getWorkflowRunData = {
|
||||||
|
'Test Node': [
|
||||||
|
{
|
||||||
|
startTime: 0,
|
||||||
|
executionTime: 0,
|
||||||
|
executionIndex: 0,
|
||||||
|
source: [],
|
||||||
|
executionStatus: 'success',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
startTime: 0,
|
||||||
|
executionTime: 0,
|
||||||
|
executionIndex: 1,
|
||||||
|
source: [],
|
||||||
|
executionStatus: 'canceled',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const { nodes: mappedNodes } = useCanvasMapping({
|
||||||
|
nodes: ref(nodes),
|
||||||
|
connections: ref(connections),
|
||||||
|
workflowObject: ref(workflowObject) as Ref<Workflow>,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mappedNodes.value[0]?.data?.execution?.status).toEqual('success');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return canceled status when only one task exists and it is canceled', () => {
|
||||||
|
const workflowsStore = mockedStore(useWorkflowsStore);
|
||||||
|
const node = createTestNode({ name: 'Test Node' });
|
||||||
|
const nodes = [node];
|
||||||
|
const connections = {};
|
||||||
|
const workflowObject = createTestWorkflowObject({ nodes, connections });
|
||||||
|
|
||||||
|
workflowsStore.getWorkflowRunData = {
|
||||||
|
'Test Node': [
|
||||||
|
{
|
||||||
|
startTime: 0,
|
||||||
|
executionTime: 0,
|
||||||
|
executionIndex: 0,
|
||||||
|
source: [],
|
||||||
|
executionStatus: 'canceled',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const { nodes: mappedNodes } = useCanvasMapping({
|
||||||
|
nodes: ref(nodes),
|
||||||
|
connections: ref(connections),
|
||||||
|
workflowObject: ref(workflowObject) as Ref<Workflow>,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mappedNodes.value[0]?.data?.execution?.status).toEqual('canceled');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return new status when no tasks exist', () => {
|
||||||
|
const workflowsStore = mockedStore(useWorkflowsStore);
|
||||||
|
const node = createTestNode({ name: 'Test Node' });
|
||||||
|
const nodes = [node];
|
||||||
|
const connections = {};
|
||||||
|
const workflowObject = createTestWorkflowObject({ nodes, connections });
|
||||||
|
|
||||||
|
workflowsStore.getWorkflowRunData = {};
|
||||||
|
|
||||||
|
const { nodes: mappedNodes } = useCanvasMapping({
|
||||||
|
nodes: ref(nodes),
|
||||||
|
connections: ref(connections),
|
||||||
|
workflowObject: ref(workflowObject) as Ref<Workflow>,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mappedNodes.value[0]?.data?.execution?.status).toEqual('new');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('nodeHasIssuesById', () => {
|
describe('nodeHasIssuesById', () => {
|
||||||
it('should return false when node has no issues or errors', () => {
|
it('should return false when node has no issues or errors', () => {
|
||||||
const workflowsStore = mockedStore(useWorkflowsStore);
|
const workflowsStore = mockedStore(useWorkflowsStore);
|
||||||
@@ -1503,5 +1869,203 @@ describe('useCanvasMapping', () => {
|
|||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('connection status with canceled tasks', () => {
|
||||||
|
it('should not set success status when last task is canceled', () => {
|
||||||
|
const workflowsStore = mockedStore(useWorkflowsStore);
|
||||||
|
const [manualTriggerNode, setNode] = mockNodes.slice(0, 2);
|
||||||
|
const nodes = [manualTriggerNode, setNode];
|
||||||
|
const connections = {
|
||||||
|
[manualTriggerNode.name]: {
|
||||||
|
[NodeConnectionTypes.Main]: [
|
||||||
|
[{ node: setNode.name, type: NodeConnectionTypes.Main, index: 0 }],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const workflowObject = createTestWorkflowObject({
|
||||||
|
nodes,
|
||||||
|
connections,
|
||||||
|
});
|
||||||
|
|
||||||
|
workflowsStore.getWorkflowResultDataByNodeName.mockImplementation((nodeName: string) => {
|
||||||
|
if (nodeName === manualTriggerNode.name) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
startTime: 0,
|
||||||
|
executionTime: 0,
|
||||||
|
executionIndex: 0,
|
||||||
|
source: [],
|
||||||
|
executionStatus: 'canceled',
|
||||||
|
data: {
|
||||||
|
[NodeConnectionTypes.Main]: [[{ json: {} }]],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
const { connections: mappedConnections } = useCanvasMapping({
|
||||||
|
nodes: ref(nodes),
|
||||||
|
connections: ref(connections),
|
||||||
|
workflowObject: ref(workflowObject) as Ref<Workflow>,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mappedConnections.value[0]?.data?.status).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set success status when last task is canceled but previous is success', () => {
|
||||||
|
const workflowsStore = mockedStore(useWorkflowsStore);
|
||||||
|
const [manualTriggerNode, setNode] = mockNodes.slice(0, 2);
|
||||||
|
const nodes = [manualTriggerNode, setNode];
|
||||||
|
const connections = {
|
||||||
|
[manualTriggerNode.name]: {
|
||||||
|
[NodeConnectionTypes.Main]: [
|
||||||
|
[{ node: setNode.name, type: NodeConnectionTypes.Main, index: 0 }],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const workflowObject = createTestWorkflowObject({
|
||||||
|
nodes,
|
||||||
|
connections,
|
||||||
|
});
|
||||||
|
|
||||||
|
workflowsStore.getWorkflowResultDataByNodeName.mockImplementation((nodeName: string) => {
|
||||||
|
if (nodeName === manualTriggerNode.name) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
startTime: 0,
|
||||||
|
executionTime: 0,
|
||||||
|
executionIndex: 0,
|
||||||
|
source: [],
|
||||||
|
executionStatus: 'success',
|
||||||
|
data: {
|
||||||
|
[NodeConnectionTypes.Main]: [[{ json: {} }]],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
startTime: 0,
|
||||||
|
executionTime: 0,
|
||||||
|
executionIndex: 1,
|
||||||
|
source: [],
|
||||||
|
executionStatus: 'canceled',
|
||||||
|
data: {
|
||||||
|
[NodeConnectionTypes.Main]: [[{ json: {} }]],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
const { connections: mappedConnections } = useCanvasMapping({
|
||||||
|
nodes: ref(nodes),
|
||||||
|
connections: ref(connections),
|
||||||
|
workflowObject: ref(workflowObject) as Ref<Workflow>,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mappedConnections.value[0]?.data?.status).toEqual('success');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle connection with only canceled tasks', () => {
|
||||||
|
const workflowsStore = mockedStore(useWorkflowsStore);
|
||||||
|
const [manualTriggerNode, setNode] = mockNodes.slice(0, 2);
|
||||||
|
const nodes = [manualTriggerNode, setNode];
|
||||||
|
const connections = {
|
||||||
|
[manualTriggerNode.name]: {
|
||||||
|
[NodeConnectionTypes.Main]: [
|
||||||
|
[{ node: setNode.name, type: NodeConnectionTypes.Main, index: 0 }],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const workflowObject = createTestWorkflowObject({
|
||||||
|
nodes,
|
||||||
|
connections,
|
||||||
|
});
|
||||||
|
|
||||||
|
workflowsStore.getWorkflowResultDataByNodeName.mockImplementation((nodeName: string) => {
|
||||||
|
if (nodeName === manualTriggerNode.name) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
startTime: 0,
|
||||||
|
executionTime: 0,
|
||||||
|
executionIndex: 0,
|
||||||
|
source: [],
|
||||||
|
executionStatus: 'canceled',
|
||||||
|
data: {
|
||||||
|
[NodeConnectionTypes.Main]: [[{ json: {} }]],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
startTime: 0,
|
||||||
|
executionTime: 0,
|
||||||
|
executionIndex: 1,
|
||||||
|
source: [],
|
||||||
|
executionStatus: 'canceled',
|
||||||
|
data: {
|
||||||
|
[NodeConnectionTypes.Main]: [[{ json: {} }]],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
const { connections: mappedConnections } = useCanvasMapping({
|
||||||
|
nodes: ref(nodes),
|
||||||
|
connections: ref(connections),
|
||||||
|
workflowObject: ref(workflowObject) as Ref<Workflow>,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mappedConnections.value[0]?.data?.status).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should prioritize running status over canceled task handling', () => {
|
||||||
|
const workflowsStore = mockedStore(useWorkflowsStore);
|
||||||
|
const [manualTriggerNode, setNode] = mockNodes.slice(0, 2);
|
||||||
|
const nodes = [manualTriggerNode, setNode];
|
||||||
|
const connections = {
|
||||||
|
[manualTriggerNode.name]: {
|
||||||
|
[NodeConnectionTypes.Main]: [
|
||||||
|
[{ node: setNode.name, type: NodeConnectionTypes.Main, index: 0 }],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const workflowObject = createTestWorkflowObject({
|
||||||
|
nodes,
|
||||||
|
connections,
|
||||||
|
});
|
||||||
|
|
||||||
|
workflowsStore.isNodeExecuting.mockImplementation((nodeName: string) => {
|
||||||
|
return nodeName === manualTriggerNode.name;
|
||||||
|
});
|
||||||
|
|
||||||
|
workflowsStore.getWorkflowResultDataByNodeName.mockImplementation((nodeName: string) => {
|
||||||
|
if (nodeName === manualTriggerNode.name) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
startTime: 0,
|
||||||
|
executionTime: 0,
|
||||||
|
executionIndex: 0,
|
||||||
|
source: [],
|
||||||
|
executionStatus: 'canceled',
|
||||||
|
data: {
|
||||||
|
[NodeConnectionTypes.Main]: [[{ json: {} }]],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
const { connections: mappedConnections } = useCanvasMapping({
|
||||||
|
nodes: ref(nodes),
|
||||||
|
connections: ref(connections),
|
||||||
|
workflowObject: ref(workflowObject) as Ref<Workflow>,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mappedConnections.value[0]?.data?.status).toEqual('running');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -345,7 +345,11 @@ export function useCanvasMapping({
|
|||||||
nodes.value.reduce<Record<string, ExecutionStatus>>((acc, node) => {
|
nodes.value.reduce<Record<string, ExecutionStatus>>((acc, node) => {
|
||||||
const tasks = workflowsStore.getWorkflowRunData?.[node.name] ?? [];
|
const tasks = workflowsStore.getWorkflowRunData?.[node.name] ?? [];
|
||||||
|
|
||||||
acc[node.id] = tasks.at(-1)?.executionStatus ?? 'new';
|
let lastExecutionStatus = tasks.at(-1)?.executionStatus;
|
||||||
|
if (tasks.length > 1 && lastExecutionStatus === 'canceled') {
|
||||||
|
lastExecutionStatus = tasks.at(-2)?.executionStatus;
|
||||||
|
}
|
||||||
|
acc[node.id] = lastExecutionStatus ?? 'new';
|
||||||
return acc;
|
return acc;
|
||||||
}, {}),
|
}, {}),
|
||||||
);
|
);
|
||||||
@@ -382,7 +386,9 @@ export function useCanvasMapping({
|
|||||||
acc[nodeId][connectionType][outputIndex] = acc[nodeId][connectionType][
|
acc[nodeId][connectionType][outputIndex] = acc[nodeId][connectionType][
|
||||||
outputIndex
|
outputIndex
|
||||||
] ?? { ...outputData };
|
] ?? { ...outputData };
|
||||||
|
if (runIteration.executionStatus !== 'canceled') {
|
||||||
acc[nodeId][connectionType][outputIndex].iterations += 1;
|
acc[nodeId][connectionType][outputIndex].iterations += 1;
|
||||||
|
}
|
||||||
acc[nodeId][connectionType][outputIndex].total +=
|
acc[nodeId][connectionType][outputIndex].total +=
|
||||||
connectionTypeOutputIndexData.length;
|
connectionTypeOutputIndexData.length;
|
||||||
}
|
}
|
||||||
@@ -593,6 +599,14 @@ export function useCanvasMapping({
|
|||||||
}, {});
|
}, {});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function filterOutCanceled(tasks: ITaskData[] | null): ITaskData[] | null {
|
||||||
|
if (!tasks) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return tasks.filter((task) => task.executionStatus !== 'canceled');
|
||||||
|
}
|
||||||
|
|
||||||
const mappedNodes = computed<CanvasNode[]>(() => {
|
const mappedNodes = computed<CanvasNode[]>(() => {
|
||||||
const connectionsBySourceNode = connections.value;
|
const connectionsBySourceNode = connections.value;
|
||||||
const connectionsByDestinationNode =
|
const connectionsByDestinationNode =
|
||||||
@@ -635,7 +649,7 @@ export function useCanvasMapping({
|
|||||||
},
|
},
|
||||||
runData: {
|
runData: {
|
||||||
outputMap: nodeExecutionRunDataOutputMapById.value[node.id],
|
outputMap: nodeExecutionRunDataOutputMapById.value[node.id],
|
||||||
iterations: nodeExecutionRunDataById.value[node.id]?.length ?? 0,
|
iterations: filterOutCanceled(nodeExecutionRunDataById.value[node.id])?.length ?? 0,
|
||||||
visible: !!nodeExecutionRunDataById.value[node.id],
|
visible: !!nodeExecutionRunDataById.value[node.id],
|
||||||
},
|
},
|
||||||
render: renderTypeByNodeId.value[node.id] ?? { type: 'default', options: {} },
|
render: renderTypeByNodeId.value[node.id] ?? { type: 'default', options: {} },
|
||||||
@@ -677,6 +691,12 @@ export function useCanvasMapping({
|
|||||||
const runDataTotal =
|
const runDataTotal =
|
||||||
nodeExecutionRunDataOutputMapById.value[connection.source]?.[type]?.[index]?.total ?? 0;
|
nodeExecutionRunDataOutputMapById.value[connection.source]?.[type]?.[index]?.total ?? 0;
|
||||||
|
|
||||||
|
const sourceTasks = nodeExecutionRunDataById.value[connection.source] ?? [];
|
||||||
|
let lastSourceTask: ITaskData | undefined = sourceTasks[sourceTasks.length - 1];
|
||||||
|
if (lastSourceTask?.executionStatus === 'canceled' && sourceTasks.length > 1) {
|
||||||
|
lastSourceTask = sourceTasks[sourceTasks.length - 2];
|
||||||
|
}
|
||||||
|
|
||||||
let status: CanvasConnectionData['status'];
|
let status: CanvasConnectionData['status'];
|
||||||
if (nodeExecutionRunningById.value[connection.source]) {
|
if (nodeExecutionRunningById.value[connection.source]) {
|
||||||
status = 'running';
|
status = 'running';
|
||||||
@@ -687,7 +707,7 @@ export function useCanvasMapping({
|
|||||||
status = 'pinned';
|
status = 'pinned';
|
||||||
} else if (nodeHasIssuesById.value[connection.source]) {
|
} else if (nodeHasIssuesById.value[connection.source]) {
|
||||||
status = 'error';
|
status = 'error';
|
||||||
} else if (runDataTotal > 0) {
|
} else if (runDataTotal > 0 && lastSourceTask?.executionStatus !== 'canceled') {
|
||||||
status = 'success';
|
status = 'success';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user