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 0000000000000..333ec09c3126a
--- /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 17bc277cfcfc0..4edba7221b322 100644
--- a/packages/frontend/@n8n/design-system/src/components/N8nIcon/icons.ts
+++ b/packages/frontend/@n8n/design-system/src/components/N8nIcon/icons.ts
@@ -11,6 +11,7 @@ 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';
@@ -435,6 +436,7 @@ export const updatedIconSet = {
'node-dirty': NodeDirty,
'node-ellipsis': NodeEllipsis,
'node-error': NodeError,
+ '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 91591e648a094..d28f29ba9f057 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 ee1d7465633d8..4c9aec478cbc0 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 f8ed31a9d857c..dac69f6e5a471 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
@@ -25,7 +25,8 @@ const $style = useCssModule();
const {
hasPinnedData,
issues,
- hasIssues,
+ hasExecutionErrors,
+ hasValidationErrors,
executionStatus,
executionWaiting,
executionWaitingForNext,
@@ -66,12 +67,6 @@ const commonClasses = computed(() => [
-
-
-
[
@@ -95,6 +90,18 @@ const commonClasses = computed(() => [
+
+
+
+
+
+
+
+
@@ -152,7 +159,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 377b1a965d719..6c135e70ec768 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: {
@@ -721,7 +722,7 @@ describe('useCanvasMapping', () => {
workflowObject: ref(workflowObject) as Ref,
});
- expect(nodeIssuesById.value[node.id]).toEqual([]);
+ expect(nodeIssuesById.value[node.id]).toEqual({ execution: [], validation: [] });
});
it('should handle execution errors', () => {
@@ -754,7 +755,10 @@ describe('useCanvasMapping', () => {
workflowObject: ref(workflowObject) as Ref,
});
- expect(nodeIssuesById.value[node.id]).toEqual([`${errorMessage} (${errorDescription})`]);
+ expect(nodeIssuesById.value[node.id]).toEqual({
+ execution: [`${errorMessage} (${errorDescription})`],
+ validation: [],
+ });
});
it('should handle execution error without description', () => {
@@ -786,7 +790,10 @@ describe('useCanvasMapping', () => {
workflowObject: ref(workflowObject) as Ref,
});
- expect(nodeIssuesById.value[node.id]).toEqual([errorMessage]);
+ expect(nodeIssuesById.value[node.id]).toEqual({
+ execution: [errorMessage],
+ validation: [],
+ });
});
it('should handle multiple execution errors', () => {
@@ -827,10 +834,10 @@ describe('useCanvasMapping', () => {
workflowObject: ref(workflowObject) as Ref,
});
- expect(nodeIssuesById.value[node.id]).toEqual([
- 'Error 1 (Description 1)',
- 'Error 2 (Description 2)',
- ]);
+ expect(nodeIssuesById.value[node.id]).toEqual({
+ execution: ['Error 1 (Description 1)', 'Error 2 (Description 2)'],
+ validation: [],
+ });
});
it('should handle node issues', () => {
@@ -853,9 +860,10 @@ describe('useCanvasMapping', () => {
workflowObject: ref(workflowObject) as Ref,
});
- expect(nodeIssuesById.value[node.id]).toEqual([
- 'Node Type "n8n-nodes-base.set" is not known.',
- ]);
+ expect(nodeIssuesById.value[node.id]).toEqual({
+ execution: [],
+ validation: ['Node Type "n8n-nodes-base.set" is not known.'],
+ });
});
it('should combine execution errors and node issues', () => {
@@ -891,10 +899,10 @@ describe('useCanvasMapping', () => {
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(nodeIssuesById.value[node.id]).toEqual({
+ execution: ['Execution error (Error description)'],
+ validation: ['Node Type "n8n-nodes-base.set" is not known.'],
+ });
});
it('should handle multiple nodes with different issues', () => {
@@ -931,10 +939,14 @@ describe('useCanvasMapping', () => {
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(nodeIssuesById.value[node1.id]).toEqual({
+ execution: [],
+ validation: ['Node Type "n8n-nodes-base.set" is not known.'],
+ });
+ expect(nodeIssuesById.value[node2.id]).toEqual({
+ execution: ['Execution error (Error description)'],
+ validation: [],
+ });
});
});
diff --git a/packages/frontend/editor-ui/src/composables/useCanvasMapping.ts b/packages/frontend/editor-ui/src/composables/useCanvasMapping.ts
index 6b47b040ecce2..319a5eb610d43 100644
--- a/packages/frontend/editor-ui/src/composables/useCanvasMapping.ts
+++ b/packages/frontend/editor-ui/src/composables/useCanvasMapping.ts
@@ -395,41 +395,69 @@ 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;
}, {}),
);
+ const nodeIssuesById = computed(() =>
+ nodes.value.reduce>(
+ (acc, node) => {
+ acc[node.id] = {
+ execution: nodeExecutionErrorsById.value[node.id] ?? [],
+ validation: nodeValidationErrorsById.value[node.id] ?? [],
+ };
+
+ return acc;
+ },
+ {},
+ ),
+ );
+
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 +633,8 @@ export function useCanvasMapping({
[CanvasConnectionMode.Output]: outputConnections,
},
issues: {
- items: nodeIssuesById.value[node.id],
+ execution: nodeIssuesById.value[node.id].execution,
+ validation: nodeIssuesById.value[node.id].validation,
visible: nodeHasIssuesById.value[node.id],
},
pinnedData: {
diff --git a/packages/frontend/editor-ui/src/composables/useCanvasNode.test.ts b/packages/frontend/editor-ui/src/composables/useCanvasNode.test.ts
index 426088b5bb829..9f2000d27a412 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 f35aa578e82b9..e8af4dbc88d38 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 c658ff3d4068d..cc7f6a6443554 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,9 @@ vi.mock('@/composables/useCanvasMapping', () => ({
additionalNodePropertiesById: computed(() => ({})),
nodeExecutionRunDataOutputMapById: computed(() => ({})),
nodeExecutionWaitingForNextById: computed(() => ({})),
- nodeIssuesById: computed(() => ({})),
+ nodeIssuesById: computed(
+ () => ({}) as Record,
+ ),
nodeHasIssuesById: computed(() => ({})),
nodes: computed(() => []),
connections: computed(() => []),
@@ -401,7 +403,9 @@ describe('useWorkflowDiff', () => {
additionalNodePropertiesById: computed(() => ({}) as Record>),
nodeExecutionRunDataOutputMapById: computed(() => ({}) as Record),
nodeExecutionWaitingForNextById: computed(() => ({}) as Record),
- nodeIssuesById: 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 1e278cb864739..4bfee032673ff 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: {