mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
feat(editor): Migrate workflowRun mixin to useRunWorkflow composable (no-changelog) (#8711)
This commit is contained in:
parent
f9a99ec029
commit
3cbe1e2136
|
@ -71,7 +71,7 @@ export default defineComponent({
|
||||||
props: ['modalName', 'isActive', 'data'],
|
props: ['modalName', 'isActive', 'data'],
|
||||||
setup() {
|
setup() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const workflowHelpers = useWorkflowHelpers(router);
|
const workflowHelpers = useWorkflowHelpers({ router });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...useToast(),
|
...useToast(),
|
||||||
|
|
|
@ -64,7 +64,7 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
setup() {
|
setup() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const workflowHelpers = useWorkflowHelpers(router);
|
const workflowHelpers = useWorkflowHelpers({ router });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
workflowHelpers,
|
workflowHelpers,
|
||||||
|
|
|
@ -84,7 +84,7 @@ export default defineComponent({
|
||||||
setup() {
|
setup() {
|
||||||
const externalHooks = useExternalHooks();
|
const externalHooks = useExternalHooks();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const workflowHelpers = useWorkflowHelpers(router);
|
const workflowHelpers = useWorkflowHelpers({ router });
|
||||||
const { callDebounced } = useDebounce();
|
const { callDebounced } = useDebounce();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -217,7 +217,7 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
setup() {
|
setup() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const workflowHelpers = useWorkflowHelpers(router);
|
const workflowHelpers = useWorkflowHelpers({ router });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
workflowHelpers,
|
workflowHelpers,
|
||||||
|
|
|
@ -209,7 +209,7 @@ export default defineComponent({
|
||||||
const { activeNode } = storeToRefs(ndvStore);
|
const { activeNode } = storeToRefs(ndvStore);
|
||||||
const pinnedData = usePinnedData(activeNode);
|
const pinnedData = usePinnedData(activeNode);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const workflowHelpers = useWorkflowHelpers(router);
|
const workflowHelpers = useWorkflowHelpers({ router });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
externalHooks,
|
externalHooks,
|
||||||
|
|
|
@ -34,7 +34,6 @@ import {
|
||||||
} from '@/constants';
|
} from '@/constants';
|
||||||
import type { INodeUi } from '@/Interface';
|
import type { INodeUi } from '@/Interface';
|
||||||
import type { INodeTypeDescription } from 'n8n-workflow';
|
import type { INodeTypeDescription } from 'n8n-workflow';
|
||||||
import { workflowRun } from '@/mixins/workflowRun';
|
|
||||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
import { useNDVStore } from '@/stores/ndv.store';
|
import { useNDVStore } from '@/stores/ndv.store';
|
||||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||||
|
@ -43,9 +42,11 @@ import { useToast } from '@/composables/useToast';
|
||||||
import { useExternalHooks } from '@/composables/useExternalHooks';
|
import { useExternalHooks } from '@/composables/useExternalHooks';
|
||||||
import { nodeViewEventBus } from '@/event-bus';
|
import { nodeViewEventBus } from '@/event-bus';
|
||||||
import { usePinnedData } from '@/composables/usePinnedData';
|
import { usePinnedData } from '@/composables/usePinnedData';
|
||||||
|
import { useRunWorkflow } from '@/composables/useRunWorkflow';
|
||||||
|
import { useUIStore } from '@/stores/ui.store';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
mixins: [workflowRun],
|
|
||||||
inheritAttrs: false,
|
inheritAttrs: false,
|
||||||
props: {
|
props: {
|
||||||
nodeName: {
|
nodeName: {
|
||||||
|
@ -76,23 +77,24 @@ export default defineComponent({
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
setup(props, ctx) {
|
setup(props) {
|
||||||
|
const router = useRouter();
|
||||||
const workflowsStore = useWorkflowsStore();
|
const workflowsStore = useWorkflowsStore();
|
||||||
const node = workflowsStore.getNodeByName(props.nodeName);
|
const node = workflowsStore.getNodeByName(props.nodeName);
|
||||||
const pinnedData = usePinnedData(node);
|
const pinnedData = usePinnedData(node);
|
||||||
const externalHooks = useExternalHooks();
|
const externalHooks = useExternalHooks();
|
||||||
|
const { runWorkflow } = useRunWorkflow({ router });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
externalHooks,
|
externalHooks,
|
||||||
pinnedData,
|
pinnedData,
|
||||||
|
runWorkflow,
|
||||||
...useToast(),
|
...useToast(),
|
||||||
...useMessage(),
|
...useMessage(),
|
||||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
|
||||||
...workflowRun.setup?.(props, ctx),
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapStores(useNodeTypesStore, useNDVStore, useWorkflowsStore),
|
...mapStores(useNodeTypesStore, useNDVStore, useWorkflowsStore, useUIStore),
|
||||||
node(): INodeUi | null {
|
node(): INodeUi | null {
|
||||||
return this.workflowsStore.getNodeByName(this.nodeName);
|
return this.workflowsStore.getNodeByName(this.nodeName);
|
||||||
},
|
},
|
||||||
|
|
|
@ -75,7 +75,7 @@ export default defineComponent({
|
||||||
setup() {
|
setup() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const clipboard = useClipboard();
|
const clipboard = useClipboard();
|
||||||
const workflowHelpers = useWorkflowHelpers(router);
|
const workflowHelpers = useWorkflowHelpers({ router });
|
||||||
return {
|
return {
|
||||||
clipboard,
|
clipboard,
|
||||||
workflowHelpers,
|
workflowHelpers,
|
||||||
|
|
|
@ -619,7 +619,7 @@ export default defineComponent({
|
||||||
const nodeHelpers = useNodeHelpers();
|
const nodeHelpers = useNodeHelpers();
|
||||||
const { callDebounced } = useDebounce();
|
const { callDebounced } = useDebounce();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const workflowHelpers = useWorkflowHelpers(router);
|
const workflowHelpers = useWorkflowHelpers({ router });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
externalHooks,
|
externalHooks,
|
||||||
|
|
|
@ -251,7 +251,7 @@ export default defineComponent({
|
||||||
const nodeHelpers = useNodeHelpers();
|
const nodeHelpers = useNodeHelpers();
|
||||||
const asyncLoadingError = ref(false);
|
const asyncLoadingError = ref(false);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const workflowHelpers = useWorkflowHelpers(router);
|
const workflowHelpers = useWorkflowHelpers({ router });
|
||||||
|
|
||||||
// This will catch errors in async components
|
// This will catch errors in async components
|
||||||
onErrorCaptured((e, component) => {
|
onErrorCaptured((e, component) => {
|
||||||
|
|
|
@ -151,7 +151,7 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
setup() {
|
setup() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const workflowHelpers = useWorkflowHelpers(router);
|
const workflowHelpers = useWorkflowHelpers({ router });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
workflowHelpers,
|
workflowHelpers,
|
||||||
|
|
|
@ -259,7 +259,7 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
setup() {
|
setup() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const workflowHelpers = useWorkflowHelpers(router);
|
const workflowHelpers = useWorkflowHelpers({ router });
|
||||||
|
|
||||||
const { callDebounced } = useDebounce();
|
const { callDebounced } = useDebounce();
|
||||||
|
|
||||||
|
|
|
@ -148,7 +148,7 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
setup() {
|
setup() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const workflowHelpers = useWorkflowHelpers(router);
|
const workflowHelpers = useWorkflowHelpers({ router });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
workflowHelpers,
|
workflowHelpers,
|
||||||
|
|
|
@ -63,7 +63,7 @@ export default defineComponent({
|
||||||
props: ['path', 'redactValues'],
|
props: ['path', 'redactValues'],
|
||||||
setup() {
|
setup() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const workflowHelpers = useWorkflowHelpers(router);
|
const workflowHelpers = useWorkflowHelpers({ router });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
workflowHelpers,
|
workflowHelpers,
|
||||||
|
|
|
@ -135,7 +135,6 @@ import {
|
||||||
WORKFLOW_LM_CHAT_MODAL_KEY,
|
WORKFLOW_LM_CHAT_MODAL_KEY,
|
||||||
} from '@/constants';
|
} from '@/constants';
|
||||||
|
|
||||||
import { workflowRun } from '@/mixins/workflowRun';
|
|
||||||
import { get, last } from 'lodash-es';
|
import { get, last } from 'lodash-es';
|
||||||
|
|
||||||
import { useUIStore } from '@/stores/ui.store';
|
import { useUIStore } from '@/stores/ui.store';
|
||||||
|
@ -152,6 +151,7 @@ import MessageTyping from '@n8n/chat/components/MessageTyping.vue';
|
||||||
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
|
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||||
|
import { useRunWorkflow } from '@/composables/useRunWorkflow';
|
||||||
|
|
||||||
const RunDataAi = defineAsyncComponent(
|
const RunDataAi = defineAsyncComponent(
|
||||||
async () => await import('@/components/RunDataAi/RunDataAi.vue'),
|
async () => await import('@/components/RunDataAi/RunDataAi.vue'),
|
||||||
|
@ -181,18 +181,17 @@ export default defineComponent({
|
||||||
MessageTyping,
|
MessageTyping,
|
||||||
RunDataAi,
|
RunDataAi,
|
||||||
},
|
},
|
||||||
mixins: [workflowRun],
|
setup() {
|
||||||
setup(props, ctx) {
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const externalHooks = useExternalHooks();
|
const externalHooks = useExternalHooks();
|
||||||
const workflowHelpers = useWorkflowHelpers(router);
|
const workflowHelpers = useWorkflowHelpers({ router });
|
||||||
|
const { runWorkflow } = useRunWorkflow({ router });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
runWorkflow,
|
||||||
externalHooks,
|
externalHooks,
|
||||||
workflowHelpers,
|
workflowHelpers,
|
||||||
...useToast(),
|
...useToast(),
|
||||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
|
||||||
...workflowRun.setup?.(props, ctx),
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
|
@ -350,7 +349,7 @@ export default defineComponent({
|
||||||
|
|
||||||
if (!memoryConnection) return [];
|
if (!memoryConnection) return [];
|
||||||
|
|
||||||
const nodeResultData = this.workflowsStore?.getWorkflowResultDataByNodeName(
|
const nodeResultData = this.workflowsStore.getWorkflowResultDataByNodeName(
|
||||||
memoryConnection.node,
|
memoryConnection.node,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
289
packages/editor-ui/src/composables/useRunWorkflow.test.ts
Normal file
289
packages/editor-ui/src/composables/useRunWorkflow.test.ts
Normal file
|
@ -0,0 +1,289 @@
|
||||||
|
import { useRootStore } from '@/stores/n8nRoot.store';
|
||||||
|
import { useRunWorkflow } from '@/composables/useRunWorkflow';
|
||||||
|
import { createTestingPinia } from '@pinia/testing';
|
||||||
|
import { setActivePinia } from 'pinia';
|
||||||
|
import type { IStartRunData, IWorkflowData } from '@/Interface';
|
||||||
|
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
|
import { useUIStore } from '@/stores/ui.store';
|
||||||
|
import { useToast } from '@/composables/useToast';
|
||||||
|
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
|
||||||
|
import { useNodeHelpers } from '@/composables/useNodeHelpers';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import type { IPinData, IRunData, Workflow } from 'n8n-workflow';
|
||||||
|
|
||||||
|
vi.mock('@/stores/n8nRoot.store', () => ({
|
||||||
|
useRootStore: vi.fn().mockReturnValue({ pushConnectionActive: true }),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@/stores/workflows.store', () => ({
|
||||||
|
useWorkflowsStore: vi.fn().mockReturnValue({
|
||||||
|
runWorkflow: vi.fn(),
|
||||||
|
subWorkflowExecutionError: null,
|
||||||
|
getWorkflowRunData: null,
|
||||||
|
setWorkflowExecutionData: vi.fn(),
|
||||||
|
activeExecutionId: null,
|
||||||
|
nodesIssuesExist: false,
|
||||||
|
executionWaitingForWebhook: false,
|
||||||
|
getCurrentWorkflow: vi.fn().mockReturnValue({ id: '123' }),
|
||||||
|
getNodeByName: vi.fn(),
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@/stores/ui.store', () => ({
|
||||||
|
useUIStore: vi.fn().mockReturnValue({
|
||||||
|
isActionActive: vi.fn().mockReturnValue(false),
|
||||||
|
addActiveAction: vi.fn(),
|
||||||
|
removeActiveAction: vi.fn(),
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@/composables/useTelemetry', () => ({
|
||||||
|
useTelemetry: vi.fn().mockReturnValue({ track: vi.fn() }),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@/composables/useI18n', () => ({
|
||||||
|
useI18n: vi.fn().mockReturnValue({ baseText: vi.fn().mockImplementation((key) => key) }),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@/composables/useExternalHooks', () => ({
|
||||||
|
useExternalHooks: vi.fn().mockReturnValue({
|
||||||
|
run: vi.fn(),
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@/composables/useToast', () => ({
|
||||||
|
useToast: vi.fn().mockReturnValue({
|
||||||
|
clearAllStickyNotifications: vi.fn(),
|
||||||
|
showMessage: vi.fn(),
|
||||||
|
showError: vi.fn(),
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@/composables/useWorkflowHelpers', () => ({
|
||||||
|
useWorkflowHelpers: vi.fn().mockReturnValue({
|
||||||
|
getCurrentWorkflow: vi.fn(),
|
||||||
|
checkReadyForExecution: vi.fn(),
|
||||||
|
saveCurrentWorkflow: vi.fn(),
|
||||||
|
getWorkflowDataToSave: vi.fn(),
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@/composables/useNodeHelpers', () => ({
|
||||||
|
useNodeHelpers: vi.fn().mockReturnValue({
|
||||||
|
refreshNodeIssues: vi.fn(),
|
||||||
|
updateNodesExecutionIssues: vi.fn(),
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@/composables/useTitleChange', () => ({
|
||||||
|
useTitleChange: vi.fn().mockReturnValue({ titleSet: vi.fn() }),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('vue-router', async (importOriginal) => {
|
||||||
|
const { RouterLink } = await importOriginal();
|
||||||
|
return {
|
||||||
|
RouterLink,
|
||||||
|
useRouter: vi.fn().mockReturnValue({
|
||||||
|
push: vi.fn(),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('useRunWorkflow({ router })', () => {
|
||||||
|
let rootStore: ReturnType<typeof useRootStore>;
|
||||||
|
let uiStore: ReturnType<typeof useUIStore>;
|
||||||
|
let workflowsStore: ReturnType<typeof useWorkflowsStore>;
|
||||||
|
let router: ReturnType<typeof useRouter>;
|
||||||
|
let toast: ReturnType<typeof useToast>;
|
||||||
|
let workflowHelpers: ReturnType<typeof useWorkflowHelpers>;
|
||||||
|
let nodeHelpers: ReturnType<typeof useNodeHelpers>;
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
const pinia = createTestingPinia();
|
||||||
|
|
||||||
|
setActivePinia(pinia);
|
||||||
|
|
||||||
|
rootStore = useRootStore();
|
||||||
|
uiStore = useUIStore();
|
||||||
|
workflowsStore = useWorkflowsStore();
|
||||||
|
|
||||||
|
router = useRouter();
|
||||||
|
toast = useToast();
|
||||||
|
workflowHelpers = useWorkflowHelpers({ router });
|
||||||
|
nodeHelpers = useNodeHelpers();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('runWorkflowApi()', () => {
|
||||||
|
it('should throw an error if push connection is not active', async () => {
|
||||||
|
const { runWorkflowApi } = useRunWorkflow({ router });
|
||||||
|
rootStore.pushConnectionActive = false;
|
||||||
|
|
||||||
|
await expect(runWorkflowApi({} as IStartRunData)).rejects.toThrow(
|
||||||
|
'workflowRun.noActiveConnectionToTheServer',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should successfully run a workflow', async () => {
|
||||||
|
const { runWorkflowApi } = useRunWorkflow({ router });
|
||||||
|
rootStore.pushConnectionActive = true;
|
||||||
|
|
||||||
|
const mockResponse = { executionId: '123', waitingForWebhook: false };
|
||||||
|
vi.mocked(workflowsStore).runWorkflow.mockResolvedValue(mockResponse);
|
||||||
|
|
||||||
|
const response = await runWorkflowApi({} as IStartRunData);
|
||||||
|
|
||||||
|
expect(response).toEqual(mockResponse);
|
||||||
|
expect(workflowsStore.activeExecutionId).toBe('123');
|
||||||
|
expect(workflowsStore.executionWaitingForWebhook).toBe(false);
|
||||||
|
expect(uiStore.addActiveAction).toHaveBeenCalledWith('workflowRunning');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle workflow run failure', async () => {
|
||||||
|
const { runWorkflowApi } = useRunWorkflow({ router });
|
||||||
|
|
||||||
|
rootStore.pushConnectionActive = true;
|
||||||
|
vi.mocked(workflowsStore).runWorkflow.mockRejectedValue(new Error('Failed to run workflow'));
|
||||||
|
|
||||||
|
await expect(runWorkflowApi({} as IStartRunData)).rejects.toThrow('Failed to run workflow');
|
||||||
|
expect(uiStore.removeActiveAction).toHaveBeenCalledWith('workflowRunning');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set waitingForWebhook if response indicates waiting', async () => {
|
||||||
|
const { runWorkflowApi } = useRunWorkflow({ router });
|
||||||
|
|
||||||
|
rootStore.pushConnectionActive = true;
|
||||||
|
const mockResponse = { executionId: '123', waitingForWebhook: true };
|
||||||
|
vi.mocked(workflowsStore).runWorkflow.mockResolvedValue(mockResponse);
|
||||||
|
|
||||||
|
const response = await runWorkflowApi({} as IStartRunData);
|
||||||
|
|
||||||
|
expect(response).toEqual(mockResponse);
|
||||||
|
expect(workflowsStore.executionWaitingForWebhook).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('runWorkflow()', () => {
|
||||||
|
it('should return undefined if UI action "workflowRunning" is active', async () => {
|
||||||
|
const { runWorkflow } = useRunWorkflow({ router });
|
||||||
|
vi.mocked(uiStore).isActionActive.mockReturnValue(true);
|
||||||
|
const result = await runWorkflow({});
|
||||||
|
expect(result).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle workflow issues correctly', async () => {
|
||||||
|
const { runWorkflow } = useRunWorkflow({ router });
|
||||||
|
|
||||||
|
vi.mocked(uiStore).isActionActive.mockReturnValue(false);
|
||||||
|
vi.mocked(workflowHelpers).getCurrentWorkflow.mockReturnValue({
|
||||||
|
name: 'Test Workflow',
|
||||||
|
} as unknown as Workflow);
|
||||||
|
vi.mocked(workflowsStore).nodesIssuesExist = true;
|
||||||
|
vi.mocked(nodeHelpers).refreshNodeIssues.mockImplementation(() => {});
|
||||||
|
vi.mocked(workflowHelpers).checkReadyForExecution.mockReturnValue({
|
||||||
|
someNode: { issues: { input: ['issue'] } },
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await runWorkflow({});
|
||||||
|
expect(result).toBeUndefined();
|
||||||
|
expect(toast.showMessage).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should execute workflow successfully', async () => {
|
||||||
|
const mockExecutionResponse = { executionId: '123' };
|
||||||
|
const { runWorkflow } = useRunWorkflow({ router });
|
||||||
|
|
||||||
|
vi.mocked(rootStore).pushConnectionActive = true;
|
||||||
|
vi.mocked(workflowsStore).runWorkflow.mockResolvedValue(mockExecutionResponse);
|
||||||
|
vi.mocked(workflowsStore).nodesIssuesExist = false;
|
||||||
|
vi.mocked(workflowHelpers).getCurrentWorkflow.mockReturnValue({
|
||||||
|
name: 'Test Workflow',
|
||||||
|
} as Workflow);
|
||||||
|
vi.mocked(nodeHelpers).refreshNodeIssues.mockImplementation(() => {});
|
||||||
|
vi.mocked(workflowHelpers).getWorkflowDataToSave.mockResolvedValue({
|
||||||
|
id: 'workflowId',
|
||||||
|
nodes: [],
|
||||||
|
} as unknown as IWorkflowData);
|
||||||
|
vi.mocked(workflowsStore).getWorkflowRunData = {
|
||||||
|
NodeName: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await runWorkflow({});
|
||||||
|
expect(result).toEqual(mockExecutionResponse);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('consolidateRunDataAndStartNodes()', () => {
|
||||||
|
it('should return empty runData and startNodeNames if runData is null', () => {
|
||||||
|
const { consolidateRunDataAndStartNodes } = useRunWorkflow({ router });
|
||||||
|
const workflowMock = {
|
||||||
|
getParentNodes: vi.fn(),
|
||||||
|
nodes: {},
|
||||||
|
} as unknown as Workflow;
|
||||||
|
|
||||||
|
const result = consolidateRunDataAndStartNodes([], null, undefined, workflowMock);
|
||||||
|
expect(result).toEqual({ runData: undefined, startNodeNames: [] });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return correct startNodeNames and newRunData for given directParentNodes and runData', () => {
|
||||||
|
const { consolidateRunDataAndStartNodes } = useRunWorkflow({ router });
|
||||||
|
const directParentNodes = ['node1', 'node2'];
|
||||||
|
const runData = {
|
||||||
|
node2: [{ data: { main: [[{ json: { value: 'data2' } }]] } }],
|
||||||
|
node3: [{ data: { main: [[{ json: { value: 'data3' } }]] } }],
|
||||||
|
} as unknown as IRunData;
|
||||||
|
const pinData: IPinData = {
|
||||||
|
node2: [{ json: { value: 'data2' } }],
|
||||||
|
};
|
||||||
|
const workflowMock = {
|
||||||
|
getParentNodes: vi.fn().mockImplementation((node) => {
|
||||||
|
if (node === 'node1') return ['node3'];
|
||||||
|
return [];
|
||||||
|
}),
|
||||||
|
nodes: {
|
||||||
|
node1: { disabled: false },
|
||||||
|
node2: { disabled: false },
|
||||||
|
node3: { disabled: true },
|
||||||
|
},
|
||||||
|
} as unknown as Workflow;
|
||||||
|
|
||||||
|
const result = consolidateRunDataAndStartNodes(
|
||||||
|
directParentNodes,
|
||||||
|
runData,
|
||||||
|
pinData,
|
||||||
|
workflowMock,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.startNodeNames).toContain('node1');
|
||||||
|
expect(result.startNodeNames).not.toContain('node3');
|
||||||
|
expect(result.runData).toEqual(runData);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include directParentNode in startNodeNames if it has no runData or pinData', () => {
|
||||||
|
const { consolidateRunDataAndStartNodes } = useRunWorkflow({ router });
|
||||||
|
const directParentNodes = ['node1'];
|
||||||
|
const runData = {
|
||||||
|
node2: [
|
||||||
|
{
|
||||||
|
data: {
|
||||||
|
main: [[{ json: { value: 'data2' } }]],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
} as unknown as IRunData;
|
||||||
|
const workflowMock = {
|
||||||
|
getParentNodes: vi.fn().mockReturnValue([]),
|
||||||
|
nodes: { node1: { disabled: false } },
|
||||||
|
} as unknown as Workflow;
|
||||||
|
|
||||||
|
const result = consolidateRunDataAndStartNodes(
|
||||||
|
directParentNodes,
|
||||||
|
runData,
|
||||||
|
undefined,
|
||||||
|
workflowMock,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.startNodeNames).toContain('node1');
|
||||||
|
expect(result.runData).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
397
packages/editor-ui/src/composables/useRunWorkflow.ts
Normal file
397
packages/editor-ui/src/composables/useRunWorkflow.ts
Normal file
|
@ -0,0 +1,397 @@
|
||||||
|
import type {
|
||||||
|
IExecutionPushResponse,
|
||||||
|
IExecutionResponse,
|
||||||
|
IStartRunData,
|
||||||
|
IWorkflowDb,
|
||||||
|
} from '@/Interface';
|
||||||
|
import type {
|
||||||
|
IDataObject,
|
||||||
|
IRunData,
|
||||||
|
IRunExecutionData,
|
||||||
|
ITaskData,
|
||||||
|
IPinData,
|
||||||
|
IWorkflowBase,
|
||||||
|
Workflow,
|
||||||
|
StartNodeData,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
import {
|
||||||
|
NodeHelpers,
|
||||||
|
NodeConnectionType,
|
||||||
|
TelemetryHelpers,
|
||||||
|
FORM_TRIGGER_PATH_IDENTIFIER,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
|
import { useToast } from '@/composables/useToast';
|
||||||
|
import { useNodeHelpers } from '@/composables/useNodeHelpers';
|
||||||
|
|
||||||
|
import { FORM_TRIGGER_NODE_TYPE, WAIT_NODE_TYPE } from '@/constants';
|
||||||
|
import { useTitleChange } from '@/composables/useTitleChange';
|
||||||
|
import { useRootStore } from '@/stores/n8nRoot.store';
|
||||||
|
import { useUIStore } from '@/stores/ui.store';
|
||||||
|
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
|
import { openPopUpWindow } from '@/utils/executionUtils';
|
||||||
|
import { useExternalHooks } from '@/composables/useExternalHooks';
|
||||||
|
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
|
||||||
|
import type { useRouter } from 'vue-router';
|
||||||
|
import { isEmpty } from '@/utils/typesUtils';
|
||||||
|
import { useI18n } from '@/composables/useI18n';
|
||||||
|
import { useTelemetry } from '@/composables/useTelemetry';
|
||||||
|
import { get } from 'lodash-es';
|
||||||
|
|
||||||
|
export function useRunWorkflow(options: { router: ReturnType<typeof useRouter> }) {
|
||||||
|
const nodeHelpers = useNodeHelpers();
|
||||||
|
const workflowHelpers = useWorkflowHelpers({ router: options.router });
|
||||||
|
const i18n = useI18n();
|
||||||
|
const telemetry = useTelemetry();
|
||||||
|
const toast = useToast();
|
||||||
|
const { titleSet } = useTitleChange();
|
||||||
|
|
||||||
|
const rootStore = useRootStore();
|
||||||
|
const uiStore = useUIStore();
|
||||||
|
const workflowsStore = useWorkflowsStore();
|
||||||
|
|
||||||
|
// Starts to execute a workflow on server
|
||||||
|
async function runWorkflowApi(runData: IStartRunData): Promise<IExecutionPushResponse> {
|
||||||
|
if (!rootStore.pushConnectionActive) {
|
||||||
|
// Do not start if the connection to server is not active
|
||||||
|
// because then it can not receive the data as it executes.
|
||||||
|
throw new Error(i18n.baseText('workflowRun.noActiveConnectionToTheServer'));
|
||||||
|
}
|
||||||
|
|
||||||
|
workflowsStore.subWorkflowExecutionError = null;
|
||||||
|
|
||||||
|
uiStore.addActiveAction('workflowRunning');
|
||||||
|
|
||||||
|
let response: IExecutionPushResponse;
|
||||||
|
|
||||||
|
try {
|
||||||
|
response = await workflowsStore.runWorkflow(runData);
|
||||||
|
} catch (error) {
|
||||||
|
uiStore.removeActiveAction('workflowRunning');
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.executionId !== undefined) {
|
||||||
|
workflowsStore.activeExecutionId = response.executionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.waitingForWebhook === true) {
|
||||||
|
workflowsStore.executionWaitingForWebhook = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runWorkflow(options: {
|
||||||
|
destinationNode?: string;
|
||||||
|
triggerNode?: string;
|
||||||
|
nodeData?: ITaskData;
|
||||||
|
source?: string;
|
||||||
|
}): Promise<IExecutionPushResponse | undefined> {
|
||||||
|
const workflow = workflowHelpers.getCurrentWorkflow();
|
||||||
|
|
||||||
|
if (uiStore.isActionActive('workflowRunning')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
titleSet(workflow.name as string, 'EXECUTING');
|
||||||
|
|
||||||
|
toast.clearAllStickyNotifications();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Check first if the workflow has any issues before execute it
|
||||||
|
nodeHelpers.refreshNodeIssues();
|
||||||
|
const issuesExist = workflowsStore.nodesIssuesExist;
|
||||||
|
if (issuesExist) {
|
||||||
|
// If issues exist get all of the issues of all nodes
|
||||||
|
const workflowIssues = workflowHelpers.checkReadyForExecution(
|
||||||
|
workflow,
|
||||||
|
options.destinationNode,
|
||||||
|
);
|
||||||
|
if (workflowIssues !== null) {
|
||||||
|
const errorMessages = [];
|
||||||
|
let nodeIssues: string[];
|
||||||
|
const trackNodeIssues: Array<{
|
||||||
|
node_type: string;
|
||||||
|
error: string;
|
||||||
|
}> = [];
|
||||||
|
const trackErrorNodeTypes: string[] = [];
|
||||||
|
for (const nodeName of Object.keys(workflowIssues)) {
|
||||||
|
nodeIssues = NodeHelpers.nodeIssuesToString(workflowIssues[nodeName]);
|
||||||
|
let issueNodeType = 'UNKNOWN';
|
||||||
|
const issueNode = workflowsStore.getNodeByName(nodeName);
|
||||||
|
|
||||||
|
if (issueNode) {
|
||||||
|
issueNodeType = issueNode.type;
|
||||||
|
}
|
||||||
|
|
||||||
|
trackErrorNodeTypes.push(issueNodeType);
|
||||||
|
const trackNodeIssue = {
|
||||||
|
node_type: issueNodeType,
|
||||||
|
error: '',
|
||||||
|
caused_by_credential: !!workflowIssues[nodeName].credentials,
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const nodeIssue of nodeIssues) {
|
||||||
|
errorMessages.push(
|
||||||
|
`<a data-action='openNodeDetail' data-action-parameter-node='${nodeName}'>${nodeName}</a>: ${nodeIssue}`,
|
||||||
|
);
|
||||||
|
trackNodeIssue.error = trackNodeIssue.error.concat(', ', nodeIssue);
|
||||||
|
}
|
||||||
|
trackNodeIssues.push(trackNodeIssue);
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.showMessage({
|
||||||
|
title: i18n.baseText('workflowRun.showMessage.title'),
|
||||||
|
message: errorMessages.join('<br />'),
|
||||||
|
type: 'error',
|
||||||
|
duration: 0,
|
||||||
|
});
|
||||||
|
titleSet(workflow.name as string, 'ERROR');
|
||||||
|
void useExternalHooks().run('workflowRun.runError', {
|
||||||
|
errorMessages,
|
||||||
|
nodeName: options.destinationNode,
|
||||||
|
});
|
||||||
|
|
||||||
|
await workflowHelpers.getWorkflowDataToSave().then((workflowData) => {
|
||||||
|
telemetry.track('Workflow execution preflight failed', {
|
||||||
|
workflow_id: workflow.id,
|
||||||
|
workflow_name: workflow.name,
|
||||||
|
execution_type: options.destinationNode || options.triggerNode ? 'node' : 'workflow',
|
||||||
|
node_graph_string: JSON.stringify(
|
||||||
|
TelemetryHelpers.generateNodesGraph(
|
||||||
|
workflowData as IWorkflowBase,
|
||||||
|
workflowHelpers.getNodeTypes(),
|
||||||
|
).nodeGraph,
|
||||||
|
),
|
||||||
|
error_node_types: JSON.stringify(trackErrorNodeTypes),
|
||||||
|
errors: JSON.stringify(trackNodeIssues),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the direct parents of the node
|
||||||
|
let directParentNodes: string[] = [];
|
||||||
|
if (options.destinationNode !== undefined) {
|
||||||
|
directParentNodes = workflow.getParentNodes(
|
||||||
|
options.destinationNode,
|
||||||
|
NodeConnectionType.Main,
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const runData = workflowsStore.getWorkflowRunData;
|
||||||
|
|
||||||
|
if (workflowsStore.isNewWorkflow) {
|
||||||
|
await workflowHelpers.saveCurrentWorkflow();
|
||||||
|
}
|
||||||
|
|
||||||
|
const workflowData = await workflowHelpers.getWorkflowDataToSave();
|
||||||
|
|
||||||
|
const consolidatedData = consolidateRunDataAndStartNodes(
|
||||||
|
directParentNodes,
|
||||||
|
runData,
|
||||||
|
workflowData.pinData,
|
||||||
|
workflow,
|
||||||
|
);
|
||||||
|
|
||||||
|
const { startNodeNames } = consolidatedData;
|
||||||
|
let { runData: newRunData } = consolidatedData;
|
||||||
|
let executedNode: string | undefined;
|
||||||
|
if (
|
||||||
|
startNodeNames.length === 0 &&
|
||||||
|
'destinationNode' in options &&
|
||||||
|
options.destinationNode !== undefined
|
||||||
|
) {
|
||||||
|
executedNode = options.destinationNode;
|
||||||
|
startNodeNames.push(options.destinationNode);
|
||||||
|
} else if ('triggerNode' in options && 'nodeData' in options) {
|
||||||
|
startNodeNames.push(
|
||||||
|
...workflow.getChildNodes(options.triggerNode as string, NodeConnectionType.Main, 1),
|
||||||
|
);
|
||||||
|
newRunData = {
|
||||||
|
[options.triggerNode as string]: [options.nodeData],
|
||||||
|
} as IRunData;
|
||||||
|
executedNode = options.triggerNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const startNodes: StartNodeData[] = startNodeNames.map((name) => {
|
||||||
|
// Find for each start node the source data
|
||||||
|
let sourceData = get(runData, [name, 0, 'source', 0], null);
|
||||||
|
if (sourceData === null) {
|
||||||
|
const parentNodes = workflow.getParentNodes(name, NodeConnectionType.Main, 1);
|
||||||
|
const executeData = workflowHelpers.executeData(
|
||||||
|
parentNodes,
|
||||||
|
name,
|
||||||
|
NodeConnectionType.Main,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
sourceData = get(executeData, ['source', NodeConnectionType.Main, 0], null);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
sourceData,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const startRunData: IStartRunData = {
|
||||||
|
workflowData,
|
||||||
|
runData: newRunData,
|
||||||
|
pinData: workflowData.pinData,
|
||||||
|
startNodes,
|
||||||
|
};
|
||||||
|
if ('destinationNode' in options) {
|
||||||
|
startRunData.destinationNode = options.destinationNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init the execution data to represent the start of the execution
|
||||||
|
// that data which gets reused is already set and data of newly executed
|
||||||
|
// nodes can be added as it gets pushed in
|
||||||
|
const executionData: IExecutionResponse = {
|
||||||
|
id: '__IN_PROGRESS__',
|
||||||
|
finished: false,
|
||||||
|
mode: 'manual',
|
||||||
|
status: 'running',
|
||||||
|
startedAt: new Date(),
|
||||||
|
stoppedAt: undefined,
|
||||||
|
workflowId: workflow.id,
|
||||||
|
executedNode,
|
||||||
|
data: {
|
||||||
|
resultData: {
|
||||||
|
runData: newRunData || {},
|
||||||
|
pinData: workflowData.pinData,
|
||||||
|
workflowData,
|
||||||
|
},
|
||||||
|
} as IRunExecutionData,
|
||||||
|
workflowData: {
|
||||||
|
id: workflowsStore.workflowId,
|
||||||
|
name: workflowData.name!,
|
||||||
|
active: workflowData.active!,
|
||||||
|
createdAt: 0,
|
||||||
|
updatedAt: 0,
|
||||||
|
...workflowData,
|
||||||
|
} as IWorkflowDb,
|
||||||
|
};
|
||||||
|
workflowsStore.setWorkflowExecutionData(executionData);
|
||||||
|
nodeHelpers.updateNodesExecutionIssues();
|
||||||
|
|
||||||
|
const runWorkflowApiResponse = await runWorkflowApi(startRunData);
|
||||||
|
|
||||||
|
for (const node of workflowData.nodes) {
|
||||||
|
if (![FORM_TRIGGER_NODE_TYPE, WAIT_NODE_TYPE].includes(node.type)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
options.destinationNode &&
|
||||||
|
options.destinationNode !== node.name &&
|
||||||
|
!directParentNodes.includes(node.name)
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.name === options.destinationNode || !node.disabled) {
|
||||||
|
let testUrl = '';
|
||||||
|
|
||||||
|
if (node.type === FORM_TRIGGER_NODE_TYPE && node.typeVersion === 1) {
|
||||||
|
const webhookPath = (node.parameters.path as string) || node.webhookId;
|
||||||
|
testUrl = `${rootStore.getWebhookTestUrl}/${webhookPath}/${FORM_TRIGGER_PATH_IDENTIFIER}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.type === FORM_TRIGGER_NODE_TYPE && node.typeVersion > 1) {
|
||||||
|
const webhookPath = (node.parameters.path as string) || node.webhookId;
|
||||||
|
testUrl = `${rootStore.getFormTestUrl}/${webhookPath}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
node.type === WAIT_NODE_TYPE &&
|
||||||
|
node.parameters.resume === 'form' &&
|
||||||
|
runWorkflowApiResponse.executionId
|
||||||
|
) {
|
||||||
|
const workflowTriggerNodes = workflow.getTriggerNodes().map((node) => node.name);
|
||||||
|
|
||||||
|
const showForm =
|
||||||
|
options.destinationNode === node.name ||
|
||||||
|
directParentNodes.includes(node.name) ||
|
||||||
|
workflowTriggerNodes.some((triggerNode) =>
|
||||||
|
workflowsStore.isNodeInOutgoingNodeConnections(triggerNode, node.name),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!showForm) continue;
|
||||||
|
|
||||||
|
const { webhookSuffix } = (node.parameters.options || {}) as IDataObject;
|
||||||
|
const suffix = webhookSuffix ? `/${webhookSuffix}` : '';
|
||||||
|
testUrl = `${rootStore.getFormWaitingUrl}/${runWorkflowApiResponse.executionId}${suffix}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (testUrl) openPopUpWindow(testUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await useExternalHooks().run('workflowRun.runWorkflow', {
|
||||||
|
nodeName: options.destinationNode,
|
||||||
|
source: options.source,
|
||||||
|
});
|
||||||
|
|
||||||
|
return runWorkflowApiResponse;
|
||||||
|
} catch (error) {
|
||||||
|
titleSet(workflow.name as string, 'ERROR');
|
||||||
|
toast.showError(error, i18n.baseText('workflowRun.showError.title'));
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function consolidateRunDataAndStartNodes(
|
||||||
|
directParentNodes: string[],
|
||||||
|
runData: IRunData | null,
|
||||||
|
pinData: IPinData | undefined,
|
||||||
|
workflow: Workflow,
|
||||||
|
): { runData: IRunData | undefined; startNodeNames: string[] } {
|
||||||
|
const startNodeNames: string[] = [];
|
||||||
|
let newRunData: IRunData | undefined;
|
||||||
|
|
||||||
|
if (runData !== null && Object.keys(runData).length !== 0) {
|
||||||
|
newRunData = {};
|
||||||
|
// Go over the direct parents of the node
|
||||||
|
for (const directParentNode of directParentNodes) {
|
||||||
|
// Go over the parents of that node so that we can get a start
|
||||||
|
// node for each of the branches
|
||||||
|
const parentNodes = workflow.getParentNodes(directParentNode, NodeConnectionType.Main);
|
||||||
|
|
||||||
|
// Add also the enabled direct parent to be checked
|
||||||
|
if (workflow.nodes[directParentNode].disabled) continue;
|
||||||
|
|
||||||
|
parentNodes.push(directParentNode);
|
||||||
|
|
||||||
|
for (const parentNode of parentNodes) {
|
||||||
|
if (!runData[parentNode]?.length && !pinData?.[parentNode]?.length) {
|
||||||
|
// When we hit a node which has no data we stop and set it
|
||||||
|
// as a start node the execution from and then go on with other
|
||||||
|
// direct input nodes
|
||||||
|
startNodeNames.push(parentNode);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (runData[parentNode]) {
|
||||||
|
newRunData[parentNode] = runData[parentNode]?.slice(0, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isEmpty(newRunData)) {
|
||||||
|
// If there is no data for any of the parent nodes make sure
|
||||||
|
// that run data is empty that it runs regularly
|
||||||
|
newRunData = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { runData: newRunData, startNodeNames };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
consolidateRunDataAndStartNodes,
|
||||||
|
runWorkflow,
|
||||||
|
runWorkflowApi,
|
||||||
|
};
|
||||||
|
}
|
|
@ -30,12 +30,17 @@ export function useToast() {
|
||||||
const externalHooks = useExternalHooks();
|
const externalHooks = useExternalHooks();
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
|
|
||||||
function showMessage(messageData: NotificationOptions, track = true) {
|
function showMessage(messageData: Partial<NotificationOptions>, track = true) {
|
||||||
messageData = { ...messageDefaults, ...messageData };
|
messageData = { ...messageDefaults, ...messageData };
|
||||||
messageData.message =
|
|
||||||
typeof messageData.message === 'string'
|
Object.defineProperty(messageData, 'message', {
|
||||||
? sanitizeHtml(messageData.message)
|
value:
|
||||||
: messageData.message;
|
typeof messageData.message === 'string'
|
||||||
|
? sanitizeHtml(messageData.message)
|
||||||
|
: messageData.message,
|
||||||
|
writable: true,
|
||||||
|
enumerable: true,
|
||||||
|
});
|
||||||
|
|
||||||
const notification = Notification(messageData);
|
const notification = Notification(messageData);
|
||||||
|
|
||||||
|
@ -116,7 +121,7 @@ export function useToast() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function showError(e: Error | unknown, title: string, message?: string) {
|
function showError(e: Error | unknown, title: string, message?: string) {
|
||||||
const error = e as Error;
|
const error = e as NotificationErrorWithNodeAndDescription;
|
||||||
const messageLine = message ? `${message}<br/>` : '';
|
const messageLine = message ? `${message}<br/>` : '';
|
||||||
showMessage(
|
showMessage(
|
||||||
{
|
{
|
||||||
|
@ -124,7 +129,7 @@ export function useToast() {
|
||||||
message: `
|
message: `
|
||||||
${messageLine}
|
${messageLine}
|
||||||
<i>${error.message}</i>
|
<i>${error.message}</i>
|
||||||
${collapsableDetails(error as NotificationErrorWithNodeAndDescription)}`,
|
${collapsableDetails(error)}`,
|
||||||
type: 'error',
|
type: 'error',
|
||||||
duration: 0,
|
duration: 0,
|
||||||
},
|
},
|
||||||
|
@ -170,7 +175,7 @@ export function useToast() {
|
||||||
function showNotificationForViews(views: VIEWS[]) {
|
function showNotificationForViews(views: VIEWS[]) {
|
||||||
const notifications: NotificationOptions[] = [];
|
const notifications: NotificationOptions[] = [];
|
||||||
views.forEach((view) => {
|
views.forEach((view) => {
|
||||||
notifications.push(...uiStore.getNotificationsForView(view));
|
notifications.push(...(uiStore.getNotificationsForView(view) as NotificationOptions[]));
|
||||||
});
|
});
|
||||||
if (notifications.length) {
|
if (notifications.length) {
|
||||||
notifications.forEach(async (notification) => {
|
notifications.forEach(async (notification) => {
|
||||||
|
|
|
@ -71,7 +71,7 @@ import { useCanvasStore } from '@/stores/canvas.store';
|
||||||
import { useSourceControlStore } from '@/stores/sourceControl.store';
|
import { useSourceControlStore } from '@/stores/sourceControl.store';
|
||||||
import { tryToParseNumber } from '@/utils/typesUtils';
|
import { tryToParseNumber } from '@/utils/typesUtils';
|
||||||
import { useI18n } from '@/composables/useI18n';
|
import { useI18n } from '@/composables/useI18n';
|
||||||
import type { Router } from 'vue-router';
|
import type { useRouter } from 'vue-router';
|
||||||
import { useTelemetry } from '@/composables/useTelemetry';
|
import { useTelemetry } from '@/composables/useTelemetry';
|
||||||
|
|
||||||
export function resolveParameter(
|
export function resolveParameter(
|
||||||
|
@ -451,7 +451,8 @@ export function executeData(
|
||||||
return executeData;
|
return executeData;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useWorkflowHelpers(router: Router) {
|
export function useWorkflowHelpers(options: { router: ReturnType<typeof useRouter> }) {
|
||||||
|
const router = options.router;
|
||||||
const nodeTypesStore = useNodeTypesStore();
|
const nodeTypesStore = useNodeTypesStore();
|
||||||
const rootStore = useRootStore();
|
const rootStore = useRootStore();
|
||||||
const templatesStore = useTemplatesStore();
|
const templatesStore = useTemplatesStore();
|
||||||
|
|
|
@ -204,7 +204,7 @@ export const expressionManager = defineComponent({
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const ndvStore = useNDVStore();
|
const ndvStore = useNDVStore();
|
||||||
const workflowHelpers = useWorkflowHelpers(this.$router);
|
const workflowHelpers = useWorkflowHelpers({ router: this.$router });
|
||||||
if (!ndvStore.activeNode) {
|
if (!ndvStore.activeNode) {
|
||||||
// e.g. credential modal
|
// e.g. credential modal
|
||||||
result.resolved = Expression.resolveWithoutWorkflow(resolvable, this.additionalData);
|
result.resolved = Expression.resolveWithoutWorkflow(resolvable, this.additionalData);
|
||||||
|
|
|
@ -44,7 +44,7 @@ import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
|
||||||
export const pushConnection = defineComponent({
|
export const pushConnection = defineComponent({
|
||||||
setup() {
|
setup() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const workflowHelpers = useWorkflowHelpers(router);
|
const workflowHelpers = useWorkflowHelpers({ router });
|
||||||
const nodeHelpers = useNodeHelpers();
|
const nodeHelpers = useNodeHelpers();
|
||||||
return {
|
return {
|
||||||
...useTitleChange(),
|
...useTitleChange(),
|
||||||
|
|
|
@ -19,7 +19,7 @@ import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
|
||||||
export const workflowActivate = defineComponent({
|
export const workflowActivate = defineComponent({
|
||||||
setup() {
|
setup() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const workflowHelpers = useWorkflowHelpers(router);
|
const workflowHelpers = useWorkflowHelpers({ router });
|
||||||
return {
|
return {
|
||||||
workflowHelpers,
|
workflowHelpers,
|
||||||
...useToast(),
|
...useToast(),
|
||||||
|
|
|
@ -1,393 +0,0 @@
|
||||||
import type { IExecutionPushResponse, IExecutionResponse, IStartRunData } from '@/Interface';
|
|
||||||
import { mapStores } from 'pinia';
|
|
||||||
import { defineComponent } from 'vue';
|
|
||||||
import { get } from 'lodash-es';
|
|
||||||
|
|
||||||
import type {
|
|
||||||
IDataObject,
|
|
||||||
IRunData,
|
|
||||||
IRunExecutionData,
|
|
||||||
ITaskData,
|
|
||||||
IPinData,
|
|
||||||
IWorkflowBase,
|
|
||||||
Workflow,
|
|
||||||
StartNodeData,
|
|
||||||
} from 'n8n-workflow';
|
|
||||||
import {
|
|
||||||
NodeHelpers,
|
|
||||||
NodeConnectionType,
|
|
||||||
TelemetryHelpers,
|
|
||||||
FORM_TRIGGER_PATH_IDENTIFIER,
|
|
||||||
} from 'n8n-workflow';
|
|
||||||
|
|
||||||
import { useToast } from '@/composables/useToast';
|
|
||||||
import { useNodeHelpers } from '@/composables/useNodeHelpers';
|
|
||||||
|
|
||||||
import { FORM_TRIGGER_NODE_TYPE, WAIT_NODE_TYPE } from '@/constants';
|
|
||||||
import { useTitleChange } from '@/composables/useTitleChange';
|
|
||||||
import { useRootStore } from '@/stores/n8nRoot.store';
|
|
||||||
import { useUIStore } from '@/stores/ui.store';
|
|
||||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
|
||||||
import { openPopUpWindow } from '@/utils/executionUtils';
|
|
||||||
import { useExternalHooks } from '@/composables/useExternalHooks';
|
|
||||||
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
|
|
||||||
import { useRouter } from 'vue-router';
|
|
||||||
import { isEmpty } from '@/utils/typesUtils';
|
|
||||||
|
|
||||||
export const consolidateRunDataAndStartNodes = (
|
|
||||||
directParentNodes: string[],
|
|
||||||
runData: IRunData | null,
|
|
||||||
pinData: IPinData | undefined,
|
|
||||||
workflow: Workflow,
|
|
||||||
): { runData: IRunData | undefined; startNodeNames: string[] } => {
|
|
||||||
const startNodeNames: string[] = [];
|
|
||||||
let newRunData: IRunData | undefined;
|
|
||||||
|
|
||||||
if (runData !== null && Object.keys(runData).length !== 0) {
|
|
||||||
newRunData = {};
|
|
||||||
// Go over the direct parents of the node
|
|
||||||
for (const directParentNode of directParentNodes) {
|
|
||||||
// Go over the parents of that node so that we can get a start
|
|
||||||
// node for each of the branches
|
|
||||||
const parentNodes = workflow.getParentNodes(directParentNode, NodeConnectionType.Main);
|
|
||||||
|
|
||||||
// Add also the enabled direct parent to be checked
|
|
||||||
if (workflow.nodes[directParentNode].disabled) continue;
|
|
||||||
|
|
||||||
parentNodes.push(directParentNode);
|
|
||||||
|
|
||||||
for (const parentNode of parentNodes) {
|
|
||||||
if (!runData[parentNode]?.length && !pinData?.[parentNode]?.length) {
|
|
||||||
// When we hit a node which has no data we stop and set it
|
|
||||||
// as a start node the execution from and then go on with other
|
|
||||||
// direct input nodes
|
|
||||||
startNodeNames.push(parentNode);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (runData[parentNode]) {
|
|
||||||
newRunData[parentNode] = runData[parentNode]?.slice(0, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isEmpty(newRunData)) {
|
|
||||||
// If there is no data for any of the parent nodes make sure
|
|
||||||
// that run data is empty that it runs regularly
|
|
||||||
newRunData = undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { runData: newRunData, startNodeNames };
|
|
||||||
};
|
|
||||||
|
|
||||||
export const workflowRun = defineComponent({
|
|
||||||
setup() {
|
|
||||||
const nodeHelpers = useNodeHelpers();
|
|
||||||
const router = useRouter();
|
|
||||||
const workflowHelpers = useWorkflowHelpers(router);
|
|
||||||
|
|
||||||
return {
|
|
||||||
...useTitleChange(),
|
|
||||||
...useToast(),
|
|
||||||
nodeHelpers,
|
|
||||||
workflowHelpers,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
...mapStores(useRootStore, useUIStore, useWorkflowsStore),
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
// Starts to executes a workflow on server.
|
|
||||||
async runWorkflowApi(runData: IStartRunData): Promise<IExecutionPushResponse> {
|
|
||||||
if (!this.rootStore.pushConnectionActive) {
|
|
||||||
// Do not start if the connection to server is not active
|
|
||||||
// because then it can not receive the data as it executes.
|
|
||||||
throw new Error(this.$locale.baseText('workflowRun.noActiveConnectionToTheServer'));
|
|
||||||
}
|
|
||||||
|
|
||||||
this.workflowsStore.subWorkflowExecutionError = null;
|
|
||||||
|
|
||||||
this.uiStore.addActiveAction('workflowRunning');
|
|
||||||
|
|
||||||
let response: IExecutionPushResponse;
|
|
||||||
|
|
||||||
try {
|
|
||||||
response = await this.workflowsStore.runWorkflow(runData);
|
|
||||||
} catch (error) {
|
|
||||||
this.uiStore.removeActiveAction('workflowRunning');
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.executionId !== undefined) {
|
|
||||||
this.workflowsStore.activeExecutionId = response.executionId;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.waitingForWebhook === true) {
|
|
||||||
this.workflowsStore.executionWaitingForWebhook = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return response;
|
|
||||||
},
|
|
||||||
|
|
||||||
async runWorkflow(
|
|
||||||
options:
|
|
||||||
| { destinationNode: string; source?: string }
|
|
||||||
| { triggerNode: string; nodeData: ITaskData; source?: string }
|
|
||||||
| { source?: string },
|
|
||||||
): Promise<IExecutionPushResponse | undefined> {
|
|
||||||
const workflow = this.workflowHelpers.getCurrentWorkflow();
|
|
||||||
|
|
||||||
if (this.uiStore.isActionActive('workflowRunning')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.titleSet(workflow.name as string, 'EXECUTING');
|
|
||||||
|
|
||||||
this.clearAllStickyNotifications();
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Check first if the workflow has any issues before execute it
|
|
||||||
this.nodeHelpers.refreshNodeIssues();
|
|
||||||
const issuesExist = this.workflowsStore.nodesIssuesExist;
|
|
||||||
if (issuesExist) {
|
|
||||||
// If issues exist get all of the issues of all nodes
|
|
||||||
const workflowIssues = this.workflowHelpers.checkReadyForExecution(
|
|
||||||
workflow,
|
|
||||||
options.destinationNode,
|
|
||||||
);
|
|
||||||
if (workflowIssues !== null) {
|
|
||||||
const errorMessages = [];
|
|
||||||
let nodeIssues: string[];
|
|
||||||
const trackNodeIssues: Array<{
|
|
||||||
node_type: string;
|
|
||||||
error: string;
|
|
||||||
}> = [];
|
|
||||||
const trackErrorNodeTypes: string[] = [];
|
|
||||||
for (const nodeName of Object.keys(workflowIssues)) {
|
|
||||||
nodeIssues = NodeHelpers.nodeIssuesToString(workflowIssues[nodeName]);
|
|
||||||
let issueNodeType = 'UNKNOWN';
|
|
||||||
const issueNode = this.workflowsStore.getNodeByName(nodeName);
|
|
||||||
|
|
||||||
if (issueNode) {
|
|
||||||
issueNodeType = issueNode.type;
|
|
||||||
}
|
|
||||||
|
|
||||||
trackErrorNodeTypes.push(issueNodeType);
|
|
||||||
const trackNodeIssue = {
|
|
||||||
node_type: issueNodeType,
|
|
||||||
error: '',
|
|
||||||
caused_by_credential: !!workflowIssues[nodeName].credentials,
|
|
||||||
};
|
|
||||||
|
|
||||||
for (const nodeIssue of nodeIssues) {
|
|
||||||
errorMessages.push(
|
|
||||||
`<a data-action='openNodeDetail' data-action-parameter-node='${nodeName}'>${nodeName}</a>: ${nodeIssue}`,
|
|
||||||
);
|
|
||||||
trackNodeIssue.error = trackNodeIssue.error.concat(', ', nodeIssue);
|
|
||||||
}
|
|
||||||
trackNodeIssues.push(trackNodeIssue);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.showMessage({
|
|
||||||
title: this.$locale.baseText('workflowRun.showMessage.title'),
|
|
||||||
message: errorMessages.join('<br />'),
|
|
||||||
type: 'error',
|
|
||||||
duration: 0,
|
|
||||||
});
|
|
||||||
this.titleSet(workflow.name as string, 'ERROR');
|
|
||||||
void useExternalHooks().run('workflowRun.runError', {
|
|
||||||
errorMessages,
|
|
||||||
nodeName: options.destinationNode,
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.workflowHelpers.getWorkflowDataToSave().then((workflowData) => {
|
|
||||||
this.$telemetry.track('Workflow execution preflight failed', {
|
|
||||||
workflow_id: workflow.id,
|
|
||||||
workflow_name: workflow.name,
|
|
||||||
execution_type:
|
|
||||||
options.destinationNode || options.triggerNode ? 'node' : 'workflow',
|
|
||||||
node_graph_string: JSON.stringify(
|
|
||||||
TelemetryHelpers.generateNodesGraph(
|
|
||||||
workflowData as IWorkflowBase,
|
|
||||||
this.workflowHelpers.getNodeTypes(),
|
|
||||||
).nodeGraph,
|
|
||||||
),
|
|
||||||
error_node_types: JSON.stringify(trackErrorNodeTypes),
|
|
||||||
errors: JSON.stringify(trackNodeIssues),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the direct parents of the node
|
|
||||||
let directParentNodes: string[] = [];
|
|
||||||
if (options.destinationNode !== undefined) {
|
|
||||||
directParentNodes = workflow.getParentNodes(
|
|
||||||
options.destinationNode,
|
|
||||||
NodeConnectionType.Main,
|
|
||||||
1,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const runData = this.workflowsStore.getWorkflowRunData;
|
|
||||||
|
|
||||||
if (this.workflowsStore.isNewWorkflow) {
|
|
||||||
await this.workflowHelpers.saveCurrentWorkflow();
|
|
||||||
}
|
|
||||||
|
|
||||||
const workflowData = await this.workflowHelpers.getWorkflowDataToSave();
|
|
||||||
|
|
||||||
const consolidatedData = consolidateRunDataAndStartNodes(
|
|
||||||
directParentNodes,
|
|
||||||
runData,
|
|
||||||
workflowData.pinData,
|
|
||||||
workflow,
|
|
||||||
);
|
|
||||||
|
|
||||||
const { startNodeNames } = consolidatedData;
|
|
||||||
let { runData: newRunData } = consolidatedData;
|
|
||||||
let executedNode: string | undefined;
|
|
||||||
if (
|
|
||||||
startNodeNames.length === 0 &&
|
|
||||||
'destinationNode' in options &&
|
|
||||||
options.destinationNode !== undefined
|
|
||||||
) {
|
|
||||||
executedNode = options.destinationNode;
|
|
||||||
startNodeNames.push(options.destinationNode);
|
|
||||||
} else if ('triggerNode' in options && 'nodeData' in options) {
|
|
||||||
startNodeNames.push(
|
|
||||||
...workflow.getChildNodes(options.triggerNode, NodeConnectionType.Main, 1),
|
|
||||||
);
|
|
||||||
newRunData = {
|
|
||||||
[options.triggerNode]: [options.nodeData],
|
|
||||||
};
|
|
||||||
executedNode = options.triggerNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
const startNodes: StartNodeData[] = startNodeNames.map((name) => {
|
|
||||||
// Find for each start node the source data
|
|
||||||
let sourceData = get(runData, [name, 0, 'source', 0], null);
|
|
||||||
if (sourceData === null) {
|
|
||||||
const parentNodes = workflow.getParentNodes(name, NodeConnectionType.Main, 1);
|
|
||||||
const executeData = this.workflowHelpers.executeData(
|
|
||||||
parentNodes,
|
|
||||||
name,
|
|
||||||
NodeConnectionType.Main,
|
|
||||||
0,
|
|
||||||
);
|
|
||||||
sourceData = get(executeData, ['source', NodeConnectionType.Main, 0], null);
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
name,
|
|
||||||
sourceData,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const startRunData: IStartRunData = {
|
|
||||||
workflowData,
|
|
||||||
runData: newRunData,
|
|
||||||
pinData: workflowData.pinData,
|
|
||||||
startNodes,
|
|
||||||
};
|
|
||||||
if ('destinationNode' in options) {
|
|
||||||
startRunData.destinationNode = options.destinationNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Init the execution data to represent the start of the execution
|
|
||||||
// that data which gets reused is already set and data of newly executed
|
|
||||||
// nodes can be added as it gets pushed in
|
|
||||||
const executionData: IExecutionResponse = {
|
|
||||||
id: '__IN_PROGRESS__',
|
|
||||||
finished: false,
|
|
||||||
mode: 'manual',
|
|
||||||
startedAt: new Date(),
|
|
||||||
stoppedAt: undefined,
|
|
||||||
workflowId: workflow.id,
|
|
||||||
executedNode,
|
|
||||||
data: {
|
|
||||||
resultData: {
|
|
||||||
runData: newRunData || {},
|
|
||||||
pinData: workflowData.pinData,
|
|
||||||
workflowData,
|
|
||||||
},
|
|
||||||
} as IRunExecutionData,
|
|
||||||
workflowData: {
|
|
||||||
id: this.workflowsStore.workflowId,
|
|
||||||
name: workflowData.name!,
|
|
||||||
active: workflowData.active!,
|
|
||||||
createdAt: 0,
|
|
||||||
updatedAt: 0,
|
|
||||||
...workflowData,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
this.workflowsStore.setWorkflowExecutionData(executionData);
|
|
||||||
this.nodeHelpers.updateNodesExecutionIssues();
|
|
||||||
|
|
||||||
const runWorkflowApiResponse = await this.runWorkflowApi(startRunData);
|
|
||||||
|
|
||||||
for (const node of workflowData.nodes) {
|
|
||||||
if (![FORM_TRIGGER_NODE_TYPE, WAIT_NODE_TYPE].includes(node.type)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
options.destinationNode &&
|
|
||||||
options.destinationNode !== node.name &&
|
|
||||||
!directParentNodes.includes(node.name)
|
|
||||||
) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (node.name === options.destinationNode || !node.disabled) {
|
|
||||||
let testUrl = '';
|
|
||||||
|
|
||||||
if (node.type === FORM_TRIGGER_NODE_TYPE && node.typeVersion === 1) {
|
|
||||||
const webhookPath = (node.parameters.path as string) || node.webhookId;
|
|
||||||
testUrl = `${this.rootStore.getWebhookTestUrl}/${webhookPath}/${FORM_TRIGGER_PATH_IDENTIFIER}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (node.type === FORM_TRIGGER_NODE_TYPE && node.typeVersion > 1) {
|
|
||||||
const webhookPath = (node.parameters.path as string) || node.webhookId;
|
|
||||||
testUrl = `${this.rootStore.getFormTestUrl}/${webhookPath}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
node.type === WAIT_NODE_TYPE &&
|
|
||||||
node.parameters.resume === 'form' &&
|
|
||||||
runWorkflowApiResponse.executionId
|
|
||||||
) {
|
|
||||||
const workflowTriggerNodes = workflow.getTriggerNodes().map((node) => node.name);
|
|
||||||
|
|
||||||
const showForm =
|
|
||||||
options.destinationNode === node.name ||
|
|
||||||
directParentNodes.includes(node.name) ||
|
|
||||||
workflowTriggerNodes.some((triggerNode) =>
|
|
||||||
this.workflowsStore.isNodeInOutgoingNodeConnections(triggerNode, node.name),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!showForm) continue;
|
|
||||||
|
|
||||||
const { webhookSuffix } = (node.parameters.options || {}) as IDataObject;
|
|
||||||
const suffix = webhookSuffix ? `/${webhookSuffix}` : '';
|
|
||||||
testUrl = `${this.rootStore.getFormWaitingUrl}/${runWorkflowApiResponse.executionId}${suffix}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (testUrl) openPopUpWindow(testUrl);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await useExternalHooks().run('workflowRun.runWorkflow', {
|
|
||||||
nodeName: options.destinationNode,
|
|
||||||
source: options.source,
|
|
||||||
});
|
|
||||||
|
|
||||||
return runWorkflowApiResponse;
|
|
||||||
} catch (error) {
|
|
||||||
this.titleSet(workflow.name as string, 'ERROR');
|
|
||||||
this.showError(error, this.$locale.baseText('workflowRun.showError.title'));
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
|
@ -259,7 +259,6 @@ import { useUniqueNodeName } from '@/composables/useUniqueNodeName';
|
||||||
import { useI18n } from '@/composables/useI18n';
|
import { useI18n } from '@/composables/useI18n';
|
||||||
import { useMessage } from '@/composables/useMessage';
|
import { useMessage } from '@/composables/useMessage';
|
||||||
import { useToast } from '@/composables/useToast';
|
import { useToast } from '@/composables/useToast';
|
||||||
import { workflowRun } from '@/mixins/workflowRun';
|
|
||||||
|
|
||||||
import NodeDetailsView from '@/components/NodeDetailsView.vue';
|
import NodeDetailsView from '@/components/NodeDetailsView.vue';
|
||||||
import ContextMenu from '@/components/ContextMenu/ContextMenu.vue';
|
import ContextMenu from '@/components/ContextMenu/ContextMenu.vue';
|
||||||
|
@ -382,6 +381,7 @@ import { useDebounce } from '@/composables/useDebounce';
|
||||||
import { useCanvasPanning } from '@/composables/useCanvasPanning';
|
import { useCanvasPanning } from '@/composables/useCanvasPanning';
|
||||||
import { tryToParseNumber } from '@/utils/typesUtils';
|
import { tryToParseNumber } from '@/utils/typesUtils';
|
||||||
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
|
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
|
||||||
|
import { useRunWorkflow } from '@/composables/useRunWorkflow';
|
||||||
|
|
||||||
interface AddNodeOptions {
|
interface AddNodeOptions {
|
||||||
position?: XYPosition;
|
position?: XYPosition;
|
||||||
|
@ -413,7 +413,6 @@ export default defineComponent({
|
||||||
ContextMenu,
|
ContextMenu,
|
||||||
SetupWorkflowCredentialsButton,
|
SetupWorkflowCredentialsButton,
|
||||||
},
|
},
|
||||||
mixins: [workflowRun],
|
|
||||||
async beforeRouteLeave(to, from, next) {
|
async beforeRouteLeave(to, from, next) {
|
||||||
if (
|
if (
|
||||||
getNodeViewTab(to) === MAIN_HEADER_TABS.EXECUTIONS ||
|
getNodeViewTab(to) === MAIN_HEADER_TABS.EXECUTIONS ||
|
||||||
|
@ -474,7 +473,7 @@ export default defineComponent({
|
||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
setup(props, ctx) {
|
setup() {
|
||||||
const nodeViewRootRef = ref(null);
|
const nodeViewRootRef = ref(null);
|
||||||
const nodeViewRef = ref(null);
|
const nodeViewRef = ref(null);
|
||||||
const onMouseMoveEnd = ref(null);
|
const onMouseMoveEnd = ref(null);
|
||||||
|
@ -492,7 +491,8 @@ export default defineComponent({
|
||||||
const deviceSupport = useDeviceSupport();
|
const deviceSupport = useDeviceSupport();
|
||||||
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 } = useRunWorkflow({ router });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
locale,
|
locale,
|
||||||
|
@ -508,6 +508,7 @@ export default defineComponent({
|
||||||
nodeViewRef,
|
nodeViewRef,
|
||||||
onMouseMoveEnd,
|
onMouseMoveEnd,
|
||||||
workflowHelpers,
|
workflowHelpers,
|
||||||
|
runWorkflow,
|
||||||
callDebounced,
|
callDebounced,
|
||||||
...useCanvasMouseSelect(),
|
...useCanvasMouseSelect(),
|
||||||
...useGlobalLinkActions(),
|
...useGlobalLinkActions(),
|
||||||
|
@ -516,8 +517,6 @@ export default defineComponent({
|
||||||
...useMessage(),
|
...useMessage(),
|
||||||
...useUniqueNodeName(),
|
...useUniqueNodeName(),
|
||||||
...useExecutionDebugging(),
|
...useExecutionDebugging(),
|
||||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
|
||||||
...workflowRun.setup?.(props, ctx),
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
|
@ -1907,7 +1906,7 @@ export default defineComponent({
|
||||||
|
|
||||||
const nodeData = JSON.stringify(workflowToCopy, null, 2);
|
const nodeData = JSON.stringify(workflowToCopy, null, 2);
|
||||||
|
|
||||||
this.clipboard.copy(nodeData);
|
void this.clipboard.copy(nodeData);
|
||||||
if (data.nodes.length > 0) {
|
if (data.nodes.length > 0) {
|
||||||
if (!isCut) {
|
if (!isCut) {
|
||||||
this.showMessage({
|
this.showMessage({
|
||||||
|
|
Loading…
Reference in a new issue