false); // @TODO Implement this
font-size: 3.75em;
color: hsla(var(--color-primary-h), var(--color-primary-s), var(--color-primary-l), 0.7);
}
+.node-waiting-spinner {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 3.75em;
+ color: hsla(var(--color-primary-h), var(--color-primary-s), var(--color-primary-l), 0.7);
+ width: 100%;
+ height: 100%;
+ position: absolute;
+ left: -34px;
+ top: -34px;
+}
.issues {
color: var(--color-danger);
diff --git a/packages/editor-ui/src/composables/__tests__/useRunWorkflow.spec.ts b/packages/editor-ui/src/composables/__tests__/useRunWorkflow.spec.ts
index 4b6fde3991..0004354f50 100644
--- a/packages/editor-ui/src/composables/__tests__/useRunWorkflow.spec.ts
+++ b/packages/editor-ui/src/composables/__tests__/useRunWorkflow.spec.ts
@@ -6,7 +6,7 @@ import { ExpressionError, type IPinData, type IRunData, type Workflow } from 'n8
import { useRootStore } from '@/stores/root.store';
import { useRunWorkflow } from '@/composables/useRunWorkflow';
-import type { IStartRunData, IWorkflowData } from '@/Interface';
+import type { IExecutionResponse, IStartRunData, IWorkflowData } from '@/Interface';
import { useWorkflowsStore } from '@/stores/workflows.store';
import { useUIStore } from '@/stores/ui.store';
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
@@ -22,6 +22,7 @@ vi.mock('@/stores/workflows.store', () => ({
executionWaitingForWebhook: false,
getCurrentWorkflow: vi.fn().mockReturnValue({ id: '123' }),
getNodeByName: vi.fn(),
+ getExecution: vi.fn(),
}),
}));
@@ -303,4 +304,76 @@ describe('useRunWorkflow({ router })', () => {
expect(result.runData).toEqual(undefined);
});
});
+
+ describe('useRunWorkflow({ router }) - runWorkflowResolvePending', () => {
+ let uiStore: ReturnType
;
+ let workflowsStore: ReturnType;
+ let router: ReturnType;
+
+ beforeAll(() => {
+ const pinia = createTestingPinia({ stubActions: false });
+ setActivePinia(pinia);
+ rootStore = useRootStore();
+ uiStore = useUIStore();
+ workflowsStore = useWorkflowsStore();
+ router = useRouter();
+ workflowHelpers = useWorkflowHelpers({ router });
+ });
+
+ beforeEach(() => {
+ uiStore.activeActions = [];
+ vi.mocked(workflowsStore).runWorkflow.mockReset();
+ });
+
+ it('should resolve when runWorkflow finished', async () => {
+ const { runWorkflowResolvePending } = useRunWorkflow({ router });
+ const mockExecutionResponse = { executionId: '123' };
+
+ vi.mocked(workflowsStore).runWorkflow.mockResolvedValue(mockExecutionResponse);
+ vi.mocked(workflowsStore).allNodes = [];
+ vi.mocked(workflowsStore).getExecution.mockResolvedValue({
+ finished: true,
+ } as unknown as IExecutionResponse);
+ vi.mocked(workflowsStore).workflowExecutionData = {
+ id: '123',
+ } as unknown as IExecutionResponse;
+
+ const result = await runWorkflowResolvePending({});
+
+ expect(result).toEqual(mockExecutionResponse);
+ });
+
+ it('should return when workflowExecutionData is null', async () => {
+ const { runWorkflowResolvePending } = useRunWorkflow({ router });
+ const mockExecutionResponse = { executionId: '123' };
+
+ vi.mocked(workflowsStore).runWorkflow.mockResolvedValue(mockExecutionResponse);
+ vi.mocked(workflowsStore).allNodes = [];
+ vi.mocked(workflowsStore).getExecution.mockResolvedValue({
+ finished: true,
+ } as unknown as IExecutionResponse);
+ vi.mocked(workflowsStore).workflowExecutionData = null;
+
+ const result = await runWorkflowResolvePending({});
+
+ expect(result).toEqual(mockExecutionResponse);
+ });
+
+ it('should handle workflow execution error properly', async () => {
+ const { runWorkflowResolvePending } = useRunWorkflow({ router });
+ const mockExecutionResponse = { executionId: '123' };
+
+ vi.mocked(workflowsStore).runWorkflow.mockResolvedValue(mockExecutionResponse);
+ vi.mocked(workflowsStore).allNodes = [];
+ vi.mocked(workflowsStore).getExecution.mockResolvedValue({
+ finished: false,
+ status: 'error',
+ } as unknown as IExecutionResponse);
+
+ await runWorkflowResolvePending({});
+
+ expect(workflowsStore.setWorkflowExecutionData).toHaveBeenCalled();
+ expect(workflowsStore.workflowExecutionData).toBe(null);
+ });
+ });
});
diff --git a/packages/editor-ui/src/composables/useCanvasMapping.ts b/packages/editor-ui/src/composables/useCanvasMapping.ts
index bb2efc669d..8308cbfcbf 100644
--- a/packages/editor-ui/src/composables/useCanvasMapping.ts
+++ b/packages/editor-ui/src/composables/useCanvasMapping.ts
@@ -38,7 +38,12 @@ import type {
} from 'n8n-workflow';
import { NodeConnectionType, NodeHelpers } from 'n8n-workflow';
import type { INodeUi } from '@/Interface';
-import { CUSTOM_API_CALL_KEY, STICKY_NODE_TYPE, WAIT_TIME_UNLIMITED } from '@/constants';
+import {
+ CUSTOM_API_CALL_KEY,
+ STICKY_NODE_TYPE,
+ WAIT_NODE_TYPE,
+ WAIT_TIME_UNLIMITED,
+} from '@/constants';
import { sanitizeHtml } from '@/utils/htmlUtils';
import { MarkerType } from '@vue-flow/core';
import { useNodeHelpers } from './useNodeHelpers';
@@ -233,7 +238,8 @@ export function useCanvasMapping({
const nodeExecutionStatusById = computed(() =>
nodes.value.reduce>((acc, node) => {
acc[node.id] =
- workflowsStore.getWorkflowRunData?.[node.name]?.filter(Boolean)[0].executionStatus ?? 'new';
+ workflowsStore.getWorkflowRunData?.[node.name]?.filter(Boolean)[0]?.executionStatus ??
+ 'new';
return acc;
}, {}),
);
@@ -327,8 +333,21 @@ export function useCanvasMapping({
if (workflowExecution && lastNodeExecuted && isExecutionSummary(workflowExecution)) {
if (
node.name === workflowExecution.data?.resultData?.lastNodeExecuted &&
- workflowExecution.waitTill
+ workflowExecution?.waitTill &&
+ !workflowExecution?.finished
) {
+ if (
+ node &&
+ node.type === WAIT_NODE_TYPE &&
+ ['webhook', 'form'].includes(node.parameters.resume as string)
+ ) {
+ acc[node.id] =
+ node.parameters.resume === 'webhook'
+ ? i18n.baseText('node.theNodeIsWaitingWebhookCall')
+ : i18n.baseText('node.theNodeIsWaitingFormCall');
+ return acc;
+ }
+
const waitDate = new Date(workflowExecution.waitTill);
if (waitDate.toISOString() === WAIT_TIME_UNLIMITED) {
diff --git a/packages/editor-ui/src/composables/usePushConnection.ts b/packages/editor-ui/src/composables/usePushConnection.ts
index 4ced982610..6e1d993c0e 100644
--- a/packages/editor-ui/src/composables/usePushConnection.ts
+++ b/packages/editor-ui/src/composables/usePushConnection.ts
@@ -305,7 +305,6 @@ export function usePushConnection({ router }: { router: ReturnTypeTurn on saving manual executions and run again to see what happened after this node.';
- } else {
- action = `View the execution to see what happened after this node.`;
}
// Workflow did start but had been put to wait
workflowHelpers.setDocumentTitle(workflow.name as string, 'IDLE');
- toast.showToast({
- title: 'Workflow started waiting',
- message: `${action} More info`,
- type: 'success',
- duration: 0,
- dangerouslyUseHTMLString: true,
- });
} else if (runDataExecuted.finished !== true) {
workflowHelpers.setDocumentTitle(workflow.name as string, 'ERROR');
diff --git a/packages/editor-ui/src/composables/useRunWorkflow.ts b/packages/editor-ui/src/composables/useRunWorkflow.ts
index da0e0302d2..4d06d5b572 100644
--- a/packages/editor-ui/src/composables/useRunWorkflow.ts
+++ b/packages/editor-ui/src/composables/useRunWorkflow.ts
@@ -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 }) {
const nodeHelpers = useNodeHelpers();
const workflowHelpers = useWorkflowHelpers({ router: useRunWorkflowOpts.router });
@@ -43,7 +53,6 @@ export function useRunWorkflow(useRunWorkflowOpts: { router: ReturnType {
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 {
+ let runWorkflowApiResponse = await runWorkflow(options);
+ let { executionId } = runWorkflowApiResponse || {};
+
+ const MAX_DELAY = 3000;
+
+ const waitForWebhook = async (): Promise => {
+ return await new Promise((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 => {
+ return await new Promise((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: ReturnTypeTest node again to refresh output.",
"ndv.output.staleDataWarning.pinData": "Node parameter changes will not affect pinned output data.",
@@ -1011,6 +1014,8 @@
"node.nodeIsExecuting": "Node is executing",
"node.nodeIsWaitingTill": "Node is waiting until {date} {time}",
"node.theNodeIsWaitingIndefinitelyForAnIncomingWebhookCall": "The node is waiting for an incoming webhook call (indefinitely)",
+ "node.theNodeIsWaitingWebhookCall": "The node is waiting for an incoming webhook call",
+ "node.theNodeIsWaitingFormCall": "The node is waiting for a form submission",
"node.waitingForYouToCreateAnEventIn": "Waiting for you to create an event in {nodeType}",
"node.discovery.pinData.canvas": "You can pin this output instead of waiting for a test event. Open node to do so.",
"node.discovery.pinData.ndv": "You can pin this output instead of waiting for a test event.",
diff --git a/packages/editor-ui/src/stores/root.store.ts b/packages/editor-ui/src/stores/root.store.ts
index be9ba9080c..6e9bee10da 100644
--- a/packages/editor-ui/src/stores/root.store.ts
+++ b/packages/editor-ui/src/stores/root.store.ts
@@ -19,6 +19,7 @@ export const useRootStore = defineStore(STORES.ROOT, () => {
endpointFormWaiting: 'form-waiting',
endpointWebhook: 'webhook',
endpointWebhookTest: 'webhook-test',
+ endpointWebhookWaiting: 'webhook-waiting',
pushConnectionActive: true,
timezone: 'America/New_York',
executionTimeout: -1,
@@ -43,10 +44,16 @@ export const useRootStore = defineStore(STORES.ROOT, () => {
const formTestUrl = computed(() => `${state.value.urlBaseEditor}${state.value.endpointFormTest}`);
- const formWaitingUrl = computed(() => `${state.value.baseUrl}${state.value.endpointFormWaiting}`);
+ const formWaitingUrl = computed(
+ () => `${state.value.urlBaseEditor}${state.value.endpointFormWaiting}`,
+ );
const webhookUrl = computed(() => `${state.value.urlBaseWebhook}${state.value.endpointWebhook}`);
+ const webhookWaitingUrl = computed(
+ () => `${state.value.urlBaseEditor}${state.value.endpointWebhookWaiting}`,
+ );
+
const pushRef = computed(() => state.value.pushRef);
const binaryDataMode = computed(() => state.value.binaryDataMode);
@@ -131,6 +138,10 @@ export const useRootStore = defineStore(STORES.ROOT, () => {
state.value.endpointWebhookTest = endpointWebhookTest;
};
+ const setEndpointWebhookWaiting = (endpointWebhookWaiting: string) => {
+ state.value.endpointWebhookWaiting = endpointWebhookWaiting;
+ };
+
const setTimezone = (timezone: string) => {
state.value.timezone = timezone;
setGlobalState({ defaultTimezone: timezone });
@@ -177,6 +188,7 @@ export const useRootStore = defineStore(STORES.ROOT, () => {
formWaitingUrl,
webhookUrl,
webhookTestUrl,
+ webhookWaitingUrl,
restUrl,
restCloudApiContext,
restApiContext,
@@ -200,6 +212,7 @@ export const useRootStore = defineStore(STORES.ROOT, () => {
setEndpointFormWaiting,
setEndpointWebhook,
setEndpointWebhookTest,
+ setEndpointWebhookWaiting,
setTimezone,
setExecutionTimeout,
setMaxExecutionTimeout,
diff --git a/packages/editor-ui/src/stores/settings.store.ts b/packages/editor-ui/src/stores/settings.store.ts
index 390b21c0b5..d6abe54fce 100644
--- a/packages/editor-ui/src/stores/settings.store.ts
+++ b/packages/editor-ui/src/stores/settings.store.ts
@@ -235,6 +235,7 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, () => {
rootStore.setEndpointFormWaiting(fetchedSettings.endpointFormWaiting);
rootStore.setEndpointWebhook(fetchedSettings.endpointWebhook);
rootStore.setEndpointWebhookTest(fetchedSettings.endpointWebhookTest);
+ rootStore.setEndpointWebhookWaiting(fetchedSettings.endpointWebhookWaiting);
rootStore.setTimezone(fetchedSettings.timezone);
rootStore.setExecutionTimeout(fetchedSettings.executionTimeout);
rootStore.setMaxExecutionTimeout(fetchedSettings.maxExecutionTimeout);
diff --git a/packages/editor-ui/src/stores/workflows.store.ts b/packages/editor-ui/src/stores/workflows.store.ts
index 6a5cb221a6..9d4da95f61 100644
--- a/packages/editor-ui/src/stores/workflows.store.ts
+++ b/packages/editor-ui/src/stores/workflows.store.ts
@@ -7,6 +7,7 @@ import {
PLACEHOLDER_EMPTY_WORKFLOW_ID,
START_NODE_TYPE,
STORES,
+ WAIT_NODE_TYPE,
} from '@/constants';
import type {
ExecutionsQueryFilter,
@@ -166,6 +167,10 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
const allNodes = computed(() => workflow.value.nodes);
+ const isWaitingExecution = computed(() => {
+ return allNodes.value.some((node) => node.type === WAIT_NODE_TYPE && node.disabled !== true);
+ });
+
// Names of all nodes currently on canvas.
const canvasNames = computed(() => new Set(allNodes.value.map((n) => n.name)));
@@ -652,6 +657,11 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
}
function setWorkflowExecutionData(workflowResultData: IExecutionResponse | null) {
+ if (workflowResultData?.data?.waitTill) {
+ delete workflowResultData.data.resultData.runData[
+ workflowResultData.data.resultData.lastNodeExecuted as string
+ ];
+ }
workflowExecutionData.value = workflowResultData;
workflowExecutionPairedItemMappings.value = getPairedItemsMapping(workflowResultData);
}
@@ -1555,6 +1565,7 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
getWorkflowResultDataByNodeName,
allConnections,
allNodes,
+ isWaitingExecution,
canvasNames,
nodesByName,
nodesIssuesExist,
diff --git a/packages/editor-ui/src/utils/__tests__/executionUtils.spec.ts b/packages/editor-ui/src/utils/__tests__/executionUtils.spec.ts
index 3a5163c6fe..e1748a5bc0 100644
--- a/packages/editor-ui/src/utils/__tests__/executionUtils.spec.ts
+++ b/packages/editor-ui/src/utils/__tests__/executionUtils.spec.ts
@@ -15,7 +15,6 @@ vi.mock('../executionUtils', async () => {
describe('displayForm', () => {
const getTestUrlMock = vi.fn();
- const shouldShowFormMock = vi.fn();
beforeEach(() => {
vi.clearAllMocks();
@@ -50,11 +49,8 @@ describe('displayForm', () => {
pinData,
destinationNode: undefined,
directParentNodes: [],
- formWaitingUrl: 'http://example.com',
- executionId: undefined,
source: undefined,
getTestUrl: getTestUrlMock,
- shouldShowForm: shouldShowFormMock,
});
expect(openPopUpWindow).not.toHaveBeenCalled();
@@ -86,11 +82,8 @@ describe('displayForm', () => {
pinData: {},
destinationNode: 'Node3',
directParentNodes: ['Node4'],
- formWaitingUrl: 'http://example.com',
- executionId: '12345',
source: undefined,
getTestUrl: getTestUrlMock,
- shouldShowForm: shouldShowFormMock,
});
expect(openPopUpWindow).not.toHaveBeenCalled();
@@ -116,11 +109,8 @@ describe('displayForm', () => {
pinData: {},
destinationNode: undefined,
directParentNodes: [],
- formWaitingUrl: 'http://example.com',
- executionId: undefined,
source: 'RunData.ManualChatMessage',
getTestUrl: getTestUrlMock,
- shouldShowForm: shouldShowFormMock,
});
expect(openPopUpWindow).not.toHaveBeenCalled();
diff --git a/packages/editor-ui/src/utils/executionUtils.ts b/packages/editor-ui/src/utils/executionUtils.ts
index 90dba7d536..5c775c7608 100644
--- a/packages/editor-ui/src/utils/executionUtils.ts
+++ b/packages/editor-ui/src/utils/executionUtils.ts
@@ -1,7 +1,10 @@
import type { ExecutionStatus, IDataObject, INode, IPinData, IRunData } from 'n8n-workflow';
import type { ExecutionFilterType, ExecutionsQueryFilter } from '@/Interface';
import { isEmpty } from '@/utils/typesUtils';
-import { FORM_TRIGGER_NODE_TYPE, WAIT_NODE_TYPE } from '../constants';
+import { FORM_TRIGGER_NODE_TYPE } from '../constants';
+import { useWorkflowsStore } from '@/stores/workflows.store';
+import { useRootStore } from '@/stores/root.store';
+import { i18n } from '@/plugins/i18n';
export function getDefaultExecutionFilters(): ExecutionFilterType {
return {
@@ -79,15 +82,15 @@ export const openPopUpWindow = (
const windowWidth = window.innerWidth;
const smallScreen = windowWidth <= 800;
if (options?.alwaysInNewTab || smallScreen) {
- window.open(url, '_blank');
+ return window.open(url, '_blank');
} else {
const height = options?.width || 700;
const width = options?.height || window.innerHeight - 50;
const left = (window.innerWidth - height) / 2;
const top = 50;
const features = `width=${height},height=${width},left=${left},top=${top},resizable=yes,scrollbars=yes`;
-
- window.open(url, '_blank', features);
+ const windowName = `form-waiting-since-${Date.now()}`;
+ return window.open(url, windowName, features);
}
};
@@ -97,57 +100,69 @@ export function displayForm({
pinData,
destinationNode,
directParentNodes,
- formWaitingUrl,
- executionId,
source,
getTestUrl,
- shouldShowForm,
}: {
nodes: INode[];
runData: IRunData | undefined;
pinData: IPinData;
destinationNode: string | undefined;
directParentNodes: string[];
- formWaitingUrl: string;
- executionId: string | undefined;
source: string | undefined;
getTestUrl: (node: INode) => string;
- shouldShowForm: (node: INode) => boolean;
}) {
for (const node of nodes) {
const hasNodeRun = runData && runData?.hasOwnProperty(node.name);
if (hasNodeRun || pinData[node.name]) continue;
- if (![FORM_TRIGGER_NODE_TYPE, WAIT_NODE_TYPE].includes(node.type)) {
- continue;
- }
+ if (![FORM_TRIGGER_NODE_TYPE].includes(node.type)) continue;
- if (
- destinationNode &&
- destinationNode !== node.name &&
- !directParentNodes.includes(node.name)
- ) {
+ if (destinationNode && destinationNode !== node.name && !directParentNodes.includes(node.name))
continue;
- }
if (node.name === destinationNode || !node.disabled) {
let testUrl = '';
-
- if (node.type === FORM_TRIGGER_NODE_TYPE) {
- testUrl = getTestUrl(node);
- }
-
- if (node.type === WAIT_NODE_TYPE && node.parameters.resume === 'form' && executionId) {
- if (!shouldShowForm(node)) continue;
-
- const { webhookSuffix } = (node.parameters.options ?? {}) as IDataObject;
- const suffix =
- webhookSuffix && typeof webhookSuffix !== 'object' ? `/${webhookSuffix}` : '';
- testUrl = `${formWaitingUrl}/${executionId}${suffix}`;
- }
-
+ if (node.type === FORM_TRIGGER_NODE_TYPE) testUrl = getTestUrl(node);
if (testUrl && source !== 'RunData.ManualChatMessage') openPopUpWindow(testUrl);
}
}
}
+
+export const waitingNodeTooltip = () => {
+ try {
+ const lastNode =
+ useWorkflowsStore().workflowExecutionData?.data?.executionData?.nodeExecutionStack[0]?.node;
+ const resume = lastNode?.parameters?.resume;
+
+ if (resume) {
+ if (!['webhook', 'form'].includes(resume as string)) {
+ return i18n.baseText('ndv.output.waitNodeWaiting');
+ }
+
+ const { webhookSuffix } = (lastNode.parameters.options ?? {}) as { webhookSuffix: string };
+ const suffix = webhookSuffix && typeof webhookSuffix !== 'object' ? `/${webhookSuffix}` : '';
+
+ let message = '';
+ let resumeUrl = '';
+
+ if (resume === 'form') {
+ resumeUrl = `${useRootStore().formWaitingUrl}/${useWorkflowsStore().activeExecutionId}${suffix}`;
+ message = i18n.baseText('ndv.output.waitNodeWaitingForFormSubmission');
+ }
+
+ if (resume === 'webhook') {
+ resumeUrl = `${useRootStore().webhookWaitingUrl}/${useWorkflowsStore().activeExecutionId}${suffix}`;
+ message = i18n.baseText('ndv.output.waitNodeWaitingForWebhook');
+ }
+
+ if (message && resumeUrl) {
+ return `${message}${resumeUrl}`;
+ }
+ }
+ } catch (error) {
+ // do not throw error if could not compose tooltip
+ }
+
+ return '';
+};
diff --git a/packages/editor-ui/src/views/NodeView.v2.vue b/packages/editor-ui/src/views/NodeView.v2.vue
index b8d880d9bc..cafcdc6880 100644
--- a/packages/editor-ui/src/views/NodeView.v2.vue
+++ b/packages/editor-ui/src/views/NodeView.v2.vue
@@ -153,7 +153,8 @@ const { addBeforeUnloadEventBindings, removeBeforeUnloadEventBindings } = useBef
route,
});
const { registerCustomAction, unregisterCustomAction } = useGlobalLinkActions();
-const { runWorkflow, stopCurrentExecution, stopWaitingForWebhook } = useRunWorkflow({ router });
+const { runWorkflow, runWorkflowResolvePending, stopCurrentExecution, stopWaitingForWebhook } =
+ useRunWorkflow({ router });
const {
updateNodePosition,
updateNodesPosition,
@@ -950,7 +951,14 @@ const projectPermissions = computed(() => {
const isStoppingExecution = ref(false);
-const isWorkflowRunning = computed(() => uiStore.isActionActive.workflowRunning);
+const isWorkflowRunning = computed(() => {
+ if (uiStore.isActionActive.workflowRunning) return true;
+ if (workflowsStore.activeExecutionId) {
+ const execution = workflowsStore.getWorkflowExecution;
+ if (execution && execution.status === 'waiting' && !execution.finished) return true;
+ }
+ return false;
+});
const isExecutionWaitingForWebhook = computed(() => workflowsStore.executionWaitingForWebhook);
const isExecutionDisabled = computed(() => {
@@ -985,7 +993,11 @@ const workflowExecutionData = computed(() => workflowsStore.workflowExecutionDat
async function onRunWorkflow() {
trackRunWorkflow();
- await runWorkflow({});
+ if (!isExecutionPreview.value && workflowsStore.isWaitingExecution) {
+ void runWorkflowResolvePending({});
+ } else {
+ void runWorkflow({});
+ }
}
function trackRunWorkflow() {
@@ -1010,7 +1022,12 @@ async function onRunWorkflowToNode(id: string) {
if (!node) return;
trackRunWorkflowToNode(node);
- await runWorkflow({ destinationNode: node.name, source: 'Node.executeNode' });
+
+ if (!isExecutionPreview.value && workflowsStore.isWaitingExecution) {
+ void runWorkflowResolvePending({ destinationNode: node.name, source: 'Node.executeNode' });
+ } else {
+ void runWorkflow({ destinationNode: node.name, source: 'Node.executeNode' });
+ }
}
function trackRunWorkflowToNode(node: INodeUi) {
diff --git a/packages/editor-ui/src/views/NodeView.vue b/packages/editor-ui/src/views/NodeView.vue
index 5f8903f28e..a662941804 100644
--- a/packages/editor-ui/src/views/NodeView.vue
+++ b/packages/editor-ui/src/views/NodeView.vue
@@ -234,7 +234,9 @@ export default defineComponent({
const { callDebounced } = useDebounce();
const canvasPanning = useCanvasPanning(nodeViewRootRef, { onMouseMoveEnd });
const workflowHelpers = useWorkflowHelpers({ router });
- const { runWorkflow, stopCurrentExecution } = useRunWorkflow({ router });
+ const { runWorkflow, stopCurrentExecution, runWorkflowResolvePending } = useRunWorkflow({
+ router,
+ });
const { addBeforeUnloadEventBindings, removeBeforeUnloadEventBindings } = useBeforeUnload({
route,
});
@@ -254,6 +256,7 @@ export default defineComponent({
onMouseMoveEnd,
workflowHelpers,
runWorkflow,
+ runWorkflowResolvePending,
stopCurrentExecution,
callDebounced,
...useCanvasMouseSelect(),
@@ -422,7 +425,12 @@ export default defineComponent({
return this.workflowsStore.getWorkflowExecution;
},
workflowRunning(): boolean {
- return this.uiStore.isActionActive.workflowRunning;
+ if (this.uiStore.isActionActive.workflowRunning) return true;
+ if (this.workflowsStore.activeExecutionId) {
+ const execution = this.workflowsStore.getWorkflowExecution;
+ if (execution && execution.status === 'waiting' && !execution.finished) return true;
+ }
+ return false;
},
currentWorkflow(): string {
return this.$route.params.name?.toString() || this.workflowsStore.workflowId;
@@ -847,7 +855,12 @@ export default defineComponent({
};
this.$telemetry.track('User clicked execute node button', telemetryPayload);
void this.externalHooks.run('nodeView.onRunNode', telemetryPayload);
- void this.runWorkflow({ destinationNode: nodeName, source });
+
+ if (!this.isExecutionPreview && this.workflowsStore.isWaitingExecution) {
+ void this.runWorkflowResolvePending({ destinationNode: nodeName, source });
+ } else {
+ void this.runWorkflow({ destinationNode: nodeName, source });
+ }
},
async onOpenChat() {
const telemetryPayload = {
@@ -857,6 +870,7 @@ export default defineComponent({
void this.externalHooks.run('nodeView.onOpenChat', telemetryPayload);
this.uiStore.openModal(WORKFLOW_LM_CHAT_MODAL_KEY);
},
+
async onRunWorkflow() {
void this.workflowHelpers.getWorkflowDataToSave().then((workflowData) => {
const telemetryPayload = {
@@ -873,7 +887,12 @@ export default defineComponent({
void this.externalHooks.run('nodeView.onRunWorkflow', telemetryPayload);
});
- await this.runWorkflow({});
+ if (!this.isExecutionPreview && this.workflowsStore.isWaitingExecution) {
+ void this.runWorkflowResolvePending({});
+ } else {
+ void this.runWorkflow({});
+ }
+
this.refreshEndpointsErrorsState();
},
resetEndpointsErrors() {
@@ -932,7 +951,7 @@ export default defineComponent({
dangerouslyUseHTMLString: true,
});
},
- clearExecutionData() {
+ async clearExecutionData() {
this.workflowsStore.workflowExecutionData = null;
this.nodeHelpers.updateNodesExecutionIssues();
},
diff --git a/packages/nodes-base/nodes/Wait/Wait.node.ts b/packages/nodes-base/nodes/Wait/Wait.node.ts
index 5da813981b..43c092beed 100644
--- a/packages/nodes-base/nodes/Wait/Wait.node.ts
+++ b/packages/nodes-base/nodes/Wait/Wait.node.ts
@@ -238,14 +238,6 @@ export class Wait extends Webhook {
inputs: [NodeConnectionType.Main],
outputs: [NodeConnectionType.Main],
credentials: credentialsProperty(this.authPropertyName),
- hints: [
- {
- message:
- "When testing your workflow using the Editor UI, you can't see the rest of the execution following the Wait node. To inspect the execution results, enable Save Manual Executions in your Workflow settings so you can review the execution results there.",
- location: 'outputPane',
- whenToDisplay: 'beforeExecution',
- },
- ],
webhooks: [
{
...defaultWebhookDescription,