feat(editor): Add execute workflow functionality and statuses to new canvas (no-changelog) (#9902)
Signed-off-by: Oleg Ivaniv <me@olegivaniv.com> Co-authored-by: Oleg Ivaniv <me@olegivaniv.com>
This commit is contained in:
@@ -1,9 +1,16 @@
|
||||
/**
|
||||
* Canvas V2 Only
|
||||
* @TODO Remove this notice when Canvas V2 is the only one in use
|
||||
*/
|
||||
|
||||
import { useI18n } from '@/composables/useI18n';
|
||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import type { Ref } from 'vue';
|
||||
import { computed } from 'vue';
|
||||
import type {
|
||||
CanvasConnection,
|
||||
CanvasConnectionData,
|
||||
CanvasConnectionPort,
|
||||
CanvasElement,
|
||||
CanvasElementData,
|
||||
@@ -12,9 +19,17 @@ import {
|
||||
mapLegacyConnectionsToCanvasConnections,
|
||||
mapLegacyEndpointsToCanvasConnectionPort,
|
||||
} from '@/utils/canvasUtilsV2';
|
||||
import type { Workflow } from 'n8n-workflow';
|
||||
import type {
|
||||
ExecutionStatus,
|
||||
ExecutionSummary,
|
||||
INodeExecutionData,
|
||||
ITaskData,
|
||||
Workflow,
|
||||
} from 'n8n-workflow';
|
||||
import { NodeHelpers } from 'n8n-workflow';
|
||||
import type { IWorkflowDb } from '@/Interface';
|
||||
import { WAIT_TIME_UNLIMITED } from '@/constants';
|
||||
import { sanitizeHtml } from '@/utils/htmlUtils';
|
||||
|
||||
export function useCanvasMapping({
|
||||
workflow,
|
||||
@@ -23,7 +38,8 @@ export function useCanvasMapping({
|
||||
workflow: Ref<IWorkflowDb>;
|
||||
workflowObject: Ref<Workflow>;
|
||||
}) {
|
||||
const locale = useI18n();
|
||||
const i18n = useI18n();
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const nodeTypesStore = useNodeTypesStore();
|
||||
|
||||
const renderTypeByNodeType = computed(
|
||||
@@ -87,6 +103,97 @@ export function useCanvasMapping({
|
||||
}, {}),
|
||||
);
|
||||
|
||||
const nodePinnedDataById = computed(() =>
|
||||
workflow.value.nodes.reduce<Record<string, INodeExecutionData[] | undefined>>((acc, node) => {
|
||||
acc[node.id] = workflowsStore.pinDataByNodeName(node.name);
|
||||
return acc;
|
||||
}, {}),
|
||||
);
|
||||
|
||||
const nodeExecutionStatusById = computed(() =>
|
||||
workflow.value.nodes.reduce<Record<string, ExecutionStatus>>((acc, node) => {
|
||||
acc[node.id] =
|
||||
workflowsStore.getWorkflowRunData?.[node.name]?.filter(Boolean)[0].executionStatus ?? 'new';
|
||||
return acc;
|
||||
}, {}),
|
||||
);
|
||||
|
||||
const nodeExecutionRunDataById = computed(() =>
|
||||
workflow.value.nodes.reduce<Record<string, ITaskData[] | null>>((acc, node) => {
|
||||
acc[node.id] = workflowsStore.getWorkflowResultDataByNodeName(node.name);
|
||||
return acc;
|
||||
}, {}),
|
||||
);
|
||||
|
||||
const nodeIssuesById = computed(() =>
|
||||
workflow.value.nodes.reduce<Record<string, string[]>>((acc, node) => {
|
||||
const issues: 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));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (node?.issues !== undefined) {
|
||||
issues.push(...NodeHelpers.nodeIssuesToString(node.issues, node));
|
||||
}
|
||||
|
||||
acc[node.id] = issues;
|
||||
|
||||
return acc;
|
||||
}, {}),
|
||||
);
|
||||
|
||||
const nodeHasIssuesById = computed(() =>
|
||||
workflow.value.nodes.reduce<Record<string, boolean>>((acc, node) => {
|
||||
if (['crashed', 'error'].includes(nodeExecutionStatusById.value[node.id])) {
|
||||
acc[node.id] = true;
|
||||
} else if (nodePinnedDataById.value[node.id]) {
|
||||
acc[node.id] = false;
|
||||
} else {
|
||||
acc[node.id] = Object.keys(node?.issues ?? {}).length > 0;
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, {}),
|
||||
);
|
||||
|
||||
const nodeExecutionWaitingById = computed(() =>
|
||||
workflow.value.nodes.reduce<Record<string, string | undefined>>((acc, node) => {
|
||||
const isExecutionSummary = (execution: object): execution is ExecutionSummary =>
|
||||
'waitTill' in execution;
|
||||
|
||||
const workflowExecution = workflowsStore.getWorkflowExecution;
|
||||
const lastNodeExecuted = workflowExecution?.data?.resultData?.lastNodeExecuted;
|
||||
|
||||
if (workflowExecution && lastNodeExecuted && isExecutionSummary(workflowExecution)) {
|
||||
if (node.name === workflowExecution.data?.resultData?.lastNodeExecuted) {
|
||||
const waitDate = new Date(workflowExecution.waitTill as Date);
|
||||
|
||||
if (waitDate.toISOString() === WAIT_TIME_UNLIMITED) {
|
||||
acc[node.id] = i18n.baseText(
|
||||
'node.theNodeIsWaitingIndefinitelyForAnIncomingWebhookCall',
|
||||
);
|
||||
}
|
||||
|
||||
acc[node.id] = i18n.baseText('node.nodeIsWaitingTill', {
|
||||
interpolate: {
|
||||
date: waitDate.toLocaleDateString(),
|
||||
time: waitDate.toLocaleTimeString(),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, {}),
|
||||
);
|
||||
|
||||
const elements = computed<CanvasElement[]>(() => [
|
||||
...workflow.value.nodes.map<CanvasElement>((node) => {
|
||||
const inputConnections = workflowObject.value.connectionsByDestinationNode[node.name] ?? {};
|
||||
@@ -103,6 +210,22 @@ export function useCanvasMapping({
|
||||
input: inputConnections,
|
||||
output: outputConnections,
|
||||
},
|
||||
issues: {
|
||||
items: nodeIssuesById.value[node.id],
|
||||
visible: nodeHasIssuesById.value[node.id],
|
||||
},
|
||||
pinnedData: {
|
||||
count: nodePinnedDataById.value[node.id]?.length ?? 0,
|
||||
visible: !!nodePinnedDataById.value[node.id],
|
||||
},
|
||||
execution: {
|
||||
status: nodeExecutionStatusById.value[node.id],
|
||||
waiting: nodeExecutionWaitingById.value[node.id],
|
||||
},
|
||||
runData: {
|
||||
count: nodeExecutionRunDataById.value[node.id]?.length ?? 0,
|
||||
visible: !!nodeExecutionRunDataById.value[node.id],
|
||||
},
|
||||
renderType: renderTypeByNodeType.value[node.type] ?? 'default',
|
||||
};
|
||||
|
||||
@@ -125,26 +248,63 @@ export function useCanvasMapping({
|
||||
return mappedConnections.map((connection) => {
|
||||
const type = getConnectionType(connection);
|
||||
const label = getConnectionLabel(connection);
|
||||
const data = getConnectionData(connection);
|
||||
|
||||
return {
|
||||
...connection,
|
||||
data,
|
||||
type,
|
||||
label,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
function getConnectionData(connection: CanvasConnection): CanvasConnectionData {
|
||||
const fromNode = workflow.value.nodes.find(
|
||||
(node) => node.name === connection.data?.fromNodeName,
|
||||
);
|
||||
|
||||
let status: CanvasConnectionData['status'];
|
||||
if (fromNode) {
|
||||
if (nodePinnedDataById.value[fromNode.id] && nodeExecutionRunDataById.value[fromNode.id]) {
|
||||
status = 'pinned';
|
||||
} else if (nodeHasIssuesById.value[fromNode.id]) {
|
||||
status = 'error';
|
||||
} else if (nodeExecutionRunDataById.value[fromNode.id]) {
|
||||
status = 'success';
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...(connection.data as CanvasConnectionData),
|
||||
status,
|
||||
};
|
||||
}
|
||||
|
||||
function getConnectionType(_: CanvasConnection): string {
|
||||
return 'canvas-edge';
|
||||
}
|
||||
|
||||
function getConnectionLabel(connection: CanvasConnection): string {
|
||||
const pinData = workflow.value.pinData?.[connection.data?.fromNodeName ?? ''];
|
||||
const fromNode = workflow.value.nodes.find(
|
||||
(node) => node.name === connection.data?.fromNodeName,
|
||||
);
|
||||
|
||||
if (pinData?.length) {
|
||||
return locale.baseText('ndv.output.items', {
|
||||
adjustToNumber: pinData.length,
|
||||
interpolate: { count: String(pinData.length) },
|
||||
if (!fromNode) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (nodePinnedDataById.value[fromNode.id]) {
|
||||
const pinnedDataCount = nodePinnedDataById.value[fromNode.id]?.length ?? 0;
|
||||
return i18n.baseText('ndv.output.items', {
|
||||
adjustToNumber: pinnedDataCount,
|
||||
interpolate: { count: String(pinnedDataCount) },
|
||||
});
|
||||
} else if (nodeExecutionRunDataById.value[fromNode.id]) {
|
||||
const runDataCount = nodeExecutionRunDataById.value[fromNode.id]?.length ?? 0;
|
||||
return i18n.baseText('ndv.output.items', {
|
||||
adjustToNumber: runDataCount,
|
||||
interpolate: { count: String(runDataCount) },
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user