mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
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:
parent
f9480e9f57
commit
90b4bfc472
|
@ -33,6 +33,7 @@ export interface FrontendSettings {
|
||||||
endpointFormWaiting: string;
|
endpointFormWaiting: string;
|
||||||
endpointWebhook: string;
|
endpointWebhook: string;
|
||||||
endpointWebhookTest: string;
|
endpointWebhookTest: string;
|
||||||
|
endpointWebhookWaiting: string;
|
||||||
saveDataErrorExecution: WorkflowSettings.SaveDataExecution;
|
saveDataErrorExecution: WorkflowSettings.SaveDataExecution;
|
||||||
saveDataSuccessExecution: WorkflowSettings.SaveDataExecution;
|
saveDataSuccessExecution: WorkflowSettings.SaveDataExecution;
|
||||||
saveManualExecutions: boolean;
|
saveManualExecutions: boolean;
|
||||||
|
|
|
@ -88,6 +88,7 @@ export class FrontendService {
|
||||||
endpointFormWaiting: this.globalConfig.endpoints.formWaiting,
|
endpointFormWaiting: this.globalConfig.endpoints.formWaiting,
|
||||||
endpointWebhook: this.globalConfig.endpoints.webhook,
|
endpointWebhook: this.globalConfig.endpoints.webhook,
|
||||||
endpointWebhookTest: this.globalConfig.endpoints.webhookTest,
|
endpointWebhookTest: this.globalConfig.endpoints.webhookTest,
|
||||||
|
endpointWebhookWaiting: this.globalConfig.endpoints.webhookWaiting,
|
||||||
saveDataErrorExecution: config.getEnv('executions.saveDataOnError'),
|
saveDataErrorExecution: config.getEnv('executions.saveDataOnError'),
|
||||||
saveDataSuccessExecution: config.getEnv('executions.saveDataOnSuccess'),
|
saveDataSuccessExecution: config.getEnv('executions.saveDataOnSuccess'),
|
||||||
saveManualExecutions: config.getEnv('executions.saveDataManualExecutions'),
|
saveManualExecutions: config.getEnv('executions.saveDataManualExecutions'),
|
||||||
|
|
|
@ -735,6 +735,14 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
|
}).then(() => {
|
||||||
|
window.addEventListener('storage', function(event) {
|
||||||
|
if (event.key === 'n8n_redirect_to_next_form_test_page' && event.newValue) {
|
||||||
|
const newUrl = event.newValue;
|
||||||
|
localStorage.removeItem('n8n_redirect_to_next_form_test_page');
|
||||||
|
window.location.replace(newUrl);
|
||||||
|
}
|
||||||
|
});
|
||||||
})
|
})
|
||||||
.catch(function (error) {
|
.catch(function (error) {
|
||||||
console.error('Error:', error);
|
console.error('Error:', error);
|
||||||
|
|
|
@ -874,6 +874,7 @@ export interface RootState {
|
||||||
endpointFormWaiting: string;
|
endpointFormWaiting: string;
|
||||||
endpointWebhook: string;
|
endpointWebhook: string;
|
||||||
endpointWebhookTest: string;
|
endpointWebhookTest: string;
|
||||||
|
endpointWebhookWaiting: string;
|
||||||
pushConnectionActive: boolean;
|
pushConnectionActive: boolean;
|
||||||
timezone: string;
|
timezone: string;
|
||||||
executionTimeout: number;
|
executionTimeout: number;
|
||||||
|
@ -905,6 +906,7 @@ export interface IRootState {
|
||||||
endpointFormWaiting: string;
|
endpointFormWaiting: string;
|
||||||
endpointWebhook: string;
|
endpointWebhook: string;
|
||||||
endpointWebhookTest: string;
|
endpointWebhookTest: string;
|
||||||
|
endpointWebhookWaiting: string;
|
||||||
executionId: string | null;
|
executionId: string | null;
|
||||||
executingNode: string[];
|
executingNode: string[];
|
||||||
executionWaitingForWebhook: boolean;
|
executionWaitingForWebhook: boolean;
|
||||||
|
|
|
@ -16,6 +16,7 @@ export const defaultSettings: FrontendSettings = {
|
||||||
endpointFormWaiting: '',
|
endpointFormWaiting: '',
|
||||||
endpointWebhook: '',
|
endpointWebhook: '',
|
||||||
endpointWebhookTest: '',
|
endpointWebhookTest: '',
|
||||||
|
endpointWebhookWaiting: '',
|
||||||
enterprise: {
|
enterprise: {
|
||||||
sharing: false,
|
sharing: false,
|
||||||
ldap: false,
|
ldap: false,
|
||||||
|
|
|
@ -24,6 +24,7 @@ import InputNodeSelect from './InputNodeSelect.vue';
|
||||||
import NodeExecuteButton from './NodeExecuteButton.vue';
|
import NodeExecuteButton from './NodeExecuteButton.vue';
|
||||||
import RunData from './RunData.vue';
|
import RunData from './RunData.vue';
|
||||||
import WireMeUp from './WireMeUp.vue';
|
import WireMeUp from './WireMeUp.vue';
|
||||||
|
import { waitingNodeTooltip } from '@/utils/executionUtils';
|
||||||
|
|
||||||
type MappingMode = 'debugging' | 'mapping';
|
type MappingMode = 'debugging' | 'mapping';
|
||||||
|
|
||||||
|
@ -237,6 +238,9 @@ export default defineComponent({
|
||||||
isMultiInputNode(): boolean {
|
isMultiInputNode(): boolean {
|
||||||
return this.activeNodeType !== null && this.activeNodeType.inputs.length > 1;
|
return this.activeNodeType !== null && this.activeNodeType.inputs.length > 1;
|
||||||
},
|
},
|
||||||
|
waitingMessage(): string {
|
||||||
|
return waitingNodeTooltip();
|
||||||
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
inputMode: {
|
inputMode: {
|
||||||
|
@ -448,6 +452,11 @@ export default defineComponent({
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<template #node-waiting>
|
||||||
|
<n8n-text :bold="true" color="text-dark" size="large">Waiting for input</n8n-text>
|
||||||
|
<n8n-text v-n8n-html="waitingMessage"></n8n-text>
|
||||||
|
</template>
|
||||||
|
|
||||||
<template #no-output-data>
|
<template #no-output-data>
|
||||||
<n8n-text tag="div" :bold="true" color="text-dark" size="large">{{
|
<n8n-text tag="div" :bold="true" color="text-dark" size="large">{{
|
||||||
$locale.baseText('ndv.input.noOutputData')
|
$locale.baseText('ndv.input.noOutputData')
|
||||||
|
|
|
@ -7,6 +7,7 @@ import {
|
||||||
NODE_INSERT_SPACER_BETWEEN_INPUT_GROUPS,
|
NODE_INSERT_SPACER_BETWEEN_INPUT_GROUPS,
|
||||||
SIMULATE_NODE_TYPE,
|
SIMULATE_NODE_TYPE,
|
||||||
SIMULATE_TRIGGER_NODE_TYPE,
|
SIMULATE_TRIGGER_NODE_TYPE,
|
||||||
|
WAIT_NODE_TYPE,
|
||||||
WAIT_TIME_UNLIMITED,
|
WAIT_TIME_UNLIMITED,
|
||||||
} from '@/constants';
|
} from '@/constants';
|
||||||
import type {
|
import type {
|
||||||
|
@ -268,7 +269,7 @@ const nodeClass = computed(() => {
|
||||||
const nodeExecutionStatus = computed(() => {
|
const nodeExecutionStatus = computed(() => {
|
||||||
const nodeExecutionRunData = workflowsStore.getWorkflowRunData?.[props.name];
|
const nodeExecutionRunData = workflowsStore.getWorkflowRunData?.[props.name];
|
||||||
if (nodeExecutionRunData) {
|
if (nodeExecutionRunData) {
|
||||||
return nodeExecutionRunData.filter(Boolean)[0].executionStatus ?? '';
|
return nodeExecutionRunData.filter(Boolean)[0]?.executionStatus ?? '';
|
||||||
}
|
}
|
||||||
return '';
|
return '';
|
||||||
});
|
});
|
||||||
|
@ -320,9 +321,21 @@ const nodeTitle = computed(() => {
|
||||||
const waiting = computed(() => {
|
const waiting = computed(() => {
|
||||||
const workflowExecution = workflowsStore.getWorkflowExecution as ExecutionSummary;
|
const workflowExecution = workflowsStore.getWorkflowExecution as ExecutionSummary;
|
||||||
|
|
||||||
if (workflowExecution?.waitTill) {
|
if (workflowExecution?.waitTill && !workflowExecution?.finished) {
|
||||||
const lastNodeExecuted = get(workflowExecution, 'data.resultData.lastNodeExecuted');
|
const lastNodeExecuted = get(workflowExecution, 'data.resultData.lastNodeExecuted');
|
||||||
if (props.name === lastNodeExecuted) {
|
if (props.name === lastNodeExecuted) {
|
||||||
|
const node = props.workflow.getNode(lastNodeExecuted);
|
||||||
|
if (
|
||||||
|
node &&
|
||||||
|
node.type === WAIT_NODE_TYPE &&
|
||||||
|
['webhook', 'form'].includes(node.parameters.resume as string)
|
||||||
|
) {
|
||||||
|
const event =
|
||||||
|
node.parameters.resume === 'webhook'
|
||||||
|
? i18n.baseText('node.theNodeIsWaitingWebhookCall')
|
||||||
|
: i18n.baseText('node.theNodeIsWaitingFormCall');
|
||||||
|
return event;
|
||||||
|
}
|
||||||
const waitDate = new Date(workflowExecution.waitTill);
|
const waitDate = new Date(workflowExecution.waitTill);
|
||||||
if (waitDate.toISOString() === WAIT_TIME_UNLIMITED) {
|
if (waitDate.toISOString() === WAIT_TIME_UNLIMITED) {
|
||||||
return i18n.baseText('node.theNodeIsWaitingIndefinitelyForAnIncomingWebhookCall');
|
return i18n.baseText('node.theNodeIsWaitingIndefinitelyForAnIncomingWebhookCall');
|
||||||
|
@ -675,6 +688,10 @@ function openContextMenu(event: MouseEvent, source: 'node-button' | 'node-right-
|
||||||
<FontAwesomeIcon icon="sync-alt" spin />
|
<FontAwesomeIcon icon="sync-alt" spin />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-if="waiting" class="node-waiting-spinner" :title="waiting">
|
||||||
|
<FontAwesomeIcon icon="sync-alt" spin />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="node-trigger-tooltip__wrapper">
|
<div class="node-trigger-tooltip__wrapper">
|
||||||
<n8n-tooltip
|
<n8n-tooltip
|
||||||
placement="top"
|
placement="top"
|
||||||
|
@ -913,6 +930,20 @@ function openContextMenu(event: MouseEvent, source: 'node-button' | 'node-right-
|
||||||
color: hsla(var(--color-primary-h), var(--color-primary-s), var(--color-primary-l), 0.7);
|
color: hsla(var(--color-primary-h), var(--color-primary-s), var(--color-primary-l), 0.7);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.node-waiting-spinner {
|
||||||
|
display: inline-block;
|
||||||
|
position: absolute;
|
||||||
|
left: 0px;
|
||||||
|
top: 0px;
|
||||||
|
z-index: 12;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
font-size: 3.75em;
|
||||||
|
line-height: 1.65em;
|
||||||
|
text-align: center;
|
||||||
|
color: hsla(var(--color-primary-h), var(--color-primary-s), var(--color-primary-l), 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
.node-icon {
|
.node-icon {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: calc(50% - 20px);
|
top: calc(50% - 20px);
|
||||||
|
|
|
@ -56,7 +56,7 @@ defineOptions({
|
||||||
const lastPopupCountUpdate = ref(0);
|
const lastPopupCountUpdate = ref(0);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { runWorkflow, stopCurrentExecution } = useRunWorkflow({ router });
|
const { runWorkflowResolvePending, stopCurrentExecution } = useRunWorkflow({ router });
|
||||||
|
|
||||||
const workflowsStore = useWorkflowsStore();
|
const workflowsStore = useWorkflowsStore();
|
||||||
const externalHooks = useExternalHooks();
|
const externalHooks = useExternalHooks();
|
||||||
|
@ -264,7 +264,7 @@ async function onClick() {
|
||||||
telemetry.track('User clicked execute node button', telemetryPayload);
|
telemetry.track('User clicked execute node button', telemetryPayload);
|
||||||
await externalHooks.run('nodeExecuteButton.onClick', telemetryPayload);
|
await externalHooks.run('nodeExecuteButton.onClick', telemetryPayload);
|
||||||
|
|
||||||
await runWorkflow({
|
await runWorkflowResolvePending({
|
||||||
destinationNode: props.nodeName,
|
destinationNode: props.nodeName,
|
||||||
source: 'RunData.ExecuteNodeButton',
|
source: 'RunData.ExecuteNodeButton',
|
||||||
});
|
});
|
||||||
|
|
|
@ -14,6 +14,7 @@ import { useNodeType } from '@/composables/useNodeType';
|
||||||
import { usePinnedData } from '@/composables/usePinnedData';
|
import { usePinnedData } from '@/composables/usePinnedData';
|
||||||
import { useTelemetry } from '@/composables/useTelemetry';
|
import { useTelemetry } from '@/composables/useTelemetry';
|
||||||
import { useI18n } from '@/composables/useI18n';
|
import { useI18n } from '@/composables/useI18n';
|
||||||
|
import { waitingNodeTooltip } from '@/utils/executionUtils';
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
|
|
||||||
|
@ -348,6 +349,11 @@ const activatePane = () => {
|
||||||
</n8n-text>
|
</n8n-text>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<template #node-waiting>
|
||||||
|
<n8n-text :bold="true" color="text-dark" size="large">Waiting for input</n8n-text>
|
||||||
|
<n8n-text v-n8n-html="waitingNodeTooltip()"></n8n-text>
|
||||||
|
</template>
|
||||||
|
|
||||||
<template #no-output-data>
|
<template #no-output-data>
|
||||||
<n8n-text :bold="true" color="text-dark" size="large">{{
|
<n8n-text :bold="true" color="text-dark" size="large">{{
|
||||||
$locale.baseText('ndv.output.noOutputData.title')
|
$locale.baseText('ndv.output.noOutputData.title')
|
||||||
|
|
|
@ -218,6 +218,13 @@ export default defineComponent({
|
||||||
isReadOnlyRoute() {
|
isReadOnlyRoute() {
|
||||||
return this.$route?.meta?.readOnlyCanvas === true;
|
return this.$route?.meta?.readOnlyCanvas === true;
|
||||||
},
|
},
|
||||||
|
isWaitNodeWaiting() {
|
||||||
|
return (
|
||||||
|
this.workflowExecution?.status === 'waiting' &&
|
||||||
|
this.workflowExecution.data?.waitTill &&
|
||||||
|
this.workflowExecution?.data?.resultData?.lastNodeExecuted === this.node?.name
|
||||||
|
);
|
||||||
|
},
|
||||||
activeNode(): INodeUi | null {
|
activeNode(): INodeUi | null {
|
||||||
return this.ndvStore.activeNode;
|
return this.ndvStore.activeNode;
|
||||||
},
|
},
|
||||||
|
@ -1432,6 +1439,10 @@ export default defineComponent({
|
||||||
<NodeErrorView :error="subworkflowExecutionError" :class="$style.errorDisplay" />
|
<NodeErrorView :error="subworkflowExecutionError" :class="$style.errorDisplay" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="isWaitNodeWaiting" :class="$style.center">
|
||||||
|
<slot name="node-waiting">xxx</slot>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-else-if="!hasNodeRun && !(isInputSchemaView && node?.disabled)" :class="$style.center">
|
<div v-else-if="!hasNodeRun && !(isInputSchemaView && node?.disabled)" :class="$style.center">
|
||||||
<slot name="node-not-run"></slot>
|
<slot name="node-not-run"></slot>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -34,16 +34,18 @@ const hideNodeIssues = computed(() => false); // @TODO Implement this
|
||||||
<FontAwesomeIcon icon="exclamation-triangle" />
|
<FontAwesomeIcon icon="exclamation-triangle" />
|
||||||
</N8nTooltip>
|
</N8nTooltip>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div v-else-if="executionWaiting || executionStatus === 'waiting'">
|
||||||
v-else-if="executionWaiting || executionStatus === 'waiting'"
|
<div :class="[$style.status, $style.waiting]">
|
||||||
:class="[$style.status, $style.waiting]"
|
<N8nTooltip placement="bottom">
|
||||||
>
|
<template #content>
|
||||||
<N8nTooltip placement="bottom">
|
<div v-text="executionWaiting"></div>
|
||||||
<template #content>
|
</template>
|
||||||
<div v-text="executionWaiting"></div>
|
<FontAwesomeIcon icon="clock" />
|
||||||
</template>
|
</N8nTooltip>
|
||||||
<FontAwesomeIcon icon="clock" />
|
</div>
|
||||||
</N8nTooltip>
|
<div :class="[$style.status, $style['node-waiting-spinner']]">
|
||||||
|
<FontAwesomeIcon icon="sync-alt" spin />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else-if="hasPinnedData && !nodeHelpers.isProductionExecutionPreview.value && !isDisabled"
|
v-else-if="hasPinnedData && !nodeHelpers.isProductionExecutionPreview.value && !isDisabled"
|
||||||
|
@ -97,6 +99,18 @@ const hideNodeIssues = computed(() => false); // @TODO Implement this
|
||||||
font-size: 3.75em;
|
font-size: 3.75em;
|
||||||
color: hsla(var(--color-primary-h), var(--color-primary-s), var(--color-primary-l), 0.7);
|
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 {
|
.issues {
|
||||||
color: var(--color-danger);
|
color: var(--color-danger);
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { ExpressionError, type IPinData, type IRunData, type Workflow } from 'n8
|
||||||
|
|
||||||
import { useRootStore } from '@/stores/root.store';
|
import { useRootStore } from '@/stores/root.store';
|
||||||
import { useRunWorkflow } from '@/composables/useRunWorkflow';
|
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 { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
import { useUIStore } from '@/stores/ui.store';
|
import { useUIStore } from '@/stores/ui.store';
|
||||||
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
|
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
|
||||||
|
@ -22,6 +22,7 @@ vi.mock('@/stores/workflows.store', () => ({
|
||||||
executionWaitingForWebhook: false,
|
executionWaitingForWebhook: false,
|
||||||
getCurrentWorkflow: vi.fn().mockReturnValue({ id: '123' }),
|
getCurrentWorkflow: vi.fn().mockReturnValue({ id: '123' }),
|
||||||
getNodeByName: vi.fn(),
|
getNodeByName: vi.fn(),
|
||||||
|
getExecution: vi.fn(),
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
@ -303,4 +304,76 @@ describe('useRunWorkflow({ router })', () => {
|
||||||
expect(result.runData).toEqual(undefined);
|
expect(result.runData).toEqual(undefined);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('useRunWorkflow({ router }) - runWorkflowResolvePending', () => {
|
||||||
|
let uiStore: ReturnType<typeof useUIStore>;
|
||||||
|
let workflowsStore: ReturnType<typeof useWorkflowsStore>;
|
||||||
|
let router: ReturnType<typeof useRouter>;
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -38,7 +38,12 @@ import type {
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import { NodeConnectionType, NodeHelpers } from 'n8n-workflow';
|
import { NodeConnectionType, NodeHelpers } from 'n8n-workflow';
|
||||||
import type { INodeUi } from '@/Interface';
|
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 { sanitizeHtml } from '@/utils/htmlUtils';
|
||||||
import { MarkerType } from '@vue-flow/core';
|
import { MarkerType } from '@vue-flow/core';
|
||||||
import { useNodeHelpers } from './useNodeHelpers';
|
import { useNodeHelpers } from './useNodeHelpers';
|
||||||
|
@ -233,7 +238,8 @@ export function useCanvasMapping({
|
||||||
const nodeExecutionStatusById = computed(() =>
|
const nodeExecutionStatusById = computed(() =>
|
||||||
nodes.value.reduce<Record<string, ExecutionStatus>>((acc, node) => {
|
nodes.value.reduce<Record<string, ExecutionStatus>>((acc, node) => {
|
||||||
acc[node.id] =
|
acc[node.id] =
|
||||||
workflowsStore.getWorkflowRunData?.[node.name]?.filter(Boolean)[0].executionStatus ?? 'new';
|
workflowsStore.getWorkflowRunData?.[node.name]?.filter(Boolean)[0]?.executionStatus ??
|
||||||
|
'new';
|
||||||
return acc;
|
return acc;
|
||||||
}, {}),
|
}, {}),
|
||||||
);
|
);
|
||||||
|
@ -327,8 +333,21 @@ export function useCanvasMapping({
|
||||||
if (workflowExecution && lastNodeExecuted && isExecutionSummary(workflowExecution)) {
|
if (workflowExecution && lastNodeExecuted && isExecutionSummary(workflowExecution)) {
|
||||||
if (
|
if (
|
||||||
node.name === workflowExecution.data?.resultData?.lastNodeExecuted &&
|
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);
|
const waitDate = new Date(workflowExecution.waitTill);
|
||||||
|
|
||||||
if (waitDate.toISOString() === WAIT_TIME_UNLIMITED) {
|
if (waitDate.toISOString() === WAIT_TIME_UNLIMITED) {
|
||||||
|
|
|
@ -305,7 +305,6 @@ export function usePushConnection({ router }: { router: ReturnType<typeof useRou
|
||||||
? saveManualExecutions
|
? saveManualExecutions
|
||||||
: workflowSettings.saveManualExecutions;
|
: workflowSettings.saveManualExecutions;
|
||||||
|
|
||||||
let action;
|
|
||||||
if (!isSavingExecutions) {
|
if (!isSavingExecutions) {
|
||||||
globalLinkActionsEventBus.emit('registerGlobalLinkAction', {
|
globalLinkActionsEventBus.emit('registerGlobalLinkAction', {
|
||||||
key: 'open-settings',
|
key: 'open-settings',
|
||||||
|
@ -314,22 +313,10 @@ export function usePushConnection({ router }: { router: ReturnType<typeof useRou
|
||||||
uiStore.openModal(WORKFLOW_SETTINGS_MODAL_KEY);
|
uiStore.openModal(WORKFLOW_SETTINGS_MODAL_KEY);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
action =
|
|
||||||
'<a data-action="open-settings">Turn on saving manual executions</a> and run again to see what happened after this node.';
|
|
||||||
} else {
|
|
||||||
action = `<a href="/workflow/${workflow.id}/executions/${activeExecutionId}">View the execution</a> to see what happened after this node.`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Workflow did start but had been put to wait
|
// Workflow did start but had been put to wait
|
||||||
workflowHelpers.setDocumentTitle(workflow.name as string, 'IDLE');
|
workflowHelpers.setDocumentTitle(workflow.name as string, 'IDLE');
|
||||||
toast.showToast({
|
|
||||||
title: 'Workflow started waiting',
|
|
||||||
message: `${action} <a href="https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.wait/" target="_blank">More info</a>`,
|
|
||||||
type: 'success',
|
|
||||||
duration: 0,
|
|
||||||
dangerouslyUseHTMLString: true,
|
|
||||||
});
|
|
||||||
} else if (runDataExecuted.finished !== true) {
|
} else if (runDataExecuted.finished !== true) {
|
||||||
workflowHelpers.setDocumentTitle(workflow.name as string, 'ERROR');
|
workflowHelpers.setDocumentTitle(workflow.name as string, 'ERROR');
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ import type {
|
||||||
IStartRunData,
|
IStartRunData,
|
||||||
IWorkflowDb,
|
IWorkflowDb,
|
||||||
} from '@/Interface';
|
} from '@/Interface';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
IRunData,
|
IRunData,
|
||||||
IRunExecutionData,
|
IRunExecutionData,
|
||||||
|
@ -13,18 +14,25 @@ import type {
|
||||||
StartNodeData,
|
StartNodeData,
|
||||||
IRun,
|
IRun,
|
||||||
INode,
|
INode,
|
||||||
|
IDataObject,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
import { NodeConnectionType } from 'n8n-workflow';
|
import { NodeConnectionType } from 'n8n-workflow';
|
||||||
|
|
||||||
import { useToast } from '@/composables/useToast';
|
import { useToast } from '@/composables/useToast';
|
||||||
import { useNodeHelpers } from '@/composables/useNodeHelpers';
|
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 { useRootStore } from '@/stores/root.store';
|
||||||
import { useUIStore } from '@/stores/ui.store';
|
import { useUIStore } from '@/stores/ui.store';
|
||||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
|
||||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
import { displayForm } from '@/utils/executionUtils';
|
import { displayForm, openPopUpWindow } from '@/utils/executionUtils';
|
||||||
import { useExternalHooks } from '@/composables/useExternalHooks';
|
import { useExternalHooks } from '@/composables/useExternalHooks';
|
||||||
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
|
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
|
||||||
import type { useRouter } from 'vue-router';
|
import type { useRouter } from 'vue-router';
|
||||||
|
@ -35,6 +43,8 @@ import { useExecutionsStore } from '@/stores/executions.store';
|
||||||
import type { PushPayload } from '@n8n/api-types';
|
import type { PushPayload } from '@n8n/api-types';
|
||||||
import { useLocalStorage } from '@vueuse/core';
|
import { useLocalStorage } from '@vueuse/core';
|
||||||
|
|
||||||
|
const FORM_RELOAD = 'n8n_redirect_to_next_form_test_page';
|
||||||
|
|
||||||
export function useRunWorkflow(useRunWorkflowOpts: { router: ReturnType<typeof useRouter> }) {
|
export function useRunWorkflow(useRunWorkflowOpts: { router: ReturnType<typeof useRouter> }) {
|
||||||
const nodeHelpers = useNodeHelpers();
|
const nodeHelpers = useNodeHelpers();
|
||||||
const workflowHelpers = useWorkflowHelpers({ router: useRunWorkflowOpts.router });
|
const workflowHelpers = useWorkflowHelpers({ router: useRunWorkflowOpts.router });
|
||||||
|
@ -43,7 +53,6 @@ export function useRunWorkflow(useRunWorkflowOpts: { router: ReturnType<typeof u
|
||||||
|
|
||||||
const rootStore = useRootStore();
|
const rootStore = useRootStore();
|
||||||
const uiStore = useUIStore();
|
const uiStore = useUIStore();
|
||||||
const nodeTypesStore = useNodeTypesStore();
|
|
||||||
const workflowsStore = useWorkflowsStore();
|
const workflowsStore = useWorkflowsStore();
|
||||||
const executionsStore = useExecutionsStore();
|
const executionsStore = useExecutionsStore();
|
||||||
|
|
||||||
|
@ -263,42 +272,21 @@ export function useRunWorkflow(useRunWorkflowOpts: { router: ReturnType<typeof u
|
||||||
|
|
||||||
const getTestUrl = (() => {
|
const getTestUrl = (() => {
|
||||||
return (node: INode) => {
|
return (node: INode) => {
|
||||||
const nodeType = nodeTypesStore.getNodeType(node.type, node.typeVersion);
|
return `${rootStore.formTestUrl}/${node.parameters.path}`;
|
||||||
if (nodeType?.webhooks?.length) {
|
|
||||||
return workflowHelpers.getWebhookUrl(nodeType.webhooks[0], node, 'test');
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
const shouldShowForm = (() => {
|
try {
|
||||||
return (node: INode) => {
|
displayForm({
|
||||||
const workflowTriggerNodes = workflow
|
nodes: workflowData.nodes,
|
||||||
.getTriggerNodes()
|
runData: workflowsStore.getWorkflowExecution?.data?.resultData?.runData,
|
||||||
.map((triggerNode) => triggerNode.name);
|
destinationNode: options.destinationNode,
|
||||||
|
pinData,
|
||||||
const showForm =
|
directParentNodes,
|
||||||
options.destinationNode === node.name ||
|
source: options.source,
|
||||||
directParentNodes.includes(node.name) ||
|
getTestUrl,
|
||||||
workflowTriggerNodes.some((triggerNode) =>
|
});
|
||||||
workflowsStore.isNodeInOutgoingNodeConnections(triggerNode, node.name),
|
} catch (error) {}
|
||||||
);
|
|
||||||
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,
|
|
||||||
});
|
|
||||||
|
|
||||||
await useExternalHooks().run('workflowRun.runWorkflow', {
|
await useExternalHooks().run('workflowRun.runWorkflow', {
|
||||||
nodeName: options.destinationNode,
|
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(
|
function consolidateRunDataAndStartNodes(
|
||||||
directParentNodes: string[],
|
directParentNodes: string[],
|
||||||
runData: IRunData | null,
|
runData: IRunData | null,
|
||||||
|
@ -433,6 +543,7 @@ export function useRunWorkflow(useRunWorkflowOpts: { router: ReturnType<typeof u
|
||||||
return {
|
return {
|
||||||
consolidateRunDataAndStartNodes,
|
consolidateRunDataAndStartNodes,
|
||||||
runWorkflow,
|
runWorkflow,
|
||||||
|
runWorkflowResolvePending,
|
||||||
runWorkflowApi,
|
runWorkflowApi,
|
||||||
stopCurrentExecution,
|
stopCurrentExecution,
|
||||||
stopWaitingForWebhook,
|
stopWaitingForWebhook,
|
||||||
|
|
|
@ -965,6 +965,9 @@
|
||||||
"ndv.output.run": "Run",
|
"ndv.output.run": "Run",
|
||||||
"ndv.output.runNodeHint": "Execute this node to view data",
|
"ndv.output.runNodeHint": "Execute this node to view data",
|
||||||
"ndv.output.runNodeHintSubNode": "Output will appear here once the parent node is run",
|
"ndv.output.runNodeHintSubNode": "Output will appear here once the parent node is run",
|
||||||
|
"ndv.output.waitNodeWaitingForWebhook": "Execution will continue when webhook is received on ",
|
||||||
|
"ndv.output.waitNodeWaitingForFormSubmission": "Execution will continue when form is submitted on ",
|
||||||
|
"ndv.output.waitNodeWaiting": "Execution will continue when wait time is over",
|
||||||
"ndv.output.insertTestData": "set mock data",
|
"ndv.output.insertTestData": "set mock data",
|
||||||
"ndv.output.staleDataWarning.regular": "Node parameters have changed.<br>Test node again to refresh output.",
|
"ndv.output.staleDataWarning.regular": "Node parameters have changed.<br>Test node again to refresh output.",
|
||||||
"ndv.output.staleDataWarning.pinData": "Node parameter changes will not affect pinned output data.",
|
"ndv.output.staleDataWarning.pinData": "Node parameter changes will not affect pinned output data.",
|
||||||
|
@ -1011,6 +1014,8 @@
|
||||||
"node.nodeIsExecuting": "Node is executing",
|
"node.nodeIsExecuting": "Node is executing",
|
||||||
"node.nodeIsWaitingTill": "Node is waiting until {date} {time}",
|
"node.nodeIsWaitingTill": "Node is waiting until {date} {time}",
|
||||||
"node.theNodeIsWaitingIndefinitelyForAnIncomingWebhookCall": "The node is waiting for an incoming webhook call (indefinitely)",
|
"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.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.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.",
|
"node.discovery.pinData.ndv": "You can pin this output instead of waiting for a test event.",
|
||||||
|
|
|
@ -19,6 +19,7 @@ export const useRootStore = defineStore(STORES.ROOT, () => {
|
||||||
endpointFormWaiting: 'form-waiting',
|
endpointFormWaiting: 'form-waiting',
|
||||||
endpointWebhook: 'webhook',
|
endpointWebhook: 'webhook',
|
||||||
endpointWebhookTest: 'webhook-test',
|
endpointWebhookTest: 'webhook-test',
|
||||||
|
endpointWebhookWaiting: 'webhook-waiting',
|
||||||
pushConnectionActive: true,
|
pushConnectionActive: true,
|
||||||
timezone: 'America/New_York',
|
timezone: 'America/New_York',
|
||||||
executionTimeout: -1,
|
executionTimeout: -1,
|
||||||
|
@ -43,10 +44,16 @@ export const useRootStore = defineStore(STORES.ROOT, () => {
|
||||||
|
|
||||||
const formTestUrl = computed(() => `${state.value.urlBaseEditor}${state.value.endpointFormTest}`);
|
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 webhookUrl = computed(() => `${state.value.urlBaseWebhook}${state.value.endpointWebhook}`);
|
||||||
|
|
||||||
|
const webhookWaitingUrl = computed(
|
||||||
|
() => `${state.value.urlBaseEditor}${state.value.endpointWebhookWaiting}`,
|
||||||
|
);
|
||||||
|
|
||||||
const pushRef = computed(() => state.value.pushRef);
|
const pushRef = computed(() => state.value.pushRef);
|
||||||
|
|
||||||
const binaryDataMode = computed(() => state.value.binaryDataMode);
|
const binaryDataMode = computed(() => state.value.binaryDataMode);
|
||||||
|
@ -131,6 +138,10 @@ export const useRootStore = defineStore(STORES.ROOT, () => {
|
||||||
state.value.endpointWebhookTest = endpointWebhookTest;
|
state.value.endpointWebhookTest = endpointWebhookTest;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const setEndpointWebhookWaiting = (endpointWebhookWaiting: string) => {
|
||||||
|
state.value.endpointWebhookWaiting = endpointWebhookWaiting;
|
||||||
|
};
|
||||||
|
|
||||||
const setTimezone = (timezone: string) => {
|
const setTimezone = (timezone: string) => {
|
||||||
state.value.timezone = timezone;
|
state.value.timezone = timezone;
|
||||||
setGlobalState({ defaultTimezone: timezone });
|
setGlobalState({ defaultTimezone: timezone });
|
||||||
|
@ -177,6 +188,7 @@ export const useRootStore = defineStore(STORES.ROOT, () => {
|
||||||
formWaitingUrl,
|
formWaitingUrl,
|
||||||
webhookUrl,
|
webhookUrl,
|
||||||
webhookTestUrl,
|
webhookTestUrl,
|
||||||
|
webhookWaitingUrl,
|
||||||
restUrl,
|
restUrl,
|
||||||
restCloudApiContext,
|
restCloudApiContext,
|
||||||
restApiContext,
|
restApiContext,
|
||||||
|
@ -200,6 +212,7 @@ export const useRootStore = defineStore(STORES.ROOT, () => {
|
||||||
setEndpointFormWaiting,
|
setEndpointFormWaiting,
|
||||||
setEndpointWebhook,
|
setEndpointWebhook,
|
||||||
setEndpointWebhookTest,
|
setEndpointWebhookTest,
|
||||||
|
setEndpointWebhookWaiting,
|
||||||
setTimezone,
|
setTimezone,
|
||||||
setExecutionTimeout,
|
setExecutionTimeout,
|
||||||
setMaxExecutionTimeout,
|
setMaxExecutionTimeout,
|
||||||
|
|
|
@ -235,6 +235,7 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, () => {
|
||||||
rootStore.setEndpointFormWaiting(fetchedSettings.endpointFormWaiting);
|
rootStore.setEndpointFormWaiting(fetchedSettings.endpointFormWaiting);
|
||||||
rootStore.setEndpointWebhook(fetchedSettings.endpointWebhook);
|
rootStore.setEndpointWebhook(fetchedSettings.endpointWebhook);
|
||||||
rootStore.setEndpointWebhookTest(fetchedSettings.endpointWebhookTest);
|
rootStore.setEndpointWebhookTest(fetchedSettings.endpointWebhookTest);
|
||||||
|
rootStore.setEndpointWebhookWaiting(fetchedSettings.endpointWebhookWaiting);
|
||||||
rootStore.setTimezone(fetchedSettings.timezone);
|
rootStore.setTimezone(fetchedSettings.timezone);
|
||||||
rootStore.setExecutionTimeout(fetchedSettings.executionTimeout);
|
rootStore.setExecutionTimeout(fetchedSettings.executionTimeout);
|
||||||
rootStore.setMaxExecutionTimeout(fetchedSettings.maxExecutionTimeout);
|
rootStore.setMaxExecutionTimeout(fetchedSettings.maxExecutionTimeout);
|
||||||
|
|
|
@ -7,6 +7,7 @@ import {
|
||||||
PLACEHOLDER_EMPTY_WORKFLOW_ID,
|
PLACEHOLDER_EMPTY_WORKFLOW_ID,
|
||||||
START_NODE_TYPE,
|
START_NODE_TYPE,
|
||||||
STORES,
|
STORES,
|
||||||
|
WAIT_NODE_TYPE,
|
||||||
} from '@/constants';
|
} from '@/constants';
|
||||||
import type {
|
import type {
|
||||||
ExecutionsQueryFilter,
|
ExecutionsQueryFilter,
|
||||||
|
@ -166,6 +167,10 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
|
||||||
|
|
||||||
const allNodes = computed<INodeUi[]>(() => workflow.value.nodes);
|
const allNodes = computed<INodeUi[]>(() => 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.
|
// Names of all nodes currently on canvas.
|
||||||
const canvasNames = computed(() => new Set(allNodes.value.map((n) => n.name)));
|
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) {
|
function setWorkflowExecutionData(workflowResultData: IExecutionResponse | null) {
|
||||||
|
if (workflowResultData?.data?.waitTill) {
|
||||||
|
delete workflowResultData.data.resultData.runData[
|
||||||
|
workflowResultData.data.resultData.lastNodeExecuted as string
|
||||||
|
];
|
||||||
|
}
|
||||||
workflowExecutionData.value = workflowResultData;
|
workflowExecutionData.value = workflowResultData;
|
||||||
workflowExecutionPairedItemMappings.value = getPairedItemsMapping(workflowResultData);
|
workflowExecutionPairedItemMappings.value = getPairedItemsMapping(workflowResultData);
|
||||||
}
|
}
|
||||||
|
@ -1555,6 +1565,7 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
|
||||||
getWorkflowResultDataByNodeName,
|
getWorkflowResultDataByNodeName,
|
||||||
allConnections,
|
allConnections,
|
||||||
allNodes,
|
allNodes,
|
||||||
|
isWaitingExecution,
|
||||||
canvasNames,
|
canvasNames,
|
||||||
nodesByName,
|
nodesByName,
|
||||||
nodesIssuesExist,
|
nodesIssuesExist,
|
||||||
|
|
|
@ -15,7 +15,6 @@ vi.mock('../executionUtils', async () => {
|
||||||
|
|
||||||
describe('displayForm', () => {
|
describe('displayForm', () => {
|
||||||
const getTestUrlMock = vi.fn();
|
const getTestUrlMock = vi.fn();
|
||||||
const shouldShowFormMock = vi.fn();
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
|
@ -50,11 +49,8 @@ describe('displayForm', () => {
|
||||||
pinData,
|
pinData,
|
||||||
destinationNode: undefined,
|
destinationNode: undefined,
|
||||||
directParentNodes: [],
|
directParentNodes: [],
|
||||||
formWaitingUrl: 'http://example.com',
|
|
||||||
executionId: undefined,
|
|
||||||
source: undefined,
|
source: undefined,
|
||||||
getTestUrl: getTestUrlMock,
|
getTestUrl: getTestUrlMock,
|
||||||
shouldShowForm: shouldShowFormMock,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(openPopUpWindow).not.toHaveBeenCalled();
|
expect(openPopUpWindow).not.toHaveBeenCalled();
|
||||||
|
@ -86,11 +82,8 @@ describe('displayForm', () => {
|
||||||
pinData: {},
|
pinData: {},
|
||||||
destinationNode: 'Node3',
|
destinationNode: 'Node3',
|
||||||
directParentNodes: ['Node4'],
|
directParentNodes: ['Node4'],
|
||||||
formWaitingUrl: 'http://example.com',
|
|
||||||
executionId: '12345',
|
|
||||||
source: undefined,
|
source: undefined,
|
||||||
getTestUrl: getTestUrlMock,
|
getTestUrl: getTestUrlMock,
|
||||||
shouldShowForm: shouldShowFormMock,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(openPopUpWindow).not.toHaveBeenCalled();
|
expect(openPopUpWindow).not.toHaveBeenCalled();
|
||||||
|
@ -116,11 +109,8 @@ describe('displayForm', () => {
|
||||||
pinData: {},
|
pinData: {},
|
||||||
destinationNode: undefined,
|
destinationNode: undefined,
|
||||||
directParentNodes: [],
|
directParentNodes: [],
|
||||||
formWaitingUrl: 'http://example.com',
|
|
||||||
executionId: undefined,
|
|
||||||
source: 'RunData.ManualChatMessage',
|
source: 'RunData.ManualChatMessage',
|
||||||
getTestUrl: getTestUrlMock,
|
getTestUrl: getTestUrlMock,
|
||||||
shouldShowForm: shouldShowFormMock,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(openPopUpWindow).not.toHaveBeenCalled();
|
expect(openPopUpWindow).not.toHaveBeenCalled();
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
import type { ExecutionStatus, IDataObject, INode, IPinData, IRunData } from 'n8n-workflow';
|
import type { ExecutionStatus, IDataObject, INode, IPinData, IRunData } from 'n8n-workflow';
|
||||||
import type { ExecutionFilterType, ExecutionsQueryFilter } from '@/Interface';
|
import type { ExecutionFilterType, ExecutionsQueryFilter } from '@/Interface';
|
||||||
import { isEmpty } from '@/utils/typesUtils';
|
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 {
|
export function getDefaultExecutionFilters(): ExecutionFilterType {
|
||||||
return {
|
return {
|
||||||
|
@ -79,15 +82,15 @@ export const openPopUpWindow = (
|
||||||
const windowWidth = window.innerWidth;
|
const windowWidth = window.innerWidth;
|
||||||
const smallScreen = windowWidth <= 800;
|
const smallScreen = windowWidth <= 800;
|
||||||
if (options?.alwaysInNewTab || smallScreen) {
|
if (options?.alwaysInNewTab || smallScreen) {
|
||||||
window.open(url, '_blank');
|
return window.open(url, '_blank');
|
||||||
} else {
|
} else {
|
||||||
const height = options?.width || 700;
|
const height = options?.width || 700;
|
||||||
const width = options?.height || window.innerHeight - 50;
|
const width = options?.height || window.innerHeight - 50;
|
||||||
const left = (window.innerWidth - height) / 2;
|
const left = (window.innerWidth - height) / 2;
|
||||||
const top = 50;
|
const top = 50;
|
||||||
const features = `width=${height},height=${width},left=${left},top=${top},resizable=yes,scrollbars=yes`;
|
const features = `width=${height},height=${width},left=${left},top=${top},resizable=yes,scrollbars=yes`;
|
||||||
|
const windowName = `form-waiting-since-${Date.now()}`;
|
||||||
window.open(url, '_blank', features);
|
return window.open(url, windowName, features);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -97,57 +100,69 @@ export function displayForm({
|
||||||
pinData,
|
pinData,
|
||||||
destinationNode,
|
destinationNode,
|
||||||
directParentNodes,
|
directParentNodes,
|
||||||
formWaitingUrl,
|
|
||||||
executionId,
|
|
||||||
source,
|
source,
|
||||||
getTestUrl,
|
getTestUrl,
|
||||||
shouldShowForm,
|
|
||||||
}: {
|
}: {
|
||||||
nodes: INode[];
|
nodes: INode[];
|
||||||
runData: IRunData | undefined;
|
runData: IRunData | undefined;
|
||||||
pinData: IPinData;
|
pinData: IPinData;
|
||||||
destinationNode: string | undefined;
|
destinationNode: string | undefined;
|
||||||
directParentNodes: string[];
|
directParentNodes: string[];
|
||||||
formWaitingUrl: string;
|
|
||||||
executionId: string | undefined;
|
|
||||||
source: string | undefined;
|
source: string | undefined;
|
||||||
getTestUrl: (node: INode) => string;
|
getTestUrl: (node: INode) => string;
|
||||||
shouldShowForm: (node: INode) => boolean;
|
|
||||||
}) {
|
}) {
|
||||||
for (const node of nodes) {
|
for (const node of nodes) {
|
||||||
const hasNodeRun = runData && runData?.hasOwnProperty(node.name);
|
const hasNodeRun = runData && runData?.hasOwnProperty(node.name);
|
||||||
|
|
||||||
if (hasNodeRun || pinData[node.name]) continue;
|
if (hasNodeRun || pinData[node.name]) continue;
|
||||||
|
|
||||||
if (![FORM_TRIGGER_NODE_TYPE, WAIT_NODE_TYPE].includes(node.type)) {
|
if (![FORM_TRIGGER_NODE_TYPE].includes(node.type)) continue;
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
if (destinationNode && destinationNode !== node.name && !directParentNodes.includes(node.name))
|
||||||
destinationNode &&
|
|
||||||
destinationNode !== node.name &&
|
|
||||||
!directParentNodes.includes(node.name)
|
|
||||||
) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
|
||||||
|
|
||||||
if (node.name === destinationNode || !node.disabled) {
|
if (node.name === destinationNode || !node.disabled) {
|
||||||
let testUrl = '';
|
let testUrl = '';
|
||||||
|
if (node.type === FORM_TRIGGER_NODE_TYPE) testUrl = getTestUrl(node);
|
||||||
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 (testUrl && source !== 'RunData.ManualChatMessage') openPopUpWindow(testUrl);
|
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}<a href="${resumeUrl}" target="_blank">${resumeUrl}</a>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// do not throw error if could not compose tooltip
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
};
|
||||||
|
|
|
@ -153,7 +153,8 @@ const { addBeforeUnloadEventBindings, removeBeforeUnloadEventBindings } = useBef
|
||||||
route,
|
route,
|
||||||
});
|
});
|
||||||
const { registerCustomAction, unregisterCustomAction } = useGlobalLinkActions();
|
const { registerCustomAction, unregisterCustomAction } = useGlobalLinkActions();
|
||||||
const { runWorkflow, stopCurrentExecution, stopWaitingForWebhook } = useRunWorkflow({ router });
|
const { runWorkflow, runWorkflowResolvePending, stopCurrentExecution, stopWaitingForWebhook } =
|
||||||
|
useRunWorkflow({ router });
|
||||||
const {
|
const {
|
||||||
updateNodePosition,
|
updateNodePosition,
|
||||||
updateNodesPosition,
|
updateNodesPosition,
|
||||||
|
@ -950,7 +951,14 @@ const projectPermissions = computed(() => {
|
||||||
|
|
||||||
const isStoppingExecution = ref(false);
|
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 isExecutionWaitingForWebhook = computed(() => workflowsStore.executionWaitingForWebhook);
|
||||||
|
|
||||||
const isExecutionDisabled = computed(() => {
|
const isExecutionDisabled = computed(() => {
|
||||||
|
@ -985,7 +993,11 @@ const workflowExecutionData = computed(() => workflowsStore.workflowExecutionDat
|
||||||
async function onRunWorkflow() {
|
async function onRunWorkflow() {
|
||||||
trackRunWorkflow();
|
trackRunWorkflow();
|
||||||
|
|
||||||
await runWorkflow({});
|
if (!isExecutionPreview.value && workflowsStore.isWaitingExecution) {
|
||||||
|
void runWorkflowResolvePending({});
|
||||||
|
} else {
|
||||||
|
void runWorkflow({});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function trackRunWorkflow() {
|
function trackRunWorkflow() {
|
||||||
|
@ -1010,7 +1022,12 @@ async function onRunWorkflowToNode(id: string) {
|
||||||
if (!node) return;
|
if (!node) return;
|
||||||
|
|
||||||
trackRunWorkflowToNode(node);
|
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) {
|
function trackRunWorkflowToNode(node: INodeUi) {
|
||||||
|
|
|
@ -234,7 +234,9 @@ export default defineComponent({
|
||||||
const { callDebounced } = useDebounce();
|
const { callDebounced } = useDebounce();
|
||||||
const canvasPanning = useCanvasPanning(nodeViewRootRef, { onMouseMoveEnd });
|
const canvasPanning = useCanvasPanning(nodeViewRootRef, { onMouseMoveEnd });
|
||||||
const workflowHelpers = useWorkflowHelpers({ router });
|
const workflowHelpers = useWorkflowHelpers({ router });
|
||||||
const { runWorkflow, stopCurrentExecution } = useRunWorkflow({ router });
|
const { runWorkflow, stopCurrentExecution, runWorkflowResolvePending } = useRunWorkflow({
|
||||||
|
router,
|
||||||
|
});
|
||||||
const { addBeforeUnloadEventBindings, removeBeforeUnloadEventBindings } = useBeforeUnload({
|
const { addBeforeUnloadEventBindings, removeBeforeUnloadEventBindings } = useBeforeUnload({
|
||||||
route,
|
route,
|
||||||
});
|
});
|
||||||
|
@ -254,6 +256,7 @@ export default defineComponent({
|
||||||
onMouseMoveEnd,
|
onMouseMoveEnd,
|
||||||
workflowHelpers,
|
workflowHelpers,
|
||||||
runWorkflow,
|
runWorkflow,
|
||||||
|
runWorkflowResolvePending,
|
||||||
stopCurrentExecution,
|
stopCurrentExecution,
|
||||||
callDebounced,
|
callDebounced,
|
||||||
...useCanvasMouseSelect(),
|
...useCanvasMouseSelect(),
|
||||||
|
@ -422,7 +425,12 @@ export default defineComponent({
|
||||||
return this.workflowsStore.getWorkflowExecution;
|
return this.workflowsStore.getWorkflowExecution;
|
||||||
},
|
},
|
||||||
workflowRunning(): boolean {
|
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 {
|
currentWorkflow(): string {
|
||||||
return this.$route.params.name?.toString() || this.workflowsStore.workflowId;
|
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);
|
this.$telemetry.track('User clicked execute node button', telemetryPayload);
|
||||||
void this.externalHooks.run('nodeView.onRunNode', 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() {
|
async onOpenChat() {
|
||||||
const telemetryPayload = {
|
const telemetryPayload = {
|
||||||
|
@ -857,6 +870,7 @@ export default defineComponent({
|
||||||
void this.externalHooks.run('nodeView.onOpenChat', telemetryPayload);
|
void this.externalHooks.run('nodeView.onOpenChat', telemetryPayload);
|
||||||
this.uiStore.openModal(WORKFLOW_LM_CHAT_MODAL_KEY);
|
this.uiStore.openModal(WORKFLOW_LM_CHAT_MODAL_KEY);
|
||||||
},
|
},
|
||||||
|
|
||||||
async onRunWorkflow() {
|
async onRunWorkflow() {
|
||||||
void this.workflowHelpers.getWorkflowDataToSave().then((workflowData) => {
|
void this.workflowHelpers.getWorkflowDataToSave().then((workflowData) => {
|
||||||
const telemetryPayload = {
|
const telemetryPayload = {
|
||||||
|
@ -873,7 +887,12 @@ export default defineComponent({
|
||||||
void this.externalHooks.run('nodeView.onRunWorkflow', telemetryPayload);
|
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();
|
this.refreshEndpointsErrorsState();
|
||||||
},
|
},
|
||||||
resetEndpointsErrors() {
|
resetEndpointsErrors() {
|
||||||
|
@ -932,7 +951,7 @@ export default defineComponent({
|
||||||
dangerouslyUseHTMLString: true,
|
dangerouslyUseHTMLString: true,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
clearExecutionData() {
|
async clearExecutionData() {
|
||||||
this.workflowsStore.workflowExecutionData = null;
|
this.workflowsStore.workflowExecutionData = null;
|
||||||
this.nodeHelpers.updateNodesExecutionIssues();
|
this.nodeHelpers.updateNodesExecutionIssues();
|
||||||
},
|
},
|
||||||
|
|
|
@ -238,14 +238,6 @@ export class Wait extends Webhook {
|
||||||
inputs: [NodeConnectionType.Main],
|
inputs: [NodeConnectionType.Main],
|
||||||
outputs: [NodeConnectionType.Main],
|
outputs: [NodeConnectionType.Main],
|
||||||
credentials: credentialsProperty(this.authPropertyName),
|
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: [
|
webhooks: [
|
||||||
{
|
{
|
||||||
...defaultWebhookDescription,
|
...defaultWebhookDescription,
|
||||||
|
|
Loading…
Reference in a new issue