diff --git a/packages/frontend/@n8n/design-system/src/components/N8nIcon/custom/node-error.svg b/packages/frontend/@n8n/design-system/src/components/N8nIcon/custom/node-execution-error.svg
similarity index 100%
rename from packages/frontend/@n8n/design-system/src/components/N8nIcon/custom/node-error.svg
rename to packages/frontend/@n8n/design-system/src/components/N8nIcon/custom/node-execution-error.svg
diff --git a/packages/frontend/@n8n/design-system/src/components/N8nIcon/custom/node-validation-error.svg b/packages/frontend/@n8n/design-system/src/components/N8nIcon/custom/node-validation-error.svg
new file mode 100644
index 0000000000..333ec09c31
--- /dev/null
+++ b/packages/frontend/@n8n/design-system/src/components/N8nIcon/custom/node-validation-error.svg
@@ -0,0 +1,3 @@
+
diff --git a/packages/frontend/@n8n/design-system/src/components/N8nIcon/icons.ts b/packages/frontend/@n8n/design-system/src/components/N8nIcon/icons.ts
index 17bc277cfc..e9a6318f74 100644
--- a/packages/frontend/@n8n/design-system/src/components/N8nIcon/icons.ts
+++ b/packages/frontend/@n8n/design-system/src/components/N8nIcon/icons.ts
@@ -5,12 +5,13 @@ import EmptyOutput from './custom/empty-output.svg';
import GripLinesVertical from './custom/grip-lines-vertical.svg';
import NodeDirty from './custom/node-dirty.svg';
import NodeEllipsis from './custom/node-ellipsis.svg';
-import NodeError from './custom/node-error.svg';
+import NodeExecutionError from './custom/node-execution-error.svg';
import NodePin from './custom/node-pin.svg';
import NodePlay from './custom/node-play.svg';
import NodePower from './custom/node-power.svg';
import NodeSuccess from './custom/node-success.svg';
import NodeTrash from './custom/node-trash.svg';
+import NodeValidationError from './custom/node-validation-error.svg';
import PopOut from './custom/pop-out.svg';
import Retry from './custom/retry.svg';
import RunOnce from './custom/run-once.svg';
@@ -434,7 +435,8 @@ export const updatedIconSet = {
spinner: Spinner,
'node-dirty': NodeDirty,
'node-ellipsis': NodeEllipsis,
- 'node-error': NodeError,
+ 'node-execution-error': NodeExecutionError,
+ 'node-validation-error': NodeValidationError,
'node-pin': NodePin,
'node-play': NodePlay,
'node-power': NodePower,
diff --git a/packages/frontend/editor-ui/src/__tests__/data/canvas.ts b/packages/frontend/editor-ui/src/__tests__/data/canvas.ts
index ebd99c9c15..f70bd10ebb 100644
--- a/packages/frontend/editor-ui/src/__tests__/data/canvas.ts
+++ b/packages/frontend/editor-ui/src/__tests__/data/canvas.ts
@@ -28,7 +28,7 @@ export function createCanvasNodeData({
outputs = [],
connections = { [CanvasConnectionMode.Input]: {}, [CanvasConnectionMode.Output]: {} },
execution = { running: false },
- issues = { items: [], visible: false },
+ issues = { execution: [], validation: [], visible: false },
pinnedData = { count: 0, visible: false },
runData = { outputMap: {}, iterations: 0, visible: false },
render = {
diff --git a/packages/frontend/editor-ui/src/components/canvas/elements/nodes/render-types/CanvasNodeDefault.vue b/packages/frontend/editor-ui/src/components/canvas/elements/nodes/render-types/CanvasNodeDefault.vue
index ee1d746563..4c9aec478c 100644
--- a/packages/frontend/editor-ui/src/components/canvas/elements/nodes/render-types/CanvasNodeDefault.vue
+++ b/packages/frontend/editor-ui/src/components/canvas/elements/nodes/render-types/CanvasNodeDefault.vue
@@ -35,7 +35,7 @@ const {
executionWaitingForNext,
executionRunning,
hasRunData,
- hasIssues,
+ hasExecutionErrors,
render,
} = useCanvasNode();
const { mainOutputs, mainOutputConnections, mainInputs, mainInputConnections, nonMainInputs } =
@@ -54,7 +54,7 @@ const classes = computed(() => {
[$style.selected]: isSelected.value,
[$style.disabled]: isDisabled.value,
[$style.success]: hasRunData.value,
- [$style.error]: hasIssues.value,
+ [$style.error]: hasExecutionErrors.value,
[$style.pinned]: hasPinnedData.value,
[$style.waiting]: executionWaiting.value ?? executionStatus.value === 'waiting',
[$style.running]: executionRunning.value || executionWaitingForNext.value,
diff --git a/packages/frontend/editor-ui/src/components/canvas/elements/nodes/render-types/parts/CanvasNodeStatusIcons.vue b/packages/frontend/editor-ui/src/components/canvas/elements/nodes/render-types/parts/CanvasNodeStatusIcons.vue
index f8ed31a9d8..5236b0e6d1 100644
--- a/packages/frontend/editor-ui/src/components/canvas/elements/nodes/render-types/parts/CanvasNodeStatusIcons.vue
+++ b/packages/frontend/editor-ui/src/components/canvas/elements/nodes/render-types/parts/CanvasNodeStatusIcons.vue
@@ -24,8 +24,10 @@ const $style = useCssModule();
const {
hasPinnedData,
- issues,
- hasIssues,
+ executionErrors,
+ validationErrors,
+ hasExecutionErrors,
+ hasValidationErrors,
executionStatus,
executionWaiting,
executionWaitingForNext,
@@ -66,12 +68,6 @@ const commonClasses = computed(() => [
-
-
-
[
-
+
-
+
+
+
+
+
+
+
+
+
@@ -152,7 +160,6 @@ const commonClasses = computed(() => [
color: var(--color-secondary);
}
-.node-waiting-spinner,
.running {
color: hsl(var(--color-primary-h), var(--color-primary-s), var(--color-primary-l));
diff --git a/packages/frontend/editor-ui/src/composables/useCanvasMapping.test.ts b/packages/frontend/editor-ui/src/composables/useCanvasMapping.test.ts
index 377b1a965d..bc232bb10e 100644
--- a/packages/frontend/editor-ui/src/composables/useCanvasMapping.test.ts
+++ b/packages/frontend/editor-ui/src/composables/useCanvasMapping.test.ts
@@ -116,7 +116,8 @@ describe('useCanvasMapping', () => {
waitingForNext: false,
},
issues: {
- items: [],
+ execution: [],
+ validation: [],
visible: false,
},
pinnedData: {
@@ -705,8 +706,8 @@ describe('useCanvasMapping', () => {
});
});
- describe('nodeIssuesById', () => {
- it('should return empty array when node has no issues', () => {
+ describe('node issues', () => {
+ it('should return empty arrays when node has no issues', () => {
const workflowsStore = mockedStore(useWorkflowsStore);
const node = createTestNode({ name: 'Test Node' });
const nodes = [node];
@@ -715,13 +716,17 @@ describe('useCanvasMapping', () => {
workflowsStore.getWorkflowRunData = {};
- const { nodeIssuesById } = useCanvasMapping({
+ const { nodes: mappedNodes } = useCanvasMapping({
nodes: ref(nodes),
connections: ref(connections),
workflowObject: ref(workflowObject) as Ref,
});
- expect(nodeIssuesById.value[node.id]).toEqual([]);
+ expect(mappedNodes.value[0]?.data?.issues).toEqual({
+ execution: [],
+ validation: [],
+ visible: false,
+ });
});
it('should handle execution errors', () => {
@@ -748,13 +753,17 @@ describe('useCanvasMapping', () => {
],
};
- const { nodeIssuesById } = useCanvasMapping({
+ const { nodes: mappedNodes } = useCanvasMapping({
nodes: ref(nodes),
connections: ref(connections),
workflowObject: ref(workflowObject) as Ref,
});
- expect(nodeIssuesById.value[node.id]).toEqual([`${errorMessage} (${errorDescription})`]);
+ expect(mappedNodes.value[0]?.data?.issues).toEqual({
+ execution: [`${errorMessage} (${errorDescription})`],
+ validation: [],
+ visible: true,
+ });
});
it('should handle execution error without description', () => {
@@ -780,13 +789,17 @@ describe('useCanvasMapping', () => {
],
};
- const { nodeIssuesById } = useCanvasMapping({
+ const { nodes: mappedNodes } = useCanvasMapping({
nodes: ref(nodes),
connections: ref(connections),
workflowObject: ref(workflowObject) as Ref,
});
- expect(nodeIssuesById.value[node.id]).toEqual([errorMessage]);
+ expect(mappedNodes.value[0]?.data?.issues).toEqual({
+ execution: [errorMessage],
+ validation: [],
+ visible: true,
+ });
});
it('should handle multiple execution errors', () => {
@@ -821,16 +834,17 @@ describe('useCanvasMapping', () => {
],
};
- const { nodeIssuesById } = useCanvasMapping({
+ const { nodes: mappedNodes } = useCanvasMapping({
nodes: ref(nodes),
connections: ref(connections),
workflowObject: ref(workflowObject) as Ref,
});
- expect(nodeIssuesById.value[node.id]).toEqual([
- 'Error 1 (Description 1)',
- 'Error 2 (Description 2)',
- ]);
+ expect(mappedNodes.value[0]?.data?.issues).toEqual({
+ execution: ['Error 1 (Description 1)', 'Error 2 (Description 2)'],
+ validation: [],
+ visible: true,
+ });
});
it('should handle node issues', () => {
@@ -847,15 +861,17 @@ describe('useCanvasMapping', () => {
workflowsStore.getWorkflowRunData = {};
- const { nodeIssuesById } = useCanvasMapping({
+ const { nodes: mappedNodes } = useCanvasMapping({
nodes: ref(nodes),
connections: ref(connections),
workflowObject: ref(workflowObject) as Ref,
});
- expect(nodeIssuesById.value[node.id]).toEqual([
- 'Node Type "n8n-nodes-base.set" is not known.',
- ]);
+ expect(mappedNodes.value[0]?.data?.issues).toEqual({
+ execution: [],
+ validation: ['Node Type "n8n-nodes-base.set" is not known.'],
+ visible: true,
+ });
});
it('should combine execution errors and node issues', () => {
@@ -885,16 +901,17 @@ describe('useCanvasMapping', () => {
],
};
- const { nodeIssuesById } = useCanvasMapping({
+ const { nodes: mappedNodes } = useCanvasMapping({
nodes: ref(nodes),
connections: ref(connections),
workflowObject: ref(workflowObject) as Ref,
});
- expect(nodeIssuesById.value[node.id]).toEqual([
- 'Execution error (Error description)',
- 'Node Type "n8n-nodes-base.set" is not known.',
- ]);
+ expect(mappedNodes.value[0]?.data?.issues).toEqual({
+ execution: ['Execution error (Error description)'],
+ validation: ['Node Type "n8n-nodes-base.set" is not known.'],
+ visible: true,
+ });
});
it('should handle multiple nodes with different issues', () => {
@@ -925,16 +942,22 @@ describe('useCanvasMapping', () => {
],
};
- const { nodeIssuesById } = useCanvasMapping({
+ const { nodes: mappedNodes } = useCanvasMapping({
nodes: ref(nodes),
connections: ref(connections),
workflowObject: ref(workflowObject) as Ref,
});
- expect(nodeIssuesById.value[node1.id]).toEqual([
- 'Node Type "n8n-nodes-base.set" is not known.',
- ]);
- expect(nodeIssuesById.value[node2.id]).toEqual(['Execution error (Error description)']);
+ expect(mappedNodes.value[0]?.data?.issues).toEqual({
+ execution: [],
+ validation: ['Node Type "n8n-nodes-base.set" is not known.'],
+ visible: true,
+ });
+ expect(mappedNodes.value[1]?.data?.issues).toEqual({
+ execution: ['Execution error (Error description)'],
+ validation: [],
+ visible: true,
+ });
});
});
diff --git a/packages/frontend/editor-ui/src/composables/useCanvasMapping.ts b/packages/frontend/editor-ui/src/composables/useCanvasMapping.ts
index 6b47b040ec..6d45e350ab 100644
--- a/packages/frontend/editor-ui/src/composables/useCanvasMapping.ts
+++ b/packages/frontend/editor-ui/src/composables/useCanvasMapping.ts
@@ -395,25 +395,35 @@ export function useCanvasMapping({
{ throttle: CANVAS_EXECUTION_DATA_THROTTLE_DURATION, immediate: true },
);
- const nodeIssuesById = computed(() =>
+ const nodeExecutionErrorsById = computed(() =>
nodes.value.reduce>((acc, node) => {
- const issues: string[] = [];
+ const executionErrors: string[] = [];
const nodeExecutionRunData = workflowsStore.getWorkflowRunData?.[node.name];
if (nodeExecutionRunData) {
nodeExecutionRunData.forEach((executionRunData) => {
if (executionRunData?.error) {
const { message, description } = executionRunData.error;
const issue = `${message}${description ? ` (${description})` : ''}`;
- issues.push(sanitizeHtml(issue));
+ executionErrors.push(sanitizeHtml(issue));
}
});
}
+ acc[node.id] = executionErrors;
+
+ return acc;
+ }, {}),
+ );
+
+ const nodeValidationErrorsById = computed(() =>
+ nodes.value.reduce>((acc, node) => {
+ const validationErrors: string[] = [];
+
if (node?.issues !== undefined) {
- issues.push(...nodeHelpers.nodeIssuesToString(node.issues, node));
+ validationErrors.push(...nodeHelpers.nodeIssuesToString(node.issues, node));
}
- acc[node.id] = issues;
+ acc[node.id] = validationErrors;
return acc;
}, {}),
@@ -421,15 +431,19 @@ export function useCanvasMapping({
const nodeHasIssuesById = computed(() =>
nodes.value.reduce>((acc, node) => {
+ const hasExecutionErrors = nodeExecutionErrorsById.value[node.id]?.length > 0;
+ const hasValidationErrors = nodeValidationErrorsById.value[node.id]?.length > 0;
+
if (['crashed', 'error'].includes(nodeExecutionStatusById.value[node.id])) {
acc[node.id] = true;
} else if (nodePinnedDataById.value[node.id]) {
acc[node.id] = false;
- } else if (node.issues && nodeHelpers.nodeIssuesToString(node.issues, node).length) {
+ } else if (hasValidationErrors) {
+ acc[node.id] = true;
+ } else if (hasExecutionErrors) {
acc[node.id] = true;
} else {
const tasks = workflowsStore.getWorkflowRunData?.[node.name] ?? [];
-
acc[node.id] = Boolean(tasks.at(-1)?.error);
}
@@ -605,7 +619,8 @@ export function useCanvasMapping({
[CanvasConnectionMode.Output]: outputConnections,
},
issues: {
- items: nodeIssuesById.value[node.id],
+ execution: nodeExecutionErrorsById.value[node.id],
+ validation: nodeValidationErrorsById.value[node.id],
visible: nodeHasIssuesById.value[node.id],
},
pinnedData: {
@@ -734,7 +749,6 @@ export function useCanvasMapping({
additionalNodePropertiesById,
nodeExecutionRunDataOutputMapById,
nodeExecutionWaitingForNextById,
- nodeIssuesById,
nodeHasIssuesById,
connections: mappedConnections,
nodes: mappedNodes,
diff --git a/packages/frontend/editor-ui/src/composables/useCanvasNode.test.ts b/packages/frontend/editor-ui/src/composables/useCanvasNode.test.ts
index 426088b5bb..9f2000d27a 100644
--- a/packages/frontend/editor-ui/src/composables/useCanvasNode.test.ts
+++ b/packages/frontend/editor-ui/src/composables/useCanvasNode.test.ts
@@ -53,7 +53,11 @@ describe('useCanvasNode', () => {
[CanvasConnectionMode.Input]: { '0': [] },
[CanvasConnectionMode.Output]: {},
},
- issues: { items: ['issue1'], visible: true },
+ issues: {
+ execution: ['execution_error1'],
+ validation: ['validation_error1'],
+ visible: true,
+ },
execution: { status: 'running', waiting: 'waiting', running: true },
runData: { outputMap: {}, iterations: 1, visible: true },
pinnedData: { count: 1, visible: true },
@@ -90,7 +94,7 @@ describe('useCanvasNode', () => {
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.issues.value).toEqual(['execution_error1', 'validation_error1']);
expect(result.hasIssues.value).toBe(true);
expect(result.executionStatus.value).toBe('running');
expect(result.executionWaiting.value).toBe('waiting');
diff --git a/packages/frontend/editor-ui/src/composables/useCanvasNode.ts b/packages/frontend/editor-ui/src/composables/useCanvasNode.ts
index f35aa578e8..e8af4dbc88 100644
--- a/packages/frontend/editor-ui/src/composables/useCanvasNode.ts
+++ b/packages/frontend/editor-ui/src/composables/useCanvasNode.ts
@@ -18,7 +18,7 @@ export function useCanvasNode() {
inputs: [],
outputs: [],
connections: { [CanvasConnectionMode.Input]: {}, [CanvasConnectionMode.Output]: {} },
- issues: { items: [], visible: false },
+ issues: { execution: [], validation: [], visible: false },
pinnedData: { count: 0, visible: false },
execution: {
running: false,
@@ -47,8 +47,12 @@ export function useCanvasNode() {
const pinnedDataCount = computed(() => data.value.pinnedData.count);
const hasPinnedData = computed(() => data.value.pinnedData.count > 0);
- const issues = computed(() => data.value.issues.items ?? []);
+ const issues = computed(() => [...data.value.issues.execution, ...data.value.issues.validation]);
+ const executionErrors = computed(() => data.value.issues.execution ?? []);
+ const validationErrors = computed(() => data.value.issues.validation ?? []);
const hasIssues = computed(() => data.value.issues.visible);
+ const hasExecutionErrors = computed(() => data.value.issues.execution.length > 0);
+ const hasValidationErrors = computed(() => data.value.issues.validation.length > 0);
const executionStatus = computed(() => data.value.execution.status);
const executionWaiting = computed(() => data.value.execution.waiting);
@@ -81,7 +85,11 @@ export function useCanvasNode() {
runDataOutputMap,
hasRunData,
issues,
+ executionErrors,
+ validationErrors,
hasIssues,
+ hasExecutionErrors,
+ hasValidationErrors,
executionStatus,
executionWaiting,
executionWaitingForNext,
diff --git a/packages/frontend/editor-ui/src/features/workflow-diff/useWorkflowDiff.test.ts b/packages/frontend/editor-ui/src/features/workflow-diff/useWorkflowDiff.test.ts
index c658ff3d40..b3d08fbf0b 100644
--- a/packages/frontend/editor-ui/src/features/workflow-diff/useWorkflowDiff.test.ts
+++ b/packages/frontend/editor-ui/src/features/workflow-diff/useWorkflowDiff.test.ts
@@ -37,7 +37,6 @@ vi.mock('@/composables/useCanvasMapping', () => ({
additionalNodePropertiesById: computed(() => ({})),
nodeExecutionRunDataOutputMapById: computed(() => ({})),
nodeExecutionWaitingForNextById: computed(() => ({})),
- nodeIssuesById: computed(() => ({})),
nodeHasIssuesById: computed(() => ({})),
nodes: computed(() => []),
connections: computed(() => []),
@@ -401,7 +400,6 @@ describe('useWorkflowDiff', () => {
additionalNodePropertiesById: computed(() => ({}) as Record>),
nodeExecutionRunDataOutputMapById: computed(() => ({}) as Record),
nodeExecutionWaitingForNextById: computed(() => ({}) as Record),
- nodeIssuesById: computed(() => ({}) as Record),
nodeHasIssuesById: computed(() => ({}) as Record),
nodes: computed(() => nodes as CanvasNode[]),
connections: computed(() => connections as CanvasConnection[]),
diff --git a/packages/frontend/editor-ui/src/types/canvas.ts b/packages/frontend/editor-ui/src/types/canvas.ts
index 0d2d56fe3d..6e674dd737 100644
--- a/packages/frontend/editor-ui/src/types/canvas.ts
+++ b/packages/frontend/editor-ui/src/types/canvas.ts
@@ -111,7 +111,8 @@ export interface CanvasNodeData {
[CanvasConnectionMode.Output]: INodeConnections;
};
issues: {
- items: string[];
+ execution: string[];
+ validation: string[];
visible: boolean;
};
pinnedData: {