mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-18 10:31:15 +00:00
feat(editor): Separate node execution and validation error states (#19029)
This commit is contained in:
@@ -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 = {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(() => [
|
||||
<N8nIcon icon="clock" :size="size" />
|
||||
</N8nTooltip>
|
||||
</div>
|
||||
<div
|
||||
v-if="spinnerLayout === 'absolute'"
|
||||
:class="[...commonClasses, $style['node-waiting-spinner']]"
|
||||
>
|
||||
<N8nIcon icon="refresh-cw" spin />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="isNodeExecuting"
|
||||
@@ -84,15 +80,27 @@ const commonClasses = computed(() => [
|
||||
<N8nIcon icon="power" :size="size" />
|
||||
</div>
|
||||
<div
|
||||
v-else-if="hasIssues && !hideNodeIssues"
|
||||
v-else-if="hasExecutionErrors && !hideNodeIssues"
|
||||
:class="[...commonClasses, $style.issues]"
|
||||
data-test-id="node-issues"
|
||||
>
|
||||
<N8nTooltip :show-after="500" placement="bottom">
|
||||
<template #content>
|
||||
<TitledList :title="`${i18n.baseText('node.issues')}:`" :items="issues" />
|
||||
<TitledList :title="`${i18n.baseText('node.issues')}:`" :items="executionErrors" />
|
||||
</template>
|
||||
<N8nIcon icon="node-error" :size="size" />
|
||||
<N8nIcon icon="node-execution-error" :size="size" />
|
||||
</N8nTooltip>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="hasValidationErrors && !hideNodeIssues"
|
||||
:class="[...commonClasses, $style.issues]"
|
||||
data-test-id="node-issues"
|
||||
>
|
||||
<N8nTooltip :show-after="500" placement="bottom">
|
||||
<template #content>
|
||||
<TitledList :title="`${i18n.baseText('node.issues')}:`" :items="validationErrors" />
|
||||
</template>
|
||||
<N8nIcon icon="node-validation-error" :size="size" />
|
||||
</N8nTooltip>
|
||||
</div>
|
||||
<div v-else-if="executionStatus === 'unknown'">
|
||||
@@ -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));
|
||||
|
||||
|
||||
@@ -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<Workflow>,
|
||||
});
|
||||
|
||||
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<Workflow>,
|
||||
});
|
||||
|
||||
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<Workflow>,
|
||||
});
|
||||
|
||||
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<Workflow>,
|
||||
});
|
||||
|
||||
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<Workflow>,
|
||||
});
|
||||
|
||||
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<Workflow>,
|
||||
});
|
||||
|
||||
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<Workflow>,
|
||||
});
|
||||
|
||||
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,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -395,25 +395,35 @@ export function useCanvasMapping({
|
||||
{ throttle: CANVAS_EXECUTION_DATA_THROTTLE_DURATION, immediate: true },
|
||||
);
|
||||
|
||||
const nodeIssuesById = computed(() =>
|
||||
const nodeExecutionErrorsById = computed(() =>
|
||||
nodes.value.reduce<Record<string, string[]>>((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<Record<string, string[]>>((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<Record<string, boolean>>((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,
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<string, Partial<CanvasNode>>),
|
||||
nodeExecutionRunDataOutputMapById: computed(() => ({}) as Record<string, ExecutionOutputMap>),
|
||||
nodeExecutionWaitingForNextById: computed(() => ({}) as Record<string, boolean>),
|
||||
nodeIssuesById: computed(() => ({}) as Record<string, string[]>),
|
||||
nodeHasIssuesById: computed(() => ({}) as Record<string, boolean>),
|
||||
nodes: computed(() => nodes as CanvasNode[]),
|
||||
connections: computed(() => connections as CanvasConnection[]),
|
||||
|
||||
@@ -111,7 +111,8 @@ export interface CanvasNodeData {
|
||||
[CanvasConnectionMode.Output]: INodeConnections;
|
||||
};
|
||||
issues: {
|
||||
items: string[];
|
||||
execution: string[];
|
||||
validation: string[];
|
||||
visible: boolean;
|
||||
};
|
||||
pinnedData: {
|
||||
|
||||
Reference in New Issue
Block a user