fix: Show result of waiting execution on canvas after execution complete (#10815)

Co-authored-by: Shireen Missi <94372015+ShireenMissi@users.noreply.github.com>
Co-authored-by: Shireen Missi <shireen@n8n.io>
This commit is contained in:
Michael Kret
2024-10-03 15:09:37 +03:00
committed by GitHub
parent f9480e9f57
commit 90b4bfc472
24 changed files with 466 additions and 129 deletions

View File

@@ -4,6 +4,7 @@ import type {
IStartRunData,
IWorkflowDb,
} from '@/Interface';
import type {
IRunData,
IRunExecutionData,
@@ -13,18 +14,25 @@ import type {
StartNodeData,
IRun,
INode,
IDataObject,
} from 'n8n-workflow';
import { NodeConnectionType } from 'n8n-workflow';
import { useToast } from '@/composables/useToast';
import { useNodeHelpers } from '@/composables/useNodeHelpers';
import { CHAT_TRIGGER_NODE_TYPE, WORKFLOW_LM_CHAT_MODAL_KEY } from '@/constants';
import {
CHAT_TRIGGER_NODE_TYPE,
FORM_TRIGGER_NODE_TYPE,
WAIT_NODE_TYPE,
WORKFLOW_LM_CHAT_MODAL_KEY,
} from '@/constants';
import { useRootStore } from '@/stores/root.store';
import { useUIStore } from '@/stores/ui.store';
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import { useWorkflowsStore } from '@/stores/workflows.store';
import { displayForm } from '@/utils/executionUtils';
import { displayForm, openPopUpWindow } from '@/utils/executionUtils';
import { useExternalHooks } from '@/composables/useExternalHooks';
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
import type { useRouter } from 'vue-router';
@@ -35,6 +43,8 @@ import { useExecutionsStore } from '@/stores/executions.store';
import type { PushPayload } from '@n8n/api-types';
import { useLocalStorage } from '@vueuse/core';
const FORM_RELOAD = 'n8n_redirect_to_next_form_test_page';
export function useRunWorkflow(useRunWorkflowOpts: { router: ReturnType<typeof useRouter> }) {
const nodeHelpers = useNodeHelpers();
const workflowHelpers = useWorkflowHelpers({ router: useRunWorkflowOpts.router });
@@ -43,7 +53,6 @@ export function useRunWorkflow(useRunWorkflowOpts: { router: ReturnType<typeof u
const rootStore = useRootStore();
const uiStore = useUIStore();
const nodeTypesStore = useNodeTypesStore();
const workflowsStore = useWorkflowsStore();
const executionsStore = useExecutionsStore();
@@ -263,42 +272,21 @@ export function useRunWorkflow(useRunWorkflowOpts: { router: ReturnType<typeof u
const getTestUrl = (() => {
return (node: INode) => {
const nodeType = nodeTypesStore.getNodeType(node.type, node.typeVersion);
if (nodeType?.webhooks?.length) {
return workflowHelpers.getWebhookUrl(nodeType.webhooks[0], node, 'test');
}
return '';
return `${rootStore.formTestUrl}/${node.parameters.path}`;
};
})();
const shouldShowForm = (() => {
return (node: INode) => {
const workflowTriggerNodes = workflow
.getTriggerNodes()
.map((triggerNode) => triggerNode.name);
const showForm =
options.destinationNode === node.name ||
directParentNodes.includes(node.name) ||
workflowTriggerNodes.some((triggerNode) =>
workflowsStore.isNodeInOutgoingNodeConnections(triggerNode, node.name),
);
return showForm;
};
})();
displayForm({
nodes: workflowData.nodes,
runData: workflowsStore.getWorkflowExecution?.data?.resultData?.runData,
destinationNode: options.destinationNode,
pinData,
directParentNodes,
formWaitingUrl: rootStore.formWaitingUrl,
executionId: runWorkflowApiResponse.executionId,
source: options.source,
getTestUrl,
shouldShowForm,
});
try {
displayForm({
nodes: workflowData.nodes,
runData: workflowsStore.getWorkflowExecution?.data?.resultData?.runData,
destinationNode: options.destinationNode,
pinData,
directParentNodes,
source: options.source,
getTestUrl,
});
} catch (error) {}
await useExternalHooks().run('workflowRun.runWorkflow', {
nodeName: options.destinationNode,
@@ -313,6 +301,128 @@ export function useRunWorkflow(useRunWorkflowOpts: { router: ReturnType<typeof u
}
}
function getFormResumeUrl(node: INode, executionId: string) {
const { webhookSuffix } = (node.parameters.options ?? {}) as IDataObject;
const suffix = webhookSuffix && typeof webhookSuffix !== 'object' ? `/${webhookSuffix}` : '';
const testUrl = `${rootStore.formWaitingUrl}/${executionId}${suffix}`;
return testUrl;
}
async function runWorkflowResolvePending(options: {
destinationNode?: string;
triggerNode?: string;
nodeData?: ITaskData;
source?: string;
}): Promise<IExecutionPushResponse | undefined> {
let runWorkflowApiResponse = await runWorkflow(options);
let { executionId } = runWorkflowApiResponse || {};
const MAX_DELAY = 3000;
const waitForWebhook = async (): Promise<string> => {
return await new Promise<string>((resolve) => {
let delay = 300;
let timeoutId: NodeJS.Timeout | null = null;
const checkWebhook = async () => {
await useExternalHooks().run('workflowRun.runWorkflow', {
nodeName: options.destinationNode,
source: options.source,
});
if (workflowsStore.activeExecutionId) {
executionId = workflowsStore.activeExecutionId;
runWorkflowApiResponse = { executionId };
if (timeoutId) clearTimeout(timeoutId);
resolve(executionId);
}
delay = Math.min(delay * 1.1, MAX_DELAY);
timeoutId = setTimeout(checkWebhook, delay);
};
timeoutId = setTimeout(checkWebhook, delay);
});
};
if (!executionId) executionId = await waitForWebhook();
let isFormShown =
!options.destinationNode &&
workflowsStore.allNodes.some((node) => node.type === FORM_TRIGGER_NODE_TYPE);
const resolveWaitingNodesData = async (): Promise<void> => {
return await new Promise<void>((resolve) => {
let delay = 300;
let timeoutId: NodeJS.Timeout | null = null;
const processExecution = async () => {
await useExternalHooks().run('workflowRun.runWorkflow', {
nodeName: options.destinationNode,
source: options.source,
});
const execution = await workflowsStore.getExecution((executionId as string) || '');
localStorage.removeItem(FORM_RELOAD);
if (!execution || workflowsStore.workflowExecutionData === null) {
uiStore.removeActiveAction('workflowRunning');
if (timeoutId) clearTimeout(timeoutId);
resolve();
return;
}
if (
execution.finished ||
['error', 'canceled', 'crashed', 'success'].includes(execution.status)
) {
workflowsStore.setWorkflowExecutionData(execution);
if (timeoutId) clearTimeout(timeoutId);
resolve();
return;
}
if (execution.status === 'waiting' && execution.data?.waitTill) {
delete execution.data.resultData.runData[
execution.data.resultData.lastNodeExecuted as string
];
workflowsStore.setWorkflowExecutionRunData(execution.data);
const { lastNodeExecuted } = execution.data?.resultData || {};
const waitingNode = execution.workflowData.nodes.find((node) => {
return node.name === lastNodeExecuted;
});
if (
waitingNode &&
waitingNode.type === WAIT_NODE_TYPE &&
waitingNode.parameters.resume === 'form'
) {
const testUrl = getFormResumeUrl(waitingNode, executionId as string);
if (isFormShown) {
localStorage.setItem(FORM_RELOAD, testUrl);
} else {
isFormShown = true;
openPopUpWindow(testUrl);
}
}
}
delay = Math.min(delay * 1.1, MAX_DELAY);
timeoutId = setTimeout(processExecution, delay);
};
timeoutId = setTimeout(processExecution, delay);
});
};
await resolveWaitingNodesData();
return runWorkflowApiResponse;
}
function consolidateRunDataAndStartNodes(
directParentNodes: string[],
runData: IRunData | null,
@@ -433,6 +543,7 @@ export function useRunWorkflow(useRunWorkflowOpts: { router: ReturnType<typeof u
return {
consolidateRunDataAndStartNodes,
runWorkflow,
runWorkflowResolvePending,
runWorkflowApi,
stopCurrentExecution,
stopWaitingForWebhook,