diff --git a/packages/editor-ui/src/Interface.ts b/packages/editor-ui/src/Interface.ts index f1c0a688a2..ddd0df5689 100644 --- a/packages/editor-ui/src/Interface.ts +++ b/packages/editor-ui/src/Interface.ts @@ -253,6 +253,12 @@ export interface IWorkflowToShare extends IWorkflowDataUpdate { meta?: WorkflowMetadata; } +export interface NewWorkflowResponse { + name: string; + onboardingFlowEnabled?: boolean; + defaultSettings: IWorkflowSettings; +} + export interface IWorkflowTemplateNode extends Pick { // The credentials in a template workflow have a different type than in a regular workflow diff --git a/packages/editor-ui/src/api/workflows.ts b/packages/editor-ui/src/api/workflows.ts index 48a70be317..5947f8dbb3 100644 --- a/packages/editor-ui/src/api/workflows.ts +++ b/packages/editor-ui/src/api/workflows.ts @@ -1,9 +1,20 @@ -import type { IExecutionsCurrentSummaryExtended, IRestApiContext } from '@/Interface'; +import type { + IExecutionResponse, + IExecutionsCurrentSummaryExtended, + IRestApiContext, + IWorkflowDb, + NewWorkflowResponse, +} from '@/Interface'; import type { ExecutionFilters, ExecutionOptions, IDataObject } from 'n8n-workflow'; import { makeRestApiRequest } from '@/utils/apiUtils'; export async function getNewWorkflow(context: IRestApiContext, name?: string) { - const response = await makeRestApiRequest(context, 'GET', '/workflows/new', name ? { name } : {}); + const response = await makeRestApiRequest( + context, + 'GET', + '/workflows/new', + name ? { name } : {}, + ); return { name: response.name, onboardingFlowEnabled: response.onboardingFlowEnabled === true, @@ -14,17 +25,17 @@ export async function getNewWorkflow(context: IRestApiContext, name?: string) { export async function getWorkflow(context: IRestApiContext, id: string, filter?: object) { const sendData = filter ? { filter } : undefined; - return await makeRestApiRequest(context, 'GET', `/workflows/${id}`, sendData); + return await makeRestApiRequest(context, 'GET', `/workflows/${id}`, sendData); } export async function getWorkflows(context: IRestApiContext, filter?: object) { const sendData = filter ? { filter } : undefined; - return await makeRestApiRequest(context, 'GET', '/workflows', sendData); + return await makeRestApiRequest(context, 'GET', '/workflows', sendData); } export async function getActiveWorkflows(context: IRestApiContext) { - return await makeRestApiRequest(context, 'GET', '/active-workflows'); + return await makeRestApiRequest(context, 'GET', '/active-workflows'); } export async function getActiveExecutions(context: IRestApiContext, filter: IDataObject) { @@ -42,5 +53,9 @@ export async function getExecutions( } export async function getExecutionData(context: IRestApiContext, executionId: string) { - return await makeRestApiRequest(context, 'GET', `/executions/${executionId}`); + return await makeRestApiRequest( + context, + 'GET', + `/executions/${executionId}`, + ); } diff --git a/packages/editor-ui/src/components/__tests__/RunData.test.ts b/packages/editor-ui/src/components/__tests__/RunData.test.ts index 7107d9a3ae..f740b5018f 100644 --- a/packages/editor-ui/src/components/__tests__/RunData.test.ts +++ b/packages/editor-ui/src/components/__tests__/RunData.test.ts @@ -6,7 +6,20 @@ import RunData from '@/components/RunData.vue'; import { STORES, VIEWS } from '@/constants'; import { SETTINGS_STORE_DEFAULT_STATE } from '@/__tests__/utils'; import { createComponentRenderer } from '@/__tests__/render'; -import type { IRunDataDisplayMode } from '@/Interface'; +import type { INodeUi, IRunDataDisplayMode } from '@/Interface'; +import { useWorkflowsStore } from '@/stores/workflows.store'; +import { setActivePinia } from 'pinia'; + +const nodes = [ + { + id: '1', + typeVersion: 1, + name: 'Test Node', + position: [0, 0], + type: 'test', + parameters: {}, + }, +] as INodeUi[]; describe('RunData', () => { it('should render data correctly even when "item.json" has another "json" key', async () => { @@ -81,8 +94,64 @@ describe('RunData', () => { expect(getByTestId('ndv-binary-data_0')).toBeInTheDocument(); }); - const render = (outputData: unknown[], displayMode: IRunDataDisplayMode) => - createComponentRenderer(RunData, { + const render = (outputData: unknown[], displayMode: IRunDataDisplayMode) => { + const pinia = createTestingPinia({ + initialState: { + [STORES.SETTINGS]: { + settings: merge({}, SETTINGS_STORE_DEFAULT_STATE.settings), + }, + [STORES.NDV]: { + output: { + displayMode, + }, + activeNodeName: 'Test Node', + }, + [STORES.WORKFLOWS]: { + workflow: { + nodes, + }, + workflowExecutionData: { + id: '1', + finished: true, + mode: 'trigger', + startedAt: new Date(), + workflowData: { + id: '1', + name: 'Test Workflow', + versionId: '1', + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + active: false, + nodes: [], + connections: {}, + }, + data: { + resultData: { + runData: { + 'Test Node': [ + { + startTime: new Date().getTime(), + executionTime: new Date().getTime(), + data: { + main: [outputData], + }, + source: [null], + }, + ], + }, + }, + }, + }, + }, + }, + }); + + setActivePinia(pinia); + + const workflowsStore = useWorkflowsStore(); + vi.mocked(workflowsStore).getNodeByName.mockReturnValue(nodes[0]); + + return createComponentRenderer(RunData, { props: { node: { name: 'Test Node', @@ -114,64 +183,7 @@ describe('RunData', () => { mappingEnabled: true, distanceFromActive: 0, }, - pinia: createTestingPinia({ - initialState: { - [STORES.SETTINGS]: { - settings: merge({}, SETTINGS_STORE_DEFAULT_STATE.settings), - }, - [STORES.NDV]: { - output: { - displayMode, - }, - activeNodeName: 'Test Node', - }, - [STORES.WORKFLOWS]: { - workflow: { - nodes: [ - { - id: '1', - typeVersion: 1, - name: 'Test Node', - position: [0, 0], - type: 'test', - parameters: {}, - }, - ], - }, - workflowExecutionData: { - id: '1', - finished: true, - mode: 'trigger', - startedAt: new Date(), - workflowData: { - id: '1', - name: 'Test Workflow', - versionId: '1', - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString(), - active: false, - nodes: [], - connections: {}, - }, - data: { - resultData: { - runData: { - 'Test Node': [ - { - startTime: new Date().getTime(), - executionTime: new Date().getTime(), - data: { - main: [outputData], - }, - source: [null], - }, - ], - }, - }, - }, - }, - }, - }, - }), + pinia, }); + }; }); diff --git a/packages/editor-ui/src/components/__tests__/SQLEditor.test.ts b/packages/editor-ui/src/components/__tests__/SQLEditor.test.ts index 7e7014ec59..6de5647cd4 100644 --- a/packages/editor-ui/src/components/__tests__/SQLEditor.test.ts +++ b/packages/editor-ui/src/components/__tests__/SQLEditor.test.ts @@ -8,6 +8,8 @@ import { renderComponent } from '@/__tests__/render'; import { waitFor } from '@testing-library/vue'; import { setActivePinia } from 'pinia'; import { useRouter } from 'vue-router'; +import { useWorkflowsStore } from '@/stores/workflows.store'; +import type { INodeUi } from '@/Interface'; const EXPRESSION_OUTPUT_TEST_ID = 'inline-expression-editor-output'; @@ -18,43 +20,50 @@ const DEFAULT_SETUP = { }, }; +const nodes = [ + { + id: '1', + typeVersion: 1, + name: 'Test Node', + position: [0, 0], + type: 'test', + parameters: {}, + }, +] as INodeUi[]; + +const mockResolveExpression = () => { + const mock = vi.fn(); + vi.spyOn(workflowHelpers, 'useWorkflowHelpers').mockReturnValueOnce({ + ...workflowHelpers.useWorkflowHelpers({ router: useRouter() }), + resolveExpression: mock, + }); + + return mock; +}; + describe('SqlEditor.vue', () => { - const pinia = createTestingPinia({ - initialState: { - [STORES.SETTINGS]: { - settings: SETTINGS_STORE_DEFAULT_STATE.settings, - }, - [STORES.NDV]: { - activeNodeName: 'Test Node', - }, - [STORES.WORKFLOWS]: { - workflow: { - nodes: [ - { - id: '1', - typeVersion: 1, - name: 'Test Node', - position: [0, 0], - type: 'test', - parameters: {}, - }, - ], - connections: {}, + beforeEach(() => { + const pinia = createTestingPinia({ + initialState: { + [STORES.SETTINGS]: { + settings: SETTINGS_STORE_DEFAULT_STATE.settings, + }, + [STORES.NDV]: { + activeNodeName: 'Test Node', + }, + [STORES.WORKFLOWS]: { + workflow: { + nodes, + connections: {}, + }, }, }, - }, - }); - setActivePinia(pinia); - - const mockResolveExpression = () => { - const mock = vi.fn(); - vi.spyOn(workflowHelpers, 'useWorkflowHelpers').mockReturnValueOnce({ - ...workflowHelpers.useWorkflowHelpers({ router: useRouter() }), - resolveExpression: mock, }); + setActivePinia(pinia); - return mock; - }; + const workflowsStore = useWorkflowsStore(); + vi.mocked(workflowsStore).getNodeByName.mockReturnValue(nodes[0]); + }); afterAll(() => { vi.clearAllMocks(); diff --git a/packages/editor-ui/src/composables/__tests__/useContextMenu.test.ts b/packages/editor-ui/src/composables/__tests__/useContextMenu.test.ts index a36cd0e4a1..4ceff5ae45 100644 --- a/packages/editor-ui/src/composables/__tests__/useContextMenu.test.ts +++ b/packages/editor-ui/src/composables/__tests__/useContextMenu.test.ts @@ -1,9 +1,8 @@ import type { INodeUi } from '@/Interface'; import { useContextMenu } from '@/composables/useContextMenu'; -import { BASIC_CHAIN_NODE_TYPE, NO_OP_NODE_TYPE, STICKY_NODE_TYPE, STORES } from '@/constants'; +import { BASIC_CHAIN_NODE_TYPE, NO_OP_NODE_TYPE, STICKY_NODE_TYPE } from '@/constants'; import { faker } from '@faker-js/faker'; -import { createTestingPinia } from '@pinia/testing'; -import { setActivePinia } from 'pinia'; +import { createPinia, setActivePinia } from 'pinia'; import { useSourceControlStore } from '@/stores/sourceControl.store'; import { useUIStore } from '@/stores/ui.store'; import { useWorkflowsStore } from '@/stores/workflows.store'; @@ -27,21 +26,18 @@ describe('useContextMenu', () => { const selectedNodes = nodes.slice(0, 2); beforeAll(() => { - setActivePinia( - createTestingPinia({ - initialState: { - [STORES.UI]: { selectedNodes }, - [STORES.WORKFLOWS]: { workflow: { nodes } }, - }, - }), - ); + setActivePinia(createPinia()); sourceControlStore = useSourceControlStore(); - uiStore = useUIStore(); - workflowsStore = useWorkflowsStore(); - vi.spyOn(uiStore, 'isReadOnlyView', 'get').mockReturnValue(false); vi.spyOn(sourceControlStore, 'preferences', 'get').mockReturnValue({ branchReadOnly: false, } as never); + + uiStore = useUIStore(); + uiStore.selectedNodes = selectedNodes; + vi.spyOn(uiStore, 'isReadOnlyView', 'get').mockReturnValue(false); + + workflowsStore = useWorkflowsStore(); + workflowsStore.workflow.nodes = nodes; vi.spyOn(workflowsStore, 'getCurrentWorkflow').mockReturnValue({ nodes, getNode: (_: string) => { diff --git a/packages/editor-ui/src/stores/__tests__/workflows.spec.ts b/packages/editor-ui/src/stores/__tests__/workflows.spec.ts deleted file mode 100644 index c2f6a54d54..0000000000 --- a/packages/editor-ui/src/stores/__tests__/workflows.spec.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { setActivePinia, createPinia } from 'pinia'; -import { useWorkflowsStore } from '@/stores/workflows.store'; -import type { IWorkflowDataUpdate } from '@/Interface'; -import { makeRestApiRequest } from '@/utils/apiUtils'; -import { useRootStore } from '../n8nRoot.store'; - -vi.mock('@/utils/apiUtils', () => ({ - makeRestApiRequest: vi.fn(), -})); - -const MOCK_WORKFLOW_SIMPLE: IWorkflowDataUpdate = { - id: '1', - name: 'test', - nodes: [ - { - parameters: { - path: '21a77783-e050-4e0f-9915-2d2dd5b53cde', - options: {}, - }, - id: '2dbf9369-2eec-42e7-9b89-37e50af12289', - name: 'Webhook', - type: 'n8n-nodes-base.webhook', - typeVersion: 1, - position: [340, 240], - webhookId: '21a77783-e050-4e0f-9915-2d2dd5b53cde', - }, - { - parameters: { - table: 'product', - columns: 'name,ean', - additionalFields: {}, - }, - name: 'Insert Rows1', - type: 'n8n-nodes-base.postgres', - position: [580, 240], - typeVersion: 1, - id: 'a10ba62a-8792-437c-87df-0762fa53e157', - credentials: { - postgres: { - id: 'iEFl08xIegmR8xF6', - name: 'Postgres account', - }, - }, - }, - ], - connections: { - Webhook: { - main: [ - [ - { - node: 'Insert Rows1', - type: 'main', - index: 0, - }, - ], - ], - }, - }, -}; - -describe('worklfows store', () => { - beforeEach(() => { - setActivePinia(createPinia()); - }); - - describe('createNewWorkflow', () => { - beforeEach(() => { - vi.resetAllMocks(); - }); - - it('creates new workflow', async () => { - const workflowsStore = useWorkflowsStore(); - await workflowsStore.createNewWorkflow(MOCK_WORKFLOW_SIMPLE); - - expect(makeRestApiRequest).toHaveBeenCalledWith( - useRootStore().getRestApiContext, - 'POST', - '/workflows', - { - ...MOCK_WORKFLOW_SIMPLE, - active: false, - }, - ); - }); - - it('sets active to false', async () => { - const workflowsStore = useWorkflowsStore(); - await workflowsStore.createNewWorkflow({ ...MOCK_WORKFLOW_SIMPLE, active: true }); - - expect(makeRestApiRequest).toHaveBeenCalledWith( - useRootStore().getRestApiContext, - 'POST', - '/workflows', - { - ...MOCK_WORKFLOW_SIMPLE, - active: false, - }, - ); - }); - }); -}); diff --git a/packages/editor-ui/src/stores/__tests__/workflows.test.ts b/packages/editor-ui/src/stores/__tests__/workflows.test.ts deleted file mode 100644 index 0fd67f88bf..0000000000 --- a/packages/editor-ui/src/stores/__tests__/workflows.test.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { createTestingPinia } from '@pinia/testing'; -import { useWorkflowsStore } from '@/stores/workflows.store'; - -let pinia: ReturnType; -beforeAll(() => { - pinia = createTestingPinia(); -}); - -describe('Workflows Store', () => { - describe('shouldReplaceInputDataWithPinData', () => { - beforeEach(() => { - pinia.state.value = { - workflows: useWorkflowsStore(), - }; - }); - - it('should return true if no active execution is set', () => { - expect(useWorkflowsStore().shouldReplaceInputDataWithPinData).toBe(true); - }); - - it('should return true if active execution is set and mode is manual', () => { - pinia.state.value.workflows.activeWorkflowExecution = { mode: 'manual' }; - expect(useWorkflowsStore().shouldReplaceInputDataWithPinData).toBe(true); - }); - - it('should return false if active execution is set and mode is not manual', () => { - pinia.state.value.workflows.activeWorkflowExecution = { mode: 'webhook' }; - expect(useWorkflowsStore().shouldReplaceInputDataWithPinData).toBe(false); - }); - }); -}); diff --git a/packages/editor-ui/src/stores/workflows.store.spec.ts b/packages/editor-ui/src/stores/workflows.store.spec.ts new file mode 100644 index 0000000000..6e5abfb1fd --- /dev/null +++ b/packages/editor-ui/src/stores/workflows.store.spec.ts @@ -0,0 +1,444 @@ +import { setActivePinia, createPinia } from 'pinia'; +import * as workflowsApi from '@/api/workflows'; +import { + DUPLICATE_POSTFFIX, + MAX_WORKFLOW_NAME_LENGTH, + PLACEHOLDER_EMPTY_WORKFLOW_ID, +} from '@/constants'; +import { useWorkflowsStore } from '@/stores/workflows.store'; +import type { IExecutionResponse, INodeUi, IWorkflowDb, IWorkflowSettings } from '@/Interface'; +import { useNodeTypesStore } from '@/stores/nodeTypes.store'; +import type { ExecutionSummary, IConnection, INodeExecutionData } from 'n8n-workflow'; +import { stringSizeInBytes } from '@/utils/typesUtils'; +import { dataPinningEventBus } from '@/event-bus'; +import { useUIStore } from '@/stores/ui.store'; + +vi.mock('@/api/workflows', () => ({ + getWorkflows: vi.fn(), + getWorkflow: vi.fn(), + getNewWorkflow: vi.fn(), +})); + +vi.mock('@/stores/nodeTypes.store', () => ({ + useNodeTypesStore: vi.fn(() => ({ + getNodeType: vi.fn(), + })), +})); + +describe('useWorkflowsStore', () => { + let workflowsStore: ReturnType; + let uiStore: ReturnType; + + beforeEach(() => { + setActivePinia(createPinia()); + workflowsStore = useWorkflowsStore(); + uiStore = useUIStore(); + }); + + it('should initialize with default state', () => { + expect(workflowsStore.workflow.name).toBe(''); + expect(workflowsStore.workflow.id).toBe(PLACEHOLDER_EMPTY_WORKFLOW_ID); + }); + + describe('allWorkflows', () => { + it('should return sorted workflows by name', () => { + workflowsStore.setWorkflows([ + { id: '3', name: 'Zeta' }, + { id: '1', name: 'Alpha' }, + { id: '2', name: 'Beta' }, + ] as IWorkflowDb[]); + + const allWorkflows = workflowsStore.allWorkflows; + expect(allWorkflows[0].name).toBe('Alpha'); + expect(allWorkflows[1].name).toBe('Beta'); + expect(allWorkflows[2].name).toBe('Zeta'); + }); + + it('should return empty array when no workflows are set', () => { + workflowsStore.setWorkflows([]); + + const allWorkflows = workflowsStore.allWorkflows; + expect(allWorkflows).toEqual([]); + }); + }); + + describe('isNewWorkflow', () => { + it('should return true for a new workflow', () => { + expect(workflowsStore.isNewWorkflow).toBe(true); + }); + + it('should return false for an existing workflow', () => { + workflowsStore.setWorkflowId('123'); + expect(workflowsStore.isNewWorkflow).toBe(false); + }); + }); + + describe('workflowTriggerNodes', () => { + it('should return only nodes that are triggers', () => { + vi.mocked(useNodeTypesStore).mockReturnValueOnce({ + getNodeType: vi.fn(() => ({ + group: ['trigger'], + })), + } as unknown as ReturnType); + + workflowsStore.workflow.nodes = [ + { type: 'triggerNode', typeVersion: '1' }, + { type: 'nonTriggerNode', typeVersion: '1' }, + ] as unknown as IWorkflowDb['nodes']; + + expect(workflowsStore.workflowTriggerNodes).toHaveLength(1); + expect(workflowsStore.workflowTriggerNodes[0].type).toBe('triggerNode'); + }); + + it('should return empty array when no nodes are triggers', () => { + workflowsStore.workflow.nodes = [ + { type: 'nonTriggerNode1', typeVersion: '1' }, + { type: 'nonTriggerNode2', typeVersion: '1' }, + ] as unknown as IWorkflowDb['nodes']; + + expect(workflowsStore.workflowTriggerNodes).toHaveLength(0); + }); + }); + + describe('currentWorkflowHasWebhookNode', () => { + it('should return true when a node has a webhookId', () => { + workflowsStore.workflow.nodes = [ + { name: 'Node1', webhookId: 'webhook1' }, + { name: 'Node2' }, + ] as unknown as IWorkflowDb['nodes']; + + const hasWebhookNode = workflowsStore.currentWorkflowHasWebhookNode; + expect(hasWebhookNode).toBe(true); + }); + + it('should return false when no nodes have a webhookId', () => { + workflowsStore.workflow.nodes = [ + { name: 'Node1' }, + { name: 'Node2' }, + ] as unknown as IWorkflowDb['nodes']; + + const hasWebhookNode = workflowsStore.currentWorkflowHasWebhookNode; + expect(hasWebhookNode).toBe(false); + }); + + it('should return false when there are no nodes', () => { + workflowsStore.workflow.nodes = []; + + const hasWebhookNode = workflowsStore.currentWorkflowHasWebhookNode; + expect(hasWebhookNode).toBe(false); + }); + }); + + describe('getWorkflowRunData', () => { + it('should return null when no execution data is present', () => { + workflowsStore.workflowExecutionData = null; + + const runData = workflowsStore.getWorkflowRunData; + expect(runData).toBeNull(); + }); + + it('should return null when execution data does not contain resultData', () => { + workflowsStore.workflowExecutionData = { data: {} } as IExecutionResponse; + + const runData = workflowsStore.getWorkflowRunData; + expect(runData).toBeNull(); + }); + + it('should return runData when execution data contains resultData', () => { + const expectedRunData = { node1: [{}, {}], node2: [{}] }; + workflowsStore.workflowExecutionData = { + data: { resultData: { runData: expectedRunData } }, + } as unknown as IExecutionResponse; + + const runData = workflowsStore.getWorkflowRunData; + expect(runData).toEqual(expectedRunData); + }); + }); + + describe('nodesIssuesExist', () => { + it('should return true when a node has issues', () => { + workflowsStore.workflow.nodes = [ + { name: 'Node1', issues: { error: ['Error message'] } }, + { name: 'Node2' }, + ] as unknown as IWorkflowDb['nodes']; + + const hasIssues = workflowsStore.nodesIssuesExist; + expect(hasIssues).toBe(true); + }); + + it('should return false when no nodes have issues', () => { + workflowsStore.workflow.nodes = [ + { name: 'Node1' }, + { name: 'Node2' }, + ] as unknown as IWorkflowDb['nodes']; + + const hasIssues = workflowsStore.nodesIssuesExist; + expect(hasIssues).toBe(false); + }); + + it('should return false when there are no nodes', () => { + workflowsStore.workflow.nodes = []; + + const hasIssues = workflowsStore.nodesIssuesExist; + expect(hasIssues).toBe(false); + }); + }); + + describe('shouldReplaceInputDataWithPinData', () => { + it('should return true when no active workflow execution', () => { + workflowsStore.activeWorkflowExecution = null; + + expect(workflowsStore.shouldReplaceInputDataWithPinData).toBe(true); + }); + + it('should return true when active workflow execution mode is manual', () => { + workflowsStore.activeWorkflowExecution = { mode: 'manual' } as unknown as ExecutionSummary; + + expect(workflowsStore.shouldReplaceInputDataWithPinData).toBe(true); + }); + + it('should return false when active workflow execution mode is not manual', () => { + workflowsStore.activeWorkflowExecution = { mode: 'automatic' } as unknown as ExecutionSummary; + + expect(workflowsStore.shouldReplaceInputDataWithPinData).toBe(false); + }); + }); + + describe('getWorkflowResultDataByNodeName()', () => { + it('should return null when no workflow run data is present', () => { + workflowsStore.workflowExecutionData = null; + + const resultData = workflowsStore.getWorkflowResultDataByNodeName('Node1'); + expect(resultData).toBeNull(); + }); + + it('should return null when node name is not present in workflow run data', () => { + workflowsStore.workflowExecutionData = { + data: { resultData: { runData: {} } }, + } as unknown as IExecutionResponse; + + const resultData = workflowsStore.getWorkflowResultDataByNodeName('Node1'); + expect(resultData).toBeNull(); + }); + + it('should return result data when node name is present in workflow run data', () => { + const expectedData = [{}, {}]; + workflowsStore.workflowExecutionData = { + data: { resultData: { runData: { Node1: expectedData } } }, + } as unknown as IExecutionResponse; + + const resultData = workflowsStore.getWorkflowResultDataByNodeName('Node1'); + expect(resultData).toEqual(expectedData); + }); + }); + + describe('isNodeInOutgoingNodeConnections()', () => { + it('should return false when no outgoing connections from root node', () => { + workflowsStore.workflow.connections = {}; + + const result = workflowsStore.isNodeInOutgoingNodeConnections('RootNode', 'SearchNode'); + expect(result).toBe(false); + }); + + it('should return true when search node is directly connected to root node', () => { + workflowsStore.workflow.connections = { + RootNode: { main: [[{ node: 'SearchNode' } as IConnection]] }, + }; + + const result = workflowsStore.isNodeInOutgoingNodeConnections('RootNode', 'SearchNode'); + expect(result).toBe(true); + }); + + it('should return true when search node is indirectly connected to root node', () => { + workflowsStore.workflow.connections = { + RootNode: { main: [[{ node: 'IntermediateNode' } as IConnection]] }, + IntermediateNode: { main: [[{ node: 'SearchNode' } as IConnection]] }, + }; + + const result = workflowsStore.isNodeInOutgoingNodeConnections('RootNode', 'SearchNode'); + expect(result).toBe(true); + }); + + it('should return false when search node is not connected to root node', () => { + workflowsStore.workflow.connections = { + RootNode: { main: [[{ node: 'IntermediateNode' } as IConnection]] }, + IntermediateNode: { main: [[{ node: 'AnotherNode' } as IConnection]] }, + }; + + const result = workflowsStore.isNodeInOutgoingNodeConnections('RootNode', 'SearchNode'); + expect(result).toBe(false); + }); + }); + + describe('getPinDataSize()', () => { + it('returns zero when pinData is empty', () => { + const pinData = {}; + const result = workflowsStore.getPinDataSize(pinData); + expect(result).toBe(0); + }); + + it('returns correct size when pinData contains string values', () => { + const pinData = { + key1: 'value1', + key2: 'value2', + } as Record; + const result = workflowsStore.getPinDataSize(pinData); + expect(result).toBe(stringSizeInBytes(pinData.key1) + stringSizeInBytes(pinData.key2)); + }); + + it('returns correct size when pinData contains array values', () => { + const pinData = { + key1: [{ parameters: 'value1', data: null }], + key2: [{ parameters: 'value2', data: null }], + } as unknown as Record; + const result = workflowsStore.getPinDataSize(pinData); + expect(result).toBe(stringSizeInBytes(pinData.key1) + stringSizeInBytes(pinData.key2)); + }); + + it('returns correct size when pinData contains mixed string and array values', () => { + const pinData = { + key1: 'value1', + key2: [{ parameters: 'value2', data: null }], + } as unknown as Record; + const result = workflowsStore.getPinDataSize(pinData); + expect(result).toBe(stringSizeInBytes(pinData.key1) + stringSizeInBytes(pinData.key2)); + }); + }); + + describe('fetchAllWorkflows()', () => { + it('should fetch workflows successfully', async () => { + const mockWorkflows = [{ id: '1', name: 'Test Workflow' }] as IWorkflowDb[]; + vi.mocked(workflowsApi).getWorkflows.mockResolvedValue(mockWorkflows); + + await workflowsStore.fetchAllWorkflows(); + + expect(workflowsApi.getWorkflows).toHaveBeenCalled(); + expect(Object.values(workflowsStore.workflowsById)).toEqual(mockWorkflows); + }); + }); + + describe('setWorkflowName()', () => { + it('should set the workflow name correctly', () => { + workflowsStore.setWorkflowName({ newName: 'New Workflow Name', setStateDirty: false }); + expect(workflowsStore.workflow.name).toBe('New Workflow Name'); + }); + }); + + describe('setWorkflowActive()', () => { + it('should set workflow as active when it is not already active', () => { + workflowsStore.workflowsById = { '1': { active: false } as IWorkflowDb }; + workflowsStore.workflow.id = '1'; + + workflowsStore.setWorkflowActive('1'); + + expect(workflowsStore.activeWorkflows).toContain('1'); + expect(workflowsStore.workflowsById['1'].active).toBe(true); + expect(workflowsStore.workflow.active).toBe(true); + }); + + it('should not modify active workflows when workflow is already active', () => { + workflowsStore.activeWorkflows = ['1']; + workflowsStore.workflowsById = { '1': { active: true } as IWorkflowDb }; + workflowsStore.workflow.id = '1'; + + workflowsStore.setWorkflowActive('1'); + + expect(workflowsStore.activeWorkflows).toEqual(['1']); + expect(workflowsStore.workflowsById['1'].active).toBe(true); + expect(workflowsStore.workflow.active).toBe(true); + }); + }); + + describe('setWorkflowInactive()', () => { + it('should set workflow as inactive when it exists', () => { + workflowsStore.activeWorkflows = ['1', '2']; + workflowsStore.workflowsById = { '1': { active: true } as IWorkflowDb }; + workflowsStore.setWorkflowInactive('1'); + expect(workflowsStore.workflowsById['1'].active).toBe(false); + expect(workflowsStore.activeWorkflows).toEqual(['2']); + }); + + it('should not modify active workflows when workflow is not active', () => { + workflowsStore.workflowsById = { '2': { active: true } as IWorkflowDb }; + workflowsStore.activeWorkflows = ['2']; + workflowsStore.setWorkflowInactive('1'); + expect(workflowsStore.activeWorkflows).toEqual(['2']); + expect(workflowsStore.workflowsById['2'].active).toBe(true); + }); + + it('should set current workflow as inactive when it is the target', () => { + workflowsStore.workflow.id = '1'; + workflowsStore.workflow.active = true; + workflowsStore.setWorkflowInactive('1'); + expect(workflowsStore.workflow.active).toBe(false); + }); + }); + + describe('getDuplicateCurrentWorkflowName()', () => { + it('should return the same name if appending postfix exceeds max length', async () => { + const longName = 'a'.repeat(MAX_WORKFLOW_NAME_LENGTH - DUPLICATE_POSTFFIX.length + 1); + const newName = await workflowsStore.getDuplicateCurrentWorkflowName(longName); + expect(newName).toBe(longName); + }); + + it('should append postfix to the name if it does not exceed max length', async () => { + const name = 'TestWorkflow'; + const expectedName = `${name}${DUPLICATE_POSTFFIX}`; + vi.mocked(workflowsApi).getNewWorkflow.mockResolvedValue({ + name: expectedName, + onboardingFlowEnabled: false, + settings: {} as IWorkflowSettings, + }); + const newName = await workflowsStore.getDuplicateCurrentWorkflowName(name); + expect(newName).toBe(expectedName); + }); + + it('should handle API failure gracefully', async () => { + const name = 'TestWorkflow'; + const expectedName = `${name}${DUPLICATE_POSTFFIX}`; + vi.mocked(workflowsApi).getNewWorkflow.mockRejectedValue(new Error('API Error')); + const newName = await workflowsStore.getDuplicateCurrentWorkflowName(name); + expect(newName).toBe(expectedName); + }); + }); + + describe('pinData', () => { + it('should create pinData object if it does not exist', async () => { + workflowsStore.workflow.pinData = undefined; + const node = { name: 'TestNode' } as INodeUi; + const data = [{ json: 'testData' }] as unknown as INodeExecutionData[]; + workflowsStore.pinData({ node, data }); + expect(workflowsStore.workflow.pinData).toBeDefined(); + }); + + it('should convert data to array if it is not', async () => { + const node = { name: 'TestNode' } as INodeUi; + const data = { json: 'testData' } as unknown as INodeExecutionData; + workflowsStore.pinData({ node, data: data as unknown as INodeExecutionData[] }); + expect(Array.isArray(workflowsStore.workflow.pinData?.[node.name])).toBe(true); + }); + + it('should store pinData correctly', async () => { + const node = { name: 'TestNode' } as INodeUi; + const data = [{ json: 'testData' }] as unknown as INodeExecutionData[]; + workflowsStore.pinData({ node, data }); + expect(workflowsStore.workflow.pinData?.[node.name]).toEqual(data); + }); + + it('should emit pin-data event', async () => { + const node = { name: 'TestNode' } as INodeUi; + const data = [{ json: 'testData' }] as unknown as INodeExecutionData[]; + const emitSpy = vi.spyOn(dataPinningEventBus, 'emit'); + workflowsStore.pinData({ node, data }); + expect(emitSpy).toHaveBeenCalledWith('pin-data', { [node.name]: data }); + }); + + it('should set stateIsDirty to true', async () => { + uiStore.stateIsDirty = false; + const node = { name: 'TestNode' } as INodeUi; + const data = [{ json: 'testData' }] as unknown as INodeExecutionData[]; + workflowsStore.pinData({ node, data }); + expect(uiStore.stateIsDirty).toBe(true); + }); + }); +}); diff --git a/packages/editor-ui/src/stores/workflows.store.ts b/packages/editor-ui/src/stores/workflows.store.ts index 03876b37c1..bf7863e9b8 100644 --- a/packages/editor-ui/src/stores/workflows.store.ts +++ b/packages/editor-ui/src/stores/workflows.store.ts @@ -29,9 +29,9 @@ import type { IWorkflowDataUpdate, IWorkflowDb, IWorkflowsMap, - WorkflowsState, NodeMetadataMap, WorkflowMetadata, + IExecutionFlattedResponse, } from '@/Interface'; import { defineStore } from 'pinia'; import type { @@ -48,7 +48,6 @@ import type { INodeIssueData, INodeIssueObjectProperty, INodeParameters, - INodeTypeData, INodeTypes, IPinData, IRun, @@ -56,20 +55,13 @@ import type { IRunExecutionData, ITaskData, IWorkflowSettings, + INodeType, } from 'n8n-workflow'; import { deepCopy, NodeHelpers, Workflow } from 'n8n-workflow'; import { findLast } from 'lodash-es'; import { useRootStore } from '@/stores/n8nRoot.store'; -import { - getActiveWorkflows, - getActiveExecutions, - getExecutionData, - getExecutions, - getNewWorkflow, - getWorkflow, - getWorkflows, -} from '@/api/workflows'; +import * as workflowsApi from '@/api/workflows'; import { useUIStore } from '@/stores/ui.store'; import { dataPinningEventBus } from '@/event-bus'; import { isObject } from '@/utils/objectUtils'; @@ -82,7 +74,7 @@ import { useUsersStore } from '@/stores/users.store'; import { useSettingsStore } from '@/stores/settings.store'; import { getCredentialOnlyNodeTypeName } from '@/utils/credentialOnlyNodes'; import { i18n } from '@/plugins/i18n'; -import { ErrorReporterProxy as EventReporter } from 'n8n-workflow'; +import { computed, ref } from 'vue'; const defaults: Omit & { settings: NonNullable } = { name: '', @@ -108,1355 +100,1472 @@ const createEmptyWorkflow = (): IWorkflowDb => ({ let cachedWorkflowKey: string | null = ''; let cachedWorkflow: Workflow | null = null; -export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, { - state: (): WorkflowsState => ({ - workflow: createEmptyWorkflow(), - usedCredentials: {}, - activeWorkflows: [], - activeExecutions: [], - currentWorkflowExecutions: [], - activeWorkflowExecution: null, - finishedExecutionsCount: 0, - workflowExecutionData: null, - workflowExecutionPairedItemMappings: {}, - workflowsById: {}, - subWorkflowExecutionError: null, - activeExecutionId: null, - executingNode: [], - executionWaitingForWebhook: false, - nodeMetadata: {}, - isInDebugMode: false, - chatMessages: [], - }), - getters: { - // Workflow getters - workflowName(): string { - return this.workflow.name; - }, - workflowId(): string { - return this.workflow.id; - }, - workflowVersionId(): string | undefined { - return this.workflow.versionId; - }, - workflowSettings(): IWorkflowSettings { - return this.workflow.settings ?? { ...defaults.settings }; - }, - workflowTags(): string[] { - return this.workflow.tags as string[]; - }, - allWorkflows(): IWorkflowDb[] { - return Object.values(this.workflowsById).sort((a, b) => a.name.localeCompare(b.name)); - }, - isNewWorkflow(): boolean { - return this.workflow.id === PLACEHOLDER_EMPTY_WORKFLOW_ID; - }, - isWorkflowActive(): boolean { - return this.workflow.active; - }, - workflowTriggerNodes(): INodeUi[] { - return this.workflow.nodes.filter((node: INodeUi) => { - const nodeTypesStore = useNodeTypesStore(); - const nodeType = nodeTypesStore.getNodeType(node.type, node.typeVersion); - return nodeType && nodeType.group.includes('trigger'); - }); - }, - currentWorkflowHasWebhookNode(): boolean { - return !!this.workflow.nodes.find((node: INodeUi) => !!node.webhookId); // includes Wait node - }, - getWorkflowRunData(): IRunData | null { - if (!this.workflowExecutionData?.data?.resultData) { - return null; +export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => { + const workflow = ref(createEmptyWorkflow()); + const usedCredentials = ref>({}); + + const activeWorkflows = ref([]); + const activeExecutions = ref([]); + const activeWorkflowExecution = ref(null); + const currentWorkflowExecutions = ref([]); + const finishedExecutionsCount = ref(0); + const workflowExecutionData = ref(null); + const workflowExecutionPairedItemMappings = ref>>({}); + const activeExecutionId = ref(null); + const subWorkflowExecutionError = ref(null); + const executionWaitingForWebhook = ref(false); + const executingNode = ref([]); + const workflowsById = ref>({}); + const nodeMetadata = ref({}); + const isInDebugMode = ref(false); + const chatMessages = ref([]); + + const workflowName = computed(() => workflow.value.name); + + const workflowId = computed(() => workflow.value.id); + + const workflowVersionId = computed(() => workflow.value.versionId); + + const workflowSettings = computed(() => workflow.value.settings ?? { ...defaults.settings }); + + const workflowTags = computed(() => workflow.value.tags as string[]); + + const allWorkflows = computed(() => + Object.values(workflowsById.value).sort((a, b) => a.name.localeCompare(b.name)), + ); + + const isNewWorkflow = computed(() => workflow.value.id === PLACEHOLDER_EMPTY_WORKFLOW_ID); + + const isWorkflowActive = computed(() => workflow.value.active); + + const workflowTriggerNodes = computed(() => + workflow.value.nodes.filter((node: INodeUi) => { + const nodeTypesStore = useNodeTypesStore(); + const nodeType = nodeTypesStore.getNodeType(node.type, node.typeVersion); + return nodeType && nodeType.group.includes('trigger'); + }), + ); + + const currentWorkflowHasWebhookNode = computed( + () => !!workflow.value.nodes.find((node: INodeUi) => !!node.webhookId), + ); + + const getWorkflowRunData = computed(() => { + if (!workflowExecutionData.value?.data?.resultData) { + return null; + } + + return workflowExecutionData.value.data.resultData.runData; + }); + + const allConnections = computed(() => workflow.value.connections); + + const allNodes = computed(() => workflow.value.nodes); + + // Names of all nodes currently on canvas. + const canvasNames = computed(() => new Set(allNodes.value.map((n) => n.name))); + + const nodesByName = computed(() => { + return workflow.value.nodes.reduce>((acc, node) => { + acc[node.name] = node; + return acc; + }, {}); + }); + + const nodesIssuesExist = computed(() => { + for (const node of workflow.value.nodes) { + if (node.issues === undefined || Object.keys(node.issues).length === 0) { + continue; } - return this.workflowExecutionData.data.resultData.runData; - }, - getWorkflowResultDataByNodeName() { - return (nodeName: string): ITaskData[] | null => { - const workflowRunData = this.getWorkflowRunData; - if (workflowRunData === null) { - return null; - } - if (!workflowRunData.hasOwnProperty(nodeName)) { - return null; - } - return workflowRunData[nodeName]; - }; - }, - getWorkflowById() { - return (id: string): IWorkflowDb => this.workflowsById[id]; - }, + return true; + } - // Node getters - allConnections(): IConnections { - return this.workflow.connections; - }, - outgoingConnectionsByNodeName() { - return (nodeName: string): INodeConnections => { - if (this.workflow.connections.hasOwnProperty(nodeName)) { - return this.workflow.connections[nodeName]; + return false; + }); + + const pinnedWorkflowData = computed(() => workflow.value.pinData); + + const shouldReplaceInputDataWithPinData = computed(() => { + return !activeWorkflowExecution.value || activeWorkflowExecution.value.mode === 'manual'; + }); + + const executedNode = computed(() => workflowExecutionData.value?.executedNode); + + const getAllLoadedFinishedExecutions = computed(() => { + return currentWorkflowExecutions.value.filter( + (ex) => ex.finished === true || ex.stoppedAt !== undefined, + ); + }); + + const getWorkflowExecution = computed(() => workflowExecutionData.value); + + const getTotalFinishedExecutionsCount = computed(() => finishedExecutionsCount.value); + + const getPastChatMessages = computed(() => Array.from(new Set(chatMessages.value))); + + function getWorkflowResultDataByNodeName(nodeName: string): ITaskData[] | null { + if (getWorkflowRunData.value === null) { + return null; + } + if (!getWorkflowRunData.value.hasOwnProperty(nodeName)) { + return null; + } + return getWorkflowRunData.value[nodeName]; + } + + function outgoingConnectionsByNodeName(nodeName: string): INodeConnections { + if (workflow.value.connections.hasOwnProperty(nodeName)) { + return workflow.value.connections[nodeName] as unknown as INodeConnections; + } + return {}; + } + + function nodeHasOutputConnection(nodeName: string): boolean { + return workflow.value.connections.hasOwnProperty(nodeName); + } + + function isNodeInOutgoingNodeConnections(rootNodeName: string, searchNodeName: string): boolean { + const firstNodeConnections = outgoingConnectionsByNodeName(rootNodeName); + if (!firstNodeConnections?.main?.[0]) return false; + + const connections = firstNodeConnections.main[0]; + if (connections.some((node) => node.node === searchNodeName)) return true; + + return connections.some((node) => isNodeInOutgoingNodeConnections(node.node, searchNodeName)); + } + + function getWorkflowById(id: string): IWorkflowDb { + return workflowsById.value[id]; + } + + function getNodeByName(nodeName: string): INodeUi | null { + return nodesByName.value[nodeName] || null; + } + + function getNodeById(nodeId: string): INodeUi | undefined { + return workflow.value.nodes.find((node) => node.id === nodeId); + } + + function getParametersLastUpdate(nodeName: string): number | undefined { + return nodeMetadata.value[nodeName]?.parametersLastUpdatedAt; + } + + function isNodePristine(nodeName: string): boolean { + return nodeMetadata.value[nodeName] === undefined || nodeMetadata.value[nodeName].pristine; + } + + function isNodeExecuting(nodeName: string): boolean { + return executingNode.value.includes(nodeName); + } + + function getExecutionDataById(id: string): ExecutionSummary | undefined { + return currentWorkflowExecutions.value.find((execution) => execution.id === id); + } + + function getPinDataSize(pinData: Record = {}): number { + return Object.values(pinData).reduce((acc, value) => { + return acc + stringSizeInBytes(value); + }, 0); + } + + function getNodeTypes(): INodeTypes { + const nodeTypes: INodeTypes = { + nodeTypes: {}, + init: async (): Promise => {}, + getByNameAndVersion: (nodeType: string, version?: number): INodeType | undefined => { + const nodeTypeDescription = useNodeTypesStore().getNodeType(nodeType, version); + + if (nodeTypeDescription === null) { + return undefined; } - return {}; + + return { + description: nodeTypeDescription, + // As we do not have the trigger/poll functions available in the frontend + // we use the information available to figure out what are trigger nodes + // @ts-ignore + trigger: + (![ERROR_TRIGGER_NODE_TYPE, START_NODE_TYPE].includes(nodeType) && + nodeTypeDescription.inputs.length === 0 && + !nodeTypeDescription.webhooks) || + undefined, + }; + }, + } as unknown as INodeTypes; + + return nodeTypes; + } + + // Returns a shallow copy of the nodes which means that all the data on the lower + // levels still only gets referenced but the top level object is a different one. + // This has the advantage that it is very fast and does not cause problems with vuex + // when the workflow replaces the node-parameters. + function getNodes(): INodeUi[] { + return workflow.value.nodes.map((node) => ({ ...node })); + } + + function getWorkflow(nodes: INodeUi[], connections: IConnections, copyData?: boolean): Workflow { + const nodeTypes = getNodeTypes(); + let cachedWorkflowId: string | undefined = workflowId.value; + if (cachedWorkflowId && cachedWorkflowId === PLACEHOLDER_EMPTY_WORKFLOW_ID) { + cachedWorkflowId = undefined; + } + + cachedWorkflow = new Workflow({ + id: cachedWorkflowId, + name: workflowName.value, + nodes: copyData ? deepCopy(nodes) : nodes, + connections: copyData ? deepCopy(connections) : connections, + active: false, + nodeTypes, + settings: workflowSettings.value, + // @ts-ignore + pinData: pinnedWorkflowData.value, + }); + + return cachedWorkflow; + } + + function getCurrentWorkflow(copyData?: boolean): Workflow { + const nodes = getNodes(); + const connections = allConnections.value; + const cacheKey = JSON.stringify({ nodes, connections }); + if (!copyData && cachedWorkflow && cacheKey === cachedWorkflowKey) { + return cachedWorkflow; + } + cachedWorkflowKey = cacheKey; + + return getWorkflow(nodes, connections, copyData); + } + + async function getWorkflowFromUrl(url: string): Promise { + const rootStore = useRootStore(); + return await makeRestApiRequest(rootStore.getRestApiContext, 'GET', '/workflows/from-url', { + url, + }); + } + + async function getActivationError(id: string): Promise { + const rootStore = useRootStore(); + return await makeRestApiRequest( + rootStore.getRestApiContext, + 'GET', + `/active-workflows/error/${id}`, + ); + } + + async function fetchAllWorkflows(): Promise { + const rootStore = useRootStore(); + const workflows = await workflowsApi.getWorkflows(rootStore.getRestApiContext); + setWorkflows(workflows); + return workflows; + } + + async function fetchWorkflow(id: string): Promise { + const rootStore = useRootStore(); + const workflow = await workflowsApi.getWorkflow(rootStore.getRestApiContext, id); + addWorkflow(workflow); + return workflow; + } + + async function getNewWorkflowData(name?: string): Promise { + let workflowData = { + name: '', + onboardingFlowEnabled: false, + settings: { ...defaults.settings }, + }; + try { + const rootStore = useRootStore(); + workflowData = await workflowsApi.getNewWorkflow(rootStore.getRestApiContext, name); + } catch (e) { + // in case of error, default to original name + workflowData.name = name || DEFAULT_NEW_WORKFLOW_NAME; + } + setWorkflowName({ newName: workflowData.name, setStateDirty: false }); + return workflowData; + } + + function resetWorkflow() { + const usersStore = useUsersStore(); + const settingsStore = useSettingsStore(); + workflow.value = createEmptyWorkflow(); + if (settingsStore.isEnterpriseFeatureEnabled(EnterpriseEditionFeature.Sharing)) { + workflow.value = { + ...workflow.value, + ownedBy: usersStore.currentUser as IUser, }; - }, - nodeHasOutputConnection() { - return (nodeName: string): boolean => { - if (this.workflow.connections.hasOwnProperty(nodeName)) return true; - return false; - }; - }, - isNodeInOutgoingNodeConnections() { - return (firstNode: string, secondNode: string): boolean => { - const firstNodeConnections = this.outgoingConnectionsByNodeName(firstNode); - if (!firstNodeConnections?.main?.[0]) return false; - const connections = firstNodeConnections.main[0]; - if (connections.some((node) => node.node === secondNode)) return true; - return connections.some((node) => - this.isNodeInOutgoingNodeConnections(node.node, secondNode), - ); - }; - }, - allNodes(): INodeUi[] { - return this.workflow.nodes; - }, - /** - * Names of all nodes currently on canvas. - */ - canvasNames(): Set { - return new Set(this.allNodes.map((n) => n.name)); - }, - nodesByName(): { [name: string]: INodeUi } { - return this.workflow.nodes.reduce((accu: { [name: string]: INodeUi }, node) => { - accu[node.name] = node; - return accu; - }, {}); - }, - getNodeByName() { - return (nodeName: string): INodeUi | null => this.nodesByName[nodeName] || null; - }, - getNodeById() { - return (nodeId: string): INodeUi | undefined => - this.workflow.nodes.find((node: INodeUi) => { - return node.id === nodeId; - }); - }, - nodesIssuesExist(): boolean { - for (const node of this.workflow.nodes) { - if (node.issues === undefined || Object.keys(node.issues).length === 0) { - continue; + } + } + + function resetState() { + removeAllConnections({ setStateDirty: false }); + removeAllNodes({ setStateDirty: false, removePinData: true }); + + setWorkflowExecutionData(null); + resetAllNodesIssues(); + + setActive(defaults.active); + setWorkflowId(PLACEHOLDER_EMPTY_WORKFLOW_ID); + setWorkflowName({ newName: '', setStateDirty: false }); + setWorkflowSettings({ ...defaults.settings }); + setWorkflowTagIds([]); + + activeExecutionId.value = null; + executingNode.value.length = 0; + executionWaitingForWebhook.value = false; + } + + function addExecutingNode(nodeName: string) { + executingNode.value.push(nodeName); + } + + function removeExecutingNode(nodeName: string) { + executingNode.value = executingNode.value.filter((name) => name !== nodeName); + } + + function setWorkflowId(id: string) { + workflow.value.id = id === 'new' ? PLACEHOLDER_EMPTY_WORKFLOW_ID : id; + } + + function setUsedCredentials(data: IUsedCredential[]) { + workflow.value.usedCredentials = data; + usedCredentials.value = data.reduce<{ [name: string]: IUsedCredential }>((accu, credential) => { + accu[credential.id] = credential; + return accu; + }, {}); + } + + function setWorkflowName(data: { newName: string; setStateDirty: boolean }) { + if (data.setStateDirty) { + const uiStore = useUIStore(); + uiStore.stateIsDirty = true; + } + workflow.value.name = data.newName; + + if ( + workflow.value.id !== PLACEHOLDER_EMPTY_WORKFLOW_ID && + workflowsById.value[workflow.value.id] + ) { + workflowsById.value[workflow.value.id].name = data.newName; + } + } + + function setWorkflowVersionId(versionId: string) { + workflow.value.versionId = versionId; + } + + // replace invalid credentials in workflow + function replaceInvalidWorkflowCredentials(data: { + credentials: INodeCredentialsDetails; + invalid: INodeCredentialsDetails; + type: string; + }) { + workflow.value.nodes.forEach((node: INodeUi) => { + const nodeCredentials: INodeCredentials | undefined = (node as unknown as INode).credentials; + if (!nodeCredentials?.[data.type]) { + return; + } + + const nodeCredentialDetails: INodeCredentialsDetails | string = nodeCredentials[data.type]; + + if ( + typeof nodeCredentialDetails === 'string' && + nodeCredentialDetails === data.invalid.name + ) { + (node.credentials as INodeCredentials)[data.type] = data.credentials; + return; + } + + if (nodeCredentialDetails.id === null) { + if (nodeCredentialDetails.name === data.invalid.name) { + (node.credentials as INodeCredentials)[data.type] = data.credentials; } + return; + } + + if (nodeCredentialDetails.id === data.invalid.id) { + (node.credentials as INodeCredentials)[data.type] = data.credentials; + } + }); + } + + function setWorkflows(workflows: IWorkflowDb[]) { + workflowsById.value = workflows.reduce((acc, workflow: IWorkflowDb) => { + if (workflow.id) { + acc[workflow.id] = workflow; + } + return acc; + }, {}); + } + + async function deleteWorkflow(id: string) { + const rootStore = useRootStore(); + await makeRestApiRequest(rootStore.getRestApiContext, 'DELETE', `/workflows/${id}`); + const { [id]: deletedWorkflow, ...workflows } = workflowsById.value; + workflowsById.value = workflows; + } + + function addWorkflow(workflow: IWorkflowDb) { + workflowsById.value = { + ...workflowsById.value, + [workflow.id]: { + ...workflowsById.value[workflow.id], + ...deepCopy(workflow), + }, + }; + } + + function setWorkflowActive(targetWorkflowId: string) { + const uiStore = useUIStore(); + uiStore.stateIsDirty = false; + const index = activeWorkflows.value.indexOf(targetWorkflowId); + if (index === -1) { + activeWorkflows.value.push(targetWorkflowId); + } + if (workflowsById.value[targetWorkflowId]) { + workflowsById.value[targetWorkflowId].active = true; + } + if (targetWorkflowId === workflow.value.id) { + setActive(true); + } + } + + function setWorkflowInactive(targetWorkflowId: string) { + const index = activeWorkflows.value.indexOf(targetWorkflowId); + if (index !== -1) { + activeWorkflows.value.splice(index, 1); + } + if (workflowsById.value[targetWorkflowId]) { + workflowsById.value[targetWorkflowId].active = false; + } + if (targetWorkflowId === workflow.value.id) { + setActive(false); + } + } + + async function fetchActiveWorkflows(): Promise { + const rootStore = useRootStore(); + const data = await workflowsApi.getActiveWorkflows(rootStore.getRestApiContext); + activeWorkflows.value = data; + return data; + } + + function setActive(active: boolean) { + workflow.value.active = active; + } + + async function getDuplicateCurrentWorkflowName(currentWorkflowName: string): Promise { + if ( + currentWorkflowName && + currentWorkflowName.length + DUPLICATE_POSTFFIX.length >= MAX_WORKFLOW_NAME_LENGTH + ) { + return currentWorkflowName; + } + + let newName = `${currentWorkflowName}${DUPLICATE_POSTFFIX}`; + try { + const rootStore = useRootStore(); + const newWorkflow = await workflowsApi.getNewWorkflow(rootStore.getRestApiContext, newName); + newName = newWorkflow.name; + } catch (e) {} + return newName; + } + + function setWorkflowExecutionData(workflowResultData: IExecutionResponse | null) { + workflowExecutionData.value = workflowResultData; + workflowExecutionPairedItemMappings.value = getPairedItemsMapping(workflowResultData); + } + + function setWorkflowExecutionRunData(workflowResultData: IRunExecutionData) { + if (workflowExecutionData.value) { + workflowExecutionData.value = { + ...workflowExecutionData.value, + data: workflowResultData, + }; + } + } + + function setWorkflowSettings(workflowSettings: IWorkflowSettings) { + workflow.value = { + ...workflow.value, + settings: workflowSettings as IWorkflowDb['settings'], + }; + } + + function setWorkflowPinData(pinData: IPinData) { + workflow.value = { + ...workflow.value, + pinData: pinData || {}, + }; + + dataPinningEventBus.emit('pin-data', pinData || {}); + } + + function setWorkflowTagIds(tags: string[]) { + workflow.value = { + ...workflow.value, + tags, + }; + } + + function addWorkflowTagIds(tags: string[]) { + workflow.value = { + ...workflow.value, + tags: [...new Set([...(workflow.value.tags ?? []), ...tags])] as IWorkflowDb['tags'], + }; + } + + function removeWorkflowTagId(tagId: string) { + const tags = workflow.value.tags as string[]; + const updated = tags.filter((id: string) => id !== tagId); + workflow.value = { + ...workflow.value, + tags: updated as IWorkflowDb['tags'], + }; + } + + function setWorkflowMetadata(metadata: WorkflowMetadata | undefined): void { + workflow.value.meta = metadata; + } + + function addToWorkflowMetadata(data: Partial): void { + workflow.value.meta = { + ...workflow.value.meta, + ...data, + }; + } + + function setWorkflow(value: IWorkflowDb): void { + workflow.value = { + ...value, + ...(!value.hasOwnProperty('active') ? { active: false } : {}), + ...(!value.hasOwnProperty('connections') ? { connections: {} } : {}), + ...(!value.hasOwnProperty('createdAt') ? { createdAt: -1 } : {}), + ...(!value.hasOwnProperty('updatedAt') ? { updatedAt: -1 } : {}), + ...(!value.hasOwnProperty('id') ? { id: PLACEHOLDER_EMPTY_WORKFLOW_ID } : {}), + ...(!value.hasOwnProperty('nodes') ? { nodes: [] } : {}), + ...(!value.hasOwnProperty('settings') ? { settings: { ...defaults.settings } } : {}), + }; + } + + function pinData(payload: { node: INodeUi; data: INodeExecutionData[] }): void { + if (!workflow.value.pinData) { + workflow.value = { ...workflow.value, pinData: {} }; + } + + if (!Array.isArray(payload.data)) { + payload.data = [payload.data]; + } + + const storedPinData = payload.data.map((item) => + isJsonKeyObject(item) ? { json: item.json } : { json: item }, + ); + + workflow.value = { + ...workflow.value, + pinData: { + ...workflow.value.pinData, + [payload.node.name]: storedPinData, + }, + }; + + const uiStore = useUIStore(); + uiStore.stateIsDirty = true; + + dataPinningEventBus.emit('pin-data', { [payload.node.name]: storedPinData }); + } + + function unpinData(payload: { node: INodeUi }): void { + if (!workflow.value.pinData) { + workflow.value = { ...workflow.value, pinData: {} }; + } + + const { [payload.node.name]: _, ...pinData } = workflow.value.pinData as IPinData; + workflow.value = { + ...workflow.value, + pinData, + }; + + const uiStore = useUIStore(); + uiStore.stateIsDirty = true; + + dataPinningEventBus.emit('unpin-data', { [payload.node.name]: undefined }); + } + + function addConnection(data: { connection: IConnection[] }): void { + if (data.connection.length !== 2) { + // All connections need two entries + // TODO: Check if there is an error or whatever that is supposed to be returned + return; + } + + const sourceData: IConnection = data.connection[0]; + const destinationData: IConnection = data.connection[1]; + + // Check if source node and type exist already and if not add them + if (!workflow.value.connections.hasOwnProperty(sourceData.node)) { + workflow.value = { + ...workflow.value, + connections: { + ...workflow.value.connections, + [sourceData.node]: {}, + }, + }; + } + + if (!workflow.value.connections[sourceData.node].hasOwnProperty(sourceData.type)) { + workflow.value = { + ...workflow.value, + connections: { + ...workflow.value.connections, + [sourceData.node]: { + ...workflow.value.connections[sourceData.node], + [sourceData.type]: [], + }, + }, + }; + } + + if ( + workflow.value.connections[sourceData.node][sourceData.type].length < + sourceData.index + 1 + ) { + for ( + let i = workflow.value.connections[sourceData.node][sourceData.type].length; + i <= sourceData.index; + i++ + ) { + workflow.value.connections[sourceData.node][sourceData.type].push([]); + } + } + + // Check if the same connection exists already + const checkProperties = ['index', 'node', 'type'] as Array; + let propertyName: keyof IConnection; + let connectionExists = false; + + connectionLoop: for (const existingConnection of workflow.value.connections[sourceData.node][ + sourceData.type + ][sourceData.index]) { + for (propertyName of checkProperties) { + if (existingConnection[propertyName] !== destinationData[propertyName]) { + continue connectionLoop; + } + } + connectionExists = true; + break; + } + + // Add the new connection if it does not exist already + if (!connectionExists) { + workflow.value.connections[sourceData.node][sourceData.type][sourceData.index].push( + destinationData, + ); + } + } + + function removeConnection(data: { connection: IConnection[] }): void { + const sourceData = data.connection[0]; + const destinationData = data.connection[1]; + + if (!workflow.value.connections.hasOwnProperty(sourceData.node)) { + return; + } + + if (!workflow.value.connections[sourceData.node].hasOwnProperty(sourceData.type)) { + return; + } + + if ( + workflow.value.connections[sourceData.node][sourceData.type].length < + sourceData.index + 1 + ) { + return; + } + + const uiStore = useUIStore(); + uiStore.stateIsDirty = true; + + const connections = + workflow.value.connections[sourceData.node][sourceData.type][sourceData.index]; + for (const index in connections) { + if ( + connections[index].node === destinationData.node && + connections[index].type === destinationData.type && + connections[index].index === destinationData.index + ) { + // Found the connection to remove + connections.splice(parseInt(index, 10), 1); + } + } + } + + function removeAllConnections(data: { setStateDirty: boolean }): void { + if (data && data.setStateDirty) { + const uiStore = useUIStore(); + uiStore.stateIsDirty = true; + } + + workflow.value.connections = {}; + } + + function removeAllNodeConnection( + node: INodeUi, + { preserveInputConnections = false, preserveOutputConnections = false } = {}, + ): void { + const uiStore = useUIStore(); + uiStore.stateIsDirty = true; + + // Remove all source connections + if (!preserveOutputConnections && workflow.value.connections.hasOwnProperty(node.name)) { + delete workflow.value.connections[node.name]; + } + + // Remove all destination connections + if (preserveInputConnections) return; + + const indexesToRemove = []; + let sourceNode: string, + type: string, + sourceIndex: string, + connectionIndex: string, + connectionData: IConnection; + + for (sourceNode of Object.keys(workflow.value.connections)) { + for (type of Object.keys(workflow.value.connections[sourceNode])) { + for (sourceIndex of Object.keys(workflow.value.connections[sourceNode][type])) { + indexesToRemove.length = 0; + for (connectionIndex of Object.keys( + workflow.value.connections[sourceNode][type][parseInt(sourceIndex, 10)], + )) { + connectionData = + workflow.value.connections[sourceNode][type][parseInt(sourceIndex, 10)][ + parseInt(connectionIndex, 10) + ]; + if (connectionData.node === node.name) { + indexesToRemove.push(connectionIndex); + } + } + indexesToRemove.forEach((index) => { + workflow.value.connections[sourceNode][type][parseInt(sourceIndex, 10)].splice( + parseInt(index, 10), + 1, + ); + }); + } + } + } + } + + function renameNodeSelectedAndExecution(nameData: { old: string; new: string }): void { + const uiStore = useUIStore(); + uiStore.stateIsDirty = true; + + // If node has any WorkflowResultData rename also that one that the data + // does still get displayed also after node got renamed + if ( + workflowExecutionData.value?.data && + workflowExecutionData.value.data.resultData.runData.hasOwnProperty(nameData.old) + ) { + workflowExecutionData.value.data.resultData.runData[nameData.new] = + workflowExecutionData.value.data.resultData.runData[nameData.old]; + delete workflowExecutionData.value.data.resultData.runData[nameData.old]; + } + + // In case the renamed node was last selected set it also there with the new name + if (uiStore.lastSelectedNode === nameData.old) { + uiStore.lastSelectedNode = nameData.new; + } + + const { [nameData.old]: removed, ...rest } = nodeMetadata.value; + nodeMetadata.value = { ...rest, [nameData.new]: nodeMetadata.value[nameData.old] }; + + if (workflow.value.pinData && workflow.value.pinData.hasOwnProperty(nameData.old)) { + const { [nameData.old]: renamed, ...restPinData } = workflow.value.pinData; + workflow.value = { + ...workflow.value, + pinData: { + ...restPinData, + [nameData.new]: renamed, + }, + }; + } + } + + function resetAllNodesIssues(): boolean { + workflow.value.nodes.forEach((node) => { + node.issues = undefined; + }); + return true; + } + + function updateNodeAtIndex(nodeIndex: number, nodeData: Partial): void { + if (nodeIndex !== -1) { + const node = workflow.value.nodes[nodeIndex]; + workflow.value = { + ...workflow.value, + nodes: [ + ...workflow.value.nodes.slice(0, nodeIndex), + { ...node, ...nodeData }, + ...workflow.value.nodes.slice(nodeIndex + 1), + ], + }; + } + } + + function setNodeIssue(nodeIssueData: INodeIssueData): boolean { + const nodeIndex = workflow.value.nodes.findIndex((node) => { + return node.name === nodeIssueData.node; + }); + if (nodeIndex === -1) { + return false; + } + + const node = workflow.value.nodes[nodeIndex]; + + if (nodeIssueData.value === null) { + // Remove the value if one exists + if (node.issues?.[nodeIssueData.type] === undefined) { + // No values for type exist so nothing has to get removed return true; } - return false; - }, - pinnedWorkflowData(): IPinData | undefined { - return this.workflow.pinData; - }, - shouldReplaceInputDataWithPinData(): boolean { - return !this.activeWorkflowExecution || this.activeWorkflowExecution?.mode === 'manual'; - }, - executedNode(): string | undefined { - return this.workflowExecutionData ? this.workflowExecutionData.executedNode : undefined; - }, - getParametersLastUpdate(): (name: string) => number | undefined { - return (nodeName: string) => this.nodeMetadata[nodeName]?.parametersLastUpdatedAt; - }, - isNodePristine(): (name: string) => boolean { - return (nodeName: string) => - this.nodeMetadata[nodeName] === undefined || this.nodeMetadata[nodeName].pristine; - }, - isNodeExecuting(): (nodeName: string) => boolean { - return (nodeName: string) => this.executingNode.includes(nodeName); - }, - // Executions getters - getExecutionDataById(): (id: string) => ExecutionSummary | undefined { - return (id: string): ExecutionSummary | undefined => - this.currentWorkflowExecutions.find((execution) => execution.id === id); - }, - getAllLoadedFinishedExecutions(): ExecutionSummary[] { - return this.currentWorkflowExecutions.filter( - (ex) => ex.finished === true || ex.stoppedAt !== undefined, - ); - }, - getWorkflowExecution(): IExecutionResponse | null { - return this.workflowExecutionData; - }, - getTotalFinishedExecutionsCount(): number { - return this.finishedExecutionsCount; - }, - getPastChatMessages(): string[] { - return Array.from(new Set(this.chatMessages)); - }, - }, - actions: { - getPinDataSize(pinData: Record = {}): number { - return Object.values(pinData).reduce((acc, value) => { - return acc + stringSizeInBytes(value); - }, 0); - }, - getNodeTypes(): INodeTypes { - const nodeTypes: INodeTypes = { - nodeTypes: {}, - init: async (nodeTypes?: INodeTypeData): Promise => {}, - // @ts-ignore - getByNameAndVersion: (nodeType: string, version?: number): INodeType | undefined => { - const nodeTypeDescription = useNodeTypesStore().getNodeType(nodeType, version); - - if (nodeTypeDescription === null) { - return undefined; - } - - return { - description: nodeTypeDescription, - // As we do not have the trigger/poll functions available in the frontend - // we use the information available to figure out what are trigger nodes - // @ts-ignore - trigger: - (![ERROR_TRIGGER_NODE_TYPE, START_NODE_TYPE].includes(nodeType) && - nodeTypeDescription.inputs.length === 0 && - !nodeTypeDescription.webhooks) || - undefined, - }; - }, - }; - - return nodeTypes; - }, - - // Returns a shallow copy of the nodes which means that all the data on the lower - // levels still only gets referenced but the top level object is a different one. - // This has the advantage that it is very fast and does not cause problems with vuex - // when the workflow replaces the node-parameters. - getNodes(): INodeUi[] { - const nodes = this.allNodes; - const returnNodes: INodeUi[] = []; - - for (const node of nodes) { - returnNodes.push(Object.assign({}, node)); - } - - return returnNodes; - }, - - // Returns a workflow instance. - getWorkflow(nodes: INodeUi[], connections: IConnections, copyData?: boolean): Workflow { - const nodeTypes = this.getNodeTypes(); - let workflowId: string | undefined = this.workflowId; - if (workflowId && workflowId === PLACEHOLDER_EMPTY_WORKFLOW_ID) { - workflowId = undefined; - } - - cachedWorkflow = new Workflow({ - id: workflowId, - name: this.workflowName, - nodes: copyData ? deepCopy(nodes) : nodes, - connections: copyData ? deepCopy(connections) : connections, - active: false, - nodeTypes, - settings: this.workflowSettings, - // @ts-ignore - pinData: this.pinnedWorkflowData, + const { [nodeIssueData.type]: removedNodeIssue, ...remainingNodeIssues } = node.issues; + updateNodeAtIndex(nodeIndex, { + issues: remainingNodeIssues, }); - - return cachedWorkflow; - }, - - getCurrentWorkflow(copyData?: boolean): Workflow { - const nodes = this.getNodes(); - const connections = this.allConnections; - const cacheKey = JSON.stringify({ nodes, connections }); - if (!copyData && cachedWorkflow && cacheKey === cachedWorkflowKey) { - return cachedWorkflow; - } - cachedWorkflowKey = cacheKey; - - return this.getWorkflow(nodes, connections, copyData); - }, - - // Returns a workflow from a given URL - async getWorkflowFromUrl(url: string): Promise { - const rootStore = useRootStore(); - return await makeRestApiRequest(rootStore.getRestApiContext, 'GET', '/workflows/from-url', { - url, - }); - }, - - async getActivationError(id: string): Promise { - const rootStore = useRootStore(); - return await makeRestApiRequest( - rootStore.getRestApiContext, - 'GET', - `/active-workflows/error/${id}`, - ); - }, - - async fetchAllWorkflows(): Promise { - const rootStore = useRootStore(); - const workflows = await getWorkflows(rootStore.getRestApiContext); - this.setWorkflows(workflows); - return workflows; - }, - - async fetchWorkflow(id: string): Promise { - const rootStore = useRootStore(); - const workflow = await getWorkflow(rootStore.getRestApiContext, id); - this.addWorkflow(workflow); - return workflow; - }, - - async getNewWorkflowData(name?: string): Promise { - let workflowData = { - name: '', - onboardingFlowEnabled: false, - settings: { ...defaults.settings }, - }; - try { - const rootStore = useRootStore(); - workflowData = await getNewWorkflow(rootStore.getRestApiContext, name); - } catch (e) { - // in case of error, default to original name - workflowData.name = name || DEFAULT_NEW_WORKFLOW_NAME; - } - - this.setWorkflowName({ newName: workflowData.name, setStateDirty: false }); - - return workflowData; - }, - - resetWorkflow() { - const usersStore = useUsersStore(); - const settingsStore = useSettingsStore(); - - this.workflow = createEmptyWorkflow(); - - if (settingsStore.isEnterpriseFeatureEnabled(EnterpriseEditionFeature.Sharing)) { - this.workflow = { - ...this.workflow, - ownedBy: usersStore.currentUser as IUser, - }; - } - }, - - resetState(): void { - this.removeAllConnections({ setStateDirty: false }); - this.removeAllNodes({ setStateDirty: false, removePinData: true }); - - // Reset workflow execution data - this.setWorkflowExecutionData(null); - this.resetAllNodesIssues(); - - this.setActive(defaults.active); - this.setWorkflowId(PLACEHOLDER_EMPTY_WORKFLOW_ID); - this.setWorkflowName({ newName: '', setStateDirty: false }); - this.setWorkflowSettings({ ...defaults.settings }); - this.setWorkflowTagIds([]); - - this.activeExecutionId = null; - this.executingNode.length = 0; - this.executionWaitingForWebhook = false; - }, - - addExecutingNode(nodeName: string): void { - this.executingNode.push(nodeName); - }, - - removeExecutingNode(nodeName: string): void { - this.executingNode = this.executingNode.filter((name) => name !== nodeName); - }, - - setWorkflowId(id: string): void { - this.workflow.id = id === 'new' ? PLACEHOLDER_EMPTY_WORKFLOW_ID : id; - }, - - setUsedCredentials(data: IUsedCredential[]) { - this.workflow.usedCredentials = data; - this.usedCredentials = data.reduce<{ [name: string]: IUsedCredential }>( - (accu, credential) => { - accu[credential.id] = credential; - return accu; - }, - {}, - ); - }, - - setWorkflowName(data: { newName: string; setStateDirty: boolean }): void { - if (data.setStateDirty) { - const uiStore = useUIStore(); - uiStore.stateIsDirty = true; - } - this.workflow.name = data.newName; - - if ( - this.workflow.id !== PLACEHOLDER_EMPTY_WORKFLOW_ID && - this.workflowsById[this.workflow.id] - ) { - this.workflowsById[this.workflow.id].name = data.newName; - } - }, - - setWorkflowVersionId(versionId: string): void { - this.workflow.versionId = versionId; - }, - - // replace invalid credentials in workflow - replaceInvalidWorkflowCredentials(data: { - credentials: INodeCredentialsDetails; - invalid: INodeCredentialsDetails; - type: string; - }): void { - this.workflow.nodes.forEach((node: INodeUi) => { - const nodeCredentials: INodeCredentials | undefined = (node as unknown as INode) - .credentials; - - if (!nodeCredentials?.[data.type]) { - return; - } - - const nodeCredentialDetails: INodeCredentialsDetails | string = nodeCredentials[data.type]; - - if ( - typeof nodeCredentialDetails === 'string' && - nodeCredentialDetails === data.invalid.name - ) { - (node.credentials as INodeCredentials)[data.type] = data.credentials; - return; - } - - if (nodeCredentialDetails.id === null) { - if (nodeCredentialDetails.name === data.invalid.name) { - (node.credentials as INodeCredentials)[data.type] = data.credentials; - } - return; - } - - if (nodeCredentialDetails.id === data.invalid.id) { - (node.credentials as INodeCredentials)[data.type] = data.credentials; - } - }); - }, - - setWorkflows(workflows: IWorkflowDb[]): void { - this.workflowsById = workflows.reduce((acc, workflow: IWorkflowDb) => { - if (workflow.id) { - acc[workflow.id] = workflow; - } - - return acc; - }, {}); - }, - - async deleteWorkflow(id: string): Promise { - const rootStore = useRootStore(); - await makeRestApiRequest(rootStore.getRestApiContext, 'DELETE', `/workflows/${id}`); - const { [id]: deletedWorkflow, ...workflows } = this.workflowsById; - this.workflowsById = workflows; - }, - - addWorkflow(workflow: IWorkflowDb): void { - this.workflowsById = { - ...this.workflowsById, - [workflow.id]: { - ...this.workflowsById[workflow.id], - ...deepCopy(workflow), - }, - }; - }, - - setWorkflowActive(workflowId: string): void { - const uiStore = useUIStore(); - uiStore.stateIsDirty = false; - const index = this.activeWorkflows.indexOf(workflowId); - if (index === -1) { - this.activeWorkflows.push(workflowId); - } - if (this.workflowsById[workflowId]) { - this.workflowsById[workflowId].active = true; - } - if (workflowId === this.workflow.id) { - this.setActive(true); - } - }, - - setWorkflowInactive(workflowId: string): void { - const index = this.activeWorkflows.indexOf(workflowId); - if (index !== -1) { - this.activeWorkflows.splice(index, 1); - } - if (this.workflowsById[workflowId]) { - this.workflowsById[workflowId].active = false; - } - if (workflowId === this.workflow.id) { - this.setActive(false); - } - }, - - async fetchActiveWorkflows(): Promise { - const rootStore = useRootStore(); - const activeWorkflows = await getActiveWorkflows(rootStore.getRestApiContext); - this.activeWorkflows = activeWorkflows; - return activeWorkflows; - }, - - setActive(newActive: boolean): void { - this.workflow.active = newActive; - }, - - async getDuplicateCurrentWorkflowName(currentWorkflowName: string): Promise { - if ( - currentWorkflowName && - currentWorkflowName.length + DUPLICATE_POSTFFIX.length >= MAX_WORKFLOW_NAME_LENGTH - ) { - return currentWorkflowName; - } - - let newName = `${currentWorkflowName}${DUPLICATE_POSTFFIX}`; - try { - const rootStore = useRootStore(); - const newWorkflow = await getNewWorkflow(rootStore.getRestApiContext, newName); - newName = newWorkflow.name; - } catch (e) {} - return newName; - }, - - // Node actions - setWorkflowExecutionData(workflowResultData: IExecutionResponse | null): void { - this.workflowExecutionData = workflowResultData; - this.workflowExecutionPairedItemMappings = getPairedItemsMapping(this.workflowExecutionData); - }, - - setWorkflowExecutionRunData(workflowResultData: IRunExecutionData): void { - if (this.workflowExecutionData) this.workflowExecutionData.data = workflowResultData; - }, - - setWorkflowSettings(workflowSettings: IWorkflowSettings): void { - this.workflow = { - ...this.workflow, - settings: workflowSettings as IWorkflowDb['settings'], - }; - }, - - setWorkflowPinData(pinData: IPinData): void { - this.workflow = { - ...this.workflow, - pinData: pinData || {}, - }; - dataPinningEventBus.emit('pin-data', pinData || {}); - }, - - setWorkflowTagIds(tags: string[]): void { - this.workflow = { - ...this.workflow, - tags, - }; - }, - - addWorkflowTagIds(tags: string[]): void { - this.workflow = { - ...this.workflow, - tags: [...new Set([...(this.workflow.tags || []), ...tags])] as IWorkflowDb['tags'], - }; - }, - - removeWorkflowTagId(tagId: string): void { - const tags = this.workflow.tags as string[]; - const updated = tags.filter((id: string) => id !== tagId); - this.workflow = { - ...this.workflow, - tags: updated as IWorkflowDb['tags'], - }; - }, - - setWorkflowMetadata(metadata: WorkflowMetadata | undefined): void { - this.workflow.meta = metadata; - }, - - addToWorkflowMetadata(data: Partial): void { - this.workflow.meta = { - ...this.workflow.meta, - ...data, - }; - }, - - setWorkflow(workflow: IWorkflowDb): void { - this.workflow = workflow; - this.workflow = { - ...this.workflow, - ...(!this.workflow.hasOwnProperty('active') ? { active: false } : {}), - ...(!this.workflow.hasOwnProperty('connections') ? { connections: {} } : {}), - ...(!this.workflow.hasOwnProperty('createdAt') ? { createdAt: -1 } : {}), - ...(!this.workflow.hasOwnProperty('updatedAt') ? { updatedAt: -1 } : {}), - ...(!this.workflow.hasOwnProperty('id') ? { id: PLACEHOLDER_EMPTY_WORKFLOW_ID } : {}), - ...(!this.workflow.hasOwnProperty('nodes') ? { nodes: [] } : {}), - ...(!this.workflow.hasOwnProperty('settings') - ? { settings: { ...defaults.settings } } - : {}), - }; - }, - - pinData(payload: { node: INodeUi; data: INodeExecutionData[] }): void { - if (!this.workflow.pinData) { - this.workflow = { ...this.workflow, pinData: {} }; - } - - if (!Array.isArray(payload.data)) { - payload.data = [payload.data]; - } - - const storedPinData = payload.data.map((item) => - isJsonKeyObject(item) ? { json: item.json } : { json: item }, - ); - - this.workflow = { - ...this.workflow, - pinData: { - ...this.workflow.pinData, - [payload.node.name]: storedPinData, - }, - }; - - const uiStore = useUIStore(); - uiStore.stateIsDirty = true; - - dataPinningEventBus.emit('pin-data', { [payload.node.name]: storedPinData }); - }, - - unpinData(payload: { node: INodeUi }): void { - if (!this.workflow.pinData) { - this.workflow = { ...this.workflow, pinData: {} }; - } - - const { [payload.node.name]: _, ...pinData } = this.workflow.pinData!; - this.workflow = { - ...this.workflow, - pinData, - }; - - const uiStore = useUIStore(); - uiStore.stateIsDirty = true; - - dataPinningEventBus.emit('unpin-data', { [payload.node.name]: undefined }); - }, - - addConnection(data: { connection: IConnection[] }): void { - if (data.connection.length !== 2) { - // All connections need two entries - // TODO: Check if there is an error or whatever that is supposed to be returned - return; - } - const sourceData: IConnection = data.connection[0]; - const destinationData: IConnection = data.connection[1]; - - // Check if source node and type exist already and if not add them - if (!this.workflow.connections.hasOwnProperty(sourceData.node)) { - this.workflow = { - ...this.workflow, - connections: { - ...this.workflow.connections, - [sourceData.node]: {}, - }, - }; - } - if (!this.workflow.connections[sourceData.node].hasOwnProperty(sourceData.type)) { - this.workflow = { - ...this.workflow, - connections: { - ...this.workflow.connections, - [sourceData.node]: { - ...this.workflow.connections[sourceData.node], - [sourceData.type]: [], - }, - }, - }; - } - if ( - this.workflow.connections[sourceData.node][sourceData.type].length < - sourceData.index + 1 - ) { - for ( - let i = this.workflow.connections[sourceData.node][sourceData.type].length; - i <= sourceData.index; - i++ - ) { - this.workflow.connections[sourceData.node][sourceData.type].push([]); - } - } - - // Check if the same connection exists already - const checkProperties = ['index', 'node', 'type'] as Array; - let propertyName: keyof IConnection; - let connectionExists = false; - connectionLoop: for (const existingConnection of this.workflow.connections[sourceData.node][ - sourceData.type - ][sourceData.index]) { - for (propertyName of checkProperties) { - if (existingConnection[propertyName] !== destinationData[propertyName]) { - continue connectionLoop; - } - } - connectionExists = true; - break; - } - // Add the new connection if it does not exist already - if (!connectionExists) { - this.workflow.connections[sourceData.node][sourceData.type][sourceData.index].push( - destinationData, - ); - } - }, - - removeConnection(data: { connection: IConnection[] }): void { - const sourceData = data.connection[0]; - const destinationData = data.connection[1]; - - if (!this.workflow.connections.hasOwnProperty(sourceData.node)) { - return; - } - if (!this.workflow.connections[sourceData.node].hasOwnProperty(sourceData.type)) { - return; - } - if ( - this.workflow.connections[sourceData.node][sourceData.type].length < - sourceData.index + 1 - ) { - return; - } - const uiStore = useUIStore(); - uiStore.stateIsDirty = true; - - const connections = - this.workflow.connections[sourceData.node][sourceData.type][sourceData.index]; - for (const index in connections) { - if ( - connections[index].node === destinationData.node && - connections[index].type === destinationData.type && - connections[index].index === destinationData.index - ) { - // Found the connection to remove - connections.splice(parseInt(index, 10), 1); - } - } - }, - - removeAllConnections(data: { setStateDirty: boolean }): void { - if (data && data.setStateDirty) { - const uiStore = useUIStore(); - uiStore.stateIsDirty = true; - } - this.workflow.connections = {}; - }, - - removeAllNodeConnection( - node: INodeUi, - { preserveInputConnections = false, preserveOutputConnections = false } = {}, - ): void { - const uiStore = useUIStore(); - uiStore.stateIsDirty = true; - // Remove all source connections - if (!preserveOutputConnections && this.workflow.connections.hasOwnProperty(node.name)) { - delete this.workflow.connections[node.name]; - } - - // Remove all destination connections - if (preserveInputConnections) return; - const indexesToRemove = []; - let sourceNode: string, - type: string, - sourceIndex: string, - connectionIndex: string, - connectionData: IConnection; - for (sourceNode of Object.keys(this.workflow.connections)) { - for (type of Object.keys(this.workflow.connections[sourceNode])) { - for (sourceIndex of Object.keys(this.workflow.connections[sourceNode][type])) { - indexesToRemove.length = 0; - for (connectionIndex of Object.keys( - this.workflow.connections[sourceNode][type][parseInt(sourceIndex, 10)], - )) { - connectionData = - this.workflow.connections[sourceNode][type][parseInt(sourceIndex, 10)][ - parseInt(connectionIndex, 10) - ]; - if (connectionData.node === node.name) { - indexesToRemove.push(connectionIndex); - } - } - - indexesToRemove.forEach((index) => { - this.workflow.connections[sourceNode][type][parseInt(sourceIndex, 10)].splice( - parseInt(index, 10), - 1, - ); - }); - } - } - } - }, - - renameNodeSelectedAndExecution(nameData: { old: string; new: string }): void { - const uiStore = useUIStore(); - uiStore.stateIsDirty = true; - // If node has any WorkflowResultData rename also that one that the data - // does still get displayed also after node got renamed - if ( - this.workflowExecutionData?.data && - this.workflowExecutionData.data.resultData.runData.hasOwnProperty(nameData.old) - ) { - this.workflowExecutionData.data.resultData.runData[nameData.new] = - this.workflowExecutionData.data.resultData.runData[nameData.old]; - delete this.workflowExecutionData.data.resultData.runData[nameData.old]; - } - - // In case the renamed node was last selected set it also there with the new name - if (uiStore.lastSelectedNode === nameData.old) { - uiStore.lastSelectedNode = nameData.new; - } - - const { [nameData.old]: removed, ...rest } = this.nodeMetadata; - this.nodeMetadata = { ...rest, [nameData.new]: this.nodeMetadata[nameData.old] }; - - if (this.workflow.pinData && this.workflow.pinData.hasOwnProperty(nameData.old)) { - const { [nameData.old]: renamed, ...restPinData } = this.workflow.pinData; - this.workflow = { - ...this.workflow, - pinData: { - ...restPinData, - [nameData.new]: renamed, - }, - }; - } - }, - - resetAllNodesIssues(): boolean { - this.workflow.nodes.forEach((node) => { - node.issues = undefined; - }); - return true; - }, - - updateNodeAtIndex(nodeIndex: number, nodeData: Partial): void { - if (nodeIndex !== -1) { - const node = this.workflow.nodes[nodeIndex]; - this.workflow = { - ...this.workflow, - nodes: [ - ...this.workflow.nodes.slice(0, nodeIndex), - { ...node, ...nodeData }, - ...this.workflow.nodes.slice(nodeIndex + 1), - ], - }; - } - }, - - setNodeIssue(nodeIssueData: INodeIssueData): boolean { - const nodeIndex = this.workflow.nodes.findIndex((node) => { - return node.name === nodeIssueData.node; - }); - - if (nodeIndex === -1) { - return false; - } - - const node = this.workflow.nodes[nodeIndex]; - - if (nodeIssueData.value === null) { - // Remove the value if one exists - if (node.issues?.[nodeIssueData.type] === undefined) { - // No values for type exist so nothing has to get removed - return true; - } - - const { [nodeIssueData.type]: removedNodeIssue, ...remainingNodeIssues } = node.issues; - this.updateNodeAtIndex(nodeIndex, { - issues: remainingNodeIssues, - }); - } else { - if (node.issues === undefined) { - this.updateNodeAtIndex(nodeIndex, { - issues: {}, - }); - } - - this.updateNodeAtIndex(nodeIndex, { - issues: { - ...node.issues, - [nodeIssueData.type]: nodeIssueData.value as INodeIssueObjectProperty, - }, + } else { + if (node.issues === undefined) { + updateNodeAtIndex(nodeIndex, { + issues: {}, }); } - return true; - }, - addNode(nodeData: INodeUi): void { - if (!nodeData.hasOwnProperty('name')) { - // All nodes have to have a name - // TODO: Check if there is an error or whatever that is supposed to be returned + updateNodeAtIndex(nodeIndex, { + issues: { + ...node.issues, + [nodeIssueData.type]: nodeIssueData.value as INodeIssueObjectProperty, + }, + }); + } + return true; + } + + function addNode(nodeData: INodeUi): void { + if (!nodeData.hasOwnProperty('name')) { + // All nodes have to have a name + // TODO: Check if there is an error or whatever that is supposed to be returned + return; + } + + if (nodeData.extendsCredential) { + nodeData.type = getCredentialOnlyNodeTypeName(nodeData.extendsCredential); + } + + workflow.value.nodes.push(nodeData); + // Init node metadata + if (!nodeMetadata.value[nodeData.name]) { + nodeMetadata.value = { ...nodeMetadata.value, [nodeData.name]: {} as INodeMetadata }; + } + } + + function removeNode(node: INodeUi): void { + const uiStore = useUIStore(); + const { [node.name]: removedNodeMetadata, ...remainingNodeMetadata } = nodeMetadata.value; + nodeMetadata.value = remainingNodeMetadata; + + if (workflow.value.pinData && workflow.value.pinData.hasOwnProperty(node.name)) { + const { [node.name]: removedPinData, ...remainingPinData } = workflow.value.pinData; + workflow.value = { + ...workflow.value, + pinData: remainingPinData, + }; + } + + for (let i = 0; i < workflow.value.nodes.length; i++) { + if (workflow.value.nodes[i].name === node.name) { + workflow.value = { + ...workflow.value, + nodes: [...workflow.value.nodes.slice(0, i), ...workflow.value.nodes.slice(i + 1)], + }; + + uiStore.stateIsDirty = true; return; } + } + } - if (nodeData.extendsCredential) { - nodeData.type = getCredentialOnlyNodeTypeName(nodeData.extendsCredential); - } - - this.workflow.nodes.push(nodeData); - // Init node metadata - if (!this.nodeMetadata[nodeData.name]) { - this.nodeMetadata = { ...this.nodeMetadata, [nodeData.name]: {} as INodeMetadata }; - } - }, - - removeNode(node: INodeUi): void { + function removeAllNodes(data: { setStateDirty: boolean; removePinData: boolean }): void { + if (data.setStateDirty) { const uiStore = useUIStore(); - const { [node.name]: removedNodeMetadata, ...remainingNodeMetadata } = this.nodeMetadata; - this.nodeMetadata = remainingNodeMetadata; + uiStore.stateIsDirty = true; + } - if (this.workflow.pinData && this.workflow.pinData.hasOwnProperty(node.name)) { - const { [node.name]: removedPinData, ...remainingPinData } = this.workflow.pinData; - this.workflow = { - ...this.workflow, - pinData: remainingPinData, - }; - } + if (data.removePinData) { + workflow.value = { + ...workflow.value, + pinData: {}, + }; + } - for (let i = 0; i < this.workflow.nodes.length; i++) { - if (this.workflow.nodes[i].name === node.name) { - this.workflow = { - ...this.workflow, - nodes: [...this.workflow.nodes.slice(0, i), ...this.workflow.nodes.slice(i + 1)], - }; + workflow.value.nodes.splice(0, workflow.value.nodes.length); + nodeMetadata.value = {}; + } - uiStore.stateIsDirty = true; - return; - } - } - }, + function updateNodeProperties(updateInformation: INodeUpdatePropertiesInformation): void { + // Find the node that should be updated + const nodeIndex = workflow.value.nodes.findIndex((node) => { + return node.name === updateInformation.name; + }); - removeAllNodes(data: { setStateDirty: boolean; removePinData: boolean }): void { - if (data.setStateDirty) { + if (nodeIndex !== -1) { + for (const key of Object.keys(updateInformation.properties)) { const uiStore = useUIStore(); uiStore.stateIsDirty = true; + + updateNodeAtIndex(nodeIndex, { + [key]: updateInformation.properties[key], + }); } + } + } - if (data.removePinData) { - this.workflow = { - ...this.workflow, - pinData: {}, - }; - } + function setNodeValue(updateInformation: IUpdateInformation): void { + // Find the node that should be updated + const nodeIndex = workflow.value.nodes.findIndex((node) => { + return node.name === updateInformation.name; + }); - this.workflow.nodes.splice(0, this.workflow.nodes.length); - this.nodeMetadata = {}; - }, - - updateNodeProperties(updateInformation: INodeUpdatePropertiesInformation): void { - // Find the node that should be updated - const nodeIndex = this.workflow.nodes.findIndex((node) => { - return node.name === updateInformation.name; - }); - - if (nodeIndex !== -1) { - for (const key of Object.keys(updateInformation.properties)) { - const uiStore = useUIStore(); - uiStore.stateIsDirty = true; - - this.updateNodeAtIndex(nodeIndex, { - [key]: updateInformation.properties[key], - }); - } - } - }, - - setNodeValue(updateInformation: IUpdateInformation): void { - // Find the node that should be updated - const nodeIndex = this.workflow.nodes.findIndex((node) => { - return node.name === updateInformation.name; - }); - - if (nodeIndex === -1 || !updateInformation.key) { - throw new Error( - `Node with the name "${updateInformation.name}" could not be found to set parameter.`, - ); - } - - const uiStore = useUIStore(); - uiStore.stateIsDirty = true; - - this.updateNodeAtIndex(nodeIndex, { - [updateInformation.key]: updateInformation.value, - }); - }, - - setNodeParameters(updateInformation: IUpdateInformation, append?: boolean): void { - // Find the node that should be updated - const nodeIndex = this.workflow.nodes.findIndex((node) => { - return node.name === updateInformation.name; - }); - - if (nodeIndex === -1) { - throw new Error( - `Node with the name "${updateInformation.name}" could not be found to set parameter.`, - ); - } - - const node = this.workflow.nodes[nodeIndex]; - - const uiStore = useUIStore(); - uiStore.stateIsDirty = true; - const newParameters = - !!append && isObject(updateInformation.value) - ? { ...node.parameters, ...updateInformation.value } - : updateInformation.value; - - this.updateNodeAtIndex(nodeIndex, { - parameters: newParameters as INodeParameters, - }); - - this.nodeMetadata = { - ...this.nodeMetadata, - [node.name]: { - ...this.nodeMetadata[node.name], - parametersLastUpdatedAt: Date.now(), - }, - } as NodeMetadataMap; - }, - - setLastNodeParameters(updateInformation: IUpdateInformation) { - const latestNode = findLast( - this.workflow.nodes, - (node) => node.type === updateInformation.key, - ) as INodeUi; - const nodeType = useNodeTypesStore().getNodeType(latestNode.type); - if (!nodeType) return; - - const nodeParams = NodeHelpers.getNodeParameters( - nodeType.properties, - updateInformation.value as INodeParameters, - true, - false, - latestNode, + if (nodeIndex === -1 || !updateInformation.key) { + throw new Error( + `Node with the name "${updateInformation.name}" could not be found to set parameter.`, ); + } - if (latestNode) this.setNodeParameters({ value: nodeParams, name: latestNode.name }, true); - }, + const uiStore = useUIStore(); + uiStore.stateIsDirty = true; - addNodeExecutionData(pushData: IPushDataNodeExecuteAfter): void { - if (!this.workflowExecutionData?.data) { - throw new Error('The "workflowExecutionData" is not initialized!'); - } - if (this.workflowExecutionData.data.resultData.runData[pushData.nodeName] === undefined) { - this.workflowExecutionData = { - ...this.workflowExecutionData, - data: { - ...this.workflowExecutionData.data, - resultData: { - ...this.workflowExecutionData.data.resultData, - runData: { - ...this.workflowExecutionData.data.resultData.runData, - [pushData.nodeName]: [], - }, - }, - }, - }; - } - this.workflowExecutionData.data!.resultData.runData[pushData.nodeName].push(pushData.data); - }, - clearNodeExecutionData(nodeName: string): void { - if (!this.workflowExecutionData?.data) { - return; - } + updateNodeAtIndex(nodeIndex, { + [updateInformation.key]: updateInformation.value, + }); + } - const { [nodeName]: removedRunData, ...remainingRunData } = - this.workflowExecutionData.data.resultData.runData; - this.workflowExecutionData = { - ...this.workflowExecutionData, + function setNodeParameters(updateInformation: IUpdateInformation, append?: boolean): void { + // Find the node that should be updated + const nodeIndex = workflow.value.nodes.findIndex((node) => { + return node.name === updateInformation.name; + }); + + if (nodeIndex === -1) { + throw new Error( + `Node with the name "${updateInformation.name}" could not be found to set parameter.`, + ); + } + + const node = workflow.value.nodes[nodeIndex]; + + const uiStore = useUIStore(); + uiStore.stateIsDirty = true; + const newParameters = + !!append && isObject(updateInformation.value) + ? { ...node.parameters, ...updateInformation.value } + : updateInformation.value; + + updateNodeAtIndex(nodeIndex, { + parameters: newParameters as INodeParameters, + }); + + nodeMetadata.value = { + ...nodeMetadata.value, + [node.name]: { + ...nodeMetadata.value[node.name], + parametersLastUpdatedAt: Date.now(), + }, + } as NodeMetadataMap; + } + + function setLastNodeParameters(updateInformation: IUpdateInformation): void { + const latestNode = findLast( + workflow.value.nodes, + (node) => node.type === updateInformation.key, + ) as INodeUi; + const nodeType = useNodeTypesStore().getNodeType(latestNode.type); + if (!nodeType) return; + + const nodeParams = NodeHelpers.getNodeParameters( + nodeType.properties, + updateInformation.value as INodeParameters, + true, + false, + latestNode, + ); + + if (latestNode) { + setNodeParameters({ value: nodeParams, name: latestNode.name }, true); + } + } + + function addNodeExecutionData(pushData: IPushDataNodeExecuteAfter): void { + if (!workflowExecutionData.value?.data) { + throw new Error('The "workflowExecutionData" is not initialized!'); + } + + if (workflowExecutionData.value.data.resultData.runData[pushData.nodeName] === undefined) { + workflowExecutionData.value = { + ...workflowExecutionData.value, data: { - ...this.workflowExecutionData.data, + ...workflowExecutionData.value.data, resultData: { - ...this.workflowExecutionData.data.resultData, - runData: remainingRunData, + ...workflowExecutionData.value.data.resultData, + runData: { + ...workflowExecutionData.value.data.resultData.runData, + [pushData.nodeName]: [], + }, }, }, }; - }, + } + workflowExecutionData.value.data!.resultData.runData[pushData.nodeName].push(pushData.data); + } - pinDataByNodeName(nodeName: string): INodeExecutionData[] | undefined { - if (!this.workflow.pinData?.[nodeName]) return undefined; + function clearNodeExecutionData(nodeName: string): void { + if (!workflowExecutionData.value?.data) { + return; + } - return this.workflow.pinData[nodeName].map((item) => item.json) as INodeExecutionData[]; - }, - - activeNode(): INodeUi | null { - // kept here for FE hooks - const ndvStore = useNDVStore(); - return ndvStore.activeNode; - }, - - // Executions actions - - addActiveExecution(newActiveExecution: IExecutionsCurrentSummaryExtended): void { - // Check if the execution exists already - const activeExecution = this.activeExecutions.find((execution) => { - return execution.id === newActiveExecution.id; - }); - - if (activeExecution !== undefined) { - // Exists already so no need to add it again - if (activeExecution.workflowName === undefined) { - activeExecution.workflowName = newActiveExecution.workflowName; - } - return; - } - this.activeExecutions.unshift(newActiveExecution); - this.activeExecutionId = newActiveExecution.id; - }, - finishActiveExecution( - finishedActiveExecution: IPushDataExecutionFinished | IPushDataUnsavedExecutionFinished, - ): void { - // Find the execution to set to finished - const activeExecutionIndex = this.activeExecutions.findIndex((execution) => { - return execution.id === finishedActiveExecution.executionId; - }); - - if (activeExecutionIndex === -1) { - // The execution could not be found - return; - } - - const activeExecution = this.activeExecutions[activeExecutionIndex]; - - this.activeExecutions = [ - ...this.activeExecutions.slice(0, activeExecutionIndex), - { - ...activeExecution, - ...(finishedActiveExecution.executionId !== undefined - ? { id: finishedActiveExecution.executionId } - : {}), - finished: finishedActiveExecution.data.finished, - stoppedAt: finishedActiveExecution.data.stoppedAt, + const { [nodeName]: removedRunData, ...remainingRunData } = + workflowExecutionData.value.data.resultData.runData; + workflowExecutionData.value = { + ...workflowExecutionData.value, + data: { + ...workflowExecutionData.value.data, + resultData: { + ...workflowExecutionData.value.data.resultData, + runData: remainingRunData, }, - ...this.activeExecutions.slice(activeExecutionIndex + 1), - ]; + }, + }; + } - if (finishedActiveExecution.data && (finishedActiveExecution.data as IRun).data) { - this.setWorkflowExecutionRunData((finishedActiveExecution.data as IRun).data); + function pinDataByNodeName(nodeName: string): INodeExecutionData[] | undefined { + if (!workflow.value.pinData?.[nodeName]) return undefined; + + return workflow.value.pinData[nodeName].map((item) => item.json) as INodeExecutionData[]; + } + + function activeNode(): INodeUi | null { + // kept here for FE hooks + const ndvStore = useNDVStore(); + return ndvStore.activeNode; + } + + function addActiveExecution(newActiveExecution: IExecutionsCurrentSummaryExtended): void { + // Check if the execution exists already + const activeExecution = activeExecutions.value.find((execution) => { + return execution.id === newActiveExecution.id; + }); + + if (activeExecution !== undefined) { + // Exists already so no need to add it again + if (activeExecution.workflowName === undefined) { + activeExecution.workflowName = newActiveExecution.workflowName; } - }, + return; + } - setActiveExecutions(newActiveExecutions: IExecutionsCurrentSummaryExtended[]): void { - this.activeExecutions = newActiveExecutions; - }, - // TODO: For sure needs some kind of default filter like last day, with max 10 results, ... - async getPastExecutions( - filter: IDataObject, - limit: number, - lastId?: string, - firstId?: string, - ): Promise { - let sendData = {}; - if (filter) { - sendData = { - filter, - firstId, - lastId, - limit, - }; - } - const rootStore = useRootStore(); - return await makeRestApiRequest(rootStore.getRestApiContext, 'GET', '/executions', sendData); - }, + activeExecutions.value.unshift(newActiveExecution); + activeExecutionId.value = newActiveExecution.id; + } - async getActiveExecutions(filter: IDataObject): Promise { - let sendData = {}; - if (filter) { - sendData = { - filter, - }; - } - const rootStore = useRootStore(); - const output = await makeRestApiRequest( - rootStore.getRestApiContext, - 'GET', - '/executions', - sendData, - ); + function finishActiveExecution( + finishedActiveExecution: IPushDataExecutionFinished | IPushDataUnsavedExecutionFinished, + ): void { + // Find the execution to set to finished + const activeExecutionIndex = activeExecutions.value.findIndex((execution) => { + return execution.id === finishedActiveExecution.executionId; + }); - return output.results; - }, + if (activeExecutionIndex === -1) { + // The execution could not be found + return; + } - async getExecution(id: string): Promise { - const rootStore = useRootStore(); - const response = await makeRestApiRequest( - rootStore.getRestApiContext, - 'GET', - `/executions/${id}`, - ); - return response && unflattenExecutionData(response); - }, + const activeExecution = activeExecutions.value[activeExecutionIndex]; - // Creates a new workflow - async createNewWorkflow(sendData: IWorkflowDataUpdate): Promise { - // make sure that the new ones are not active - sendData.active = false; + activeExecutions.value = [ + ...activeExecutions.value.slice(0, activeExecutionIndex), + { + ...activeExecution, + ...(finishedActiveExecution.executionId !== undefined + ? { id: finishedActiveExecution.executionId } + : {}), + finished: finishedActiveExecution.data.finished, + stoppedAt: finishedActiveExecution.data.stoppedAt, + }, + ...activeExecutions.value.slice(activeExecutionIndex + 1), + ]; - const rootStore = useRootStore(); + if (finishedActiveExecution.data && (finishedActiveExecution.data as IRun).data) { + setWorkflowExecutionRunData((finishedActiveExecution.data as IRun).data); + } + } + + function setActiveExecutions(newActiveExecutions: IExecutionsCurrentSummaryExtended[]): void { + activeExecutions.value = newActiveExecutions; + } + + // TODO: For sure needs some kind of default filter like last day, with max 10 results, ... + async function getPastExecutions( + filter: IDataObject, + limit: number, + lastId?: string, + firstId?: string, + ): Promise { + let sendData = {}; + if (filter) { + sendData = { + filter, + firstId, + lastId, + limit, + }; + } + const rootStore = useRootStore(); + return await makeRestApiRequest(rootStore.getRestApiContext, 'GET', '/executions', sendData); + } + + async function getActiveExecutions( + filter: IDataObject, + ): Promise { + let sendData = {}; + if (filter) { + sendData = { + filter, + }; + } + const rootStore = useRootStore(); + const output = await makeRestApiRequest<{ results: IExecutionsCurrentSummaryExtended[] }>( + rootStore.getRestApiContext, + 'GET', + '/executions', + sendData, + ); + + return output.results; + } + + async function getExecution(id: string): Promise { + const rootStore = useRootStore(); + const response = await makeRestApiRequest( + rootStore.getRestApiContext, + 'GET', + `/executions/${id}`, + ); + + return response && unflattenExecutionData(response); + } + + async function createNewWorkflow(sendData: IWorkflowDataUpdate): Promise { + // make sure that the new ones are not active + sendData.active = false; + + const rootStore = useRootStore(); + return await makeRestApiRequest( + rootStore.getRestApiContext, + 'POST', + '/workflows', + sendData as unknown as IDataObject, + ); + } + + async function updateWorkflow( + id: string, + data: IWorkflowDataUpdate, + forceSave = false, + ): Promise { + const rootStore = useRootStore(); + + if (data.settings === null) { + data.settings = undefined; + } + + return await makeRestApiRequest( + rootStore.getRestApiContext, + 'PATCH', + `/workflows/${id}${forceSave ? '?forceSave=true' : ''}`, + data as unknown as IDataObject, + ); + } + + async function runWorkflow(startRunData: IStartRunData): Promise { + const rootStore = useRootStore(); + + if (startRunData.workflowData.settings === null) { + startRunData.workflowData.settings = undefined; + } + + try { return await makeRestApiRequest( rootStore.getRestApiContext, 'POST', - '/workflows', - sendData as unknown as IDataObject, + '/workflows/run', + startRunData as unknown as IDataObject, ); - }, - - // Updates an existing workflow - async updateWorkflow( - id: string, - data: IWorkflowDataUpdate, - forceSave = false, - ): Promise { - const rootStore = useRootStore(); - - if (data.settings === null) { - EventReporter.info('Detected workflow payload with settings as null'); - data.settings = undefined; + } catch (error) { + if (error.response?.status === 413) { + throw new ResponseError(i18n.baseText('workflowRun.showError.payloadTooLarge'), { + errorCode: 413, + httpStatusCode: 413, + }); } + throw error; + } + } - return await makeRestApiRequest( - rootStore.getRestApiContext, - 'PATCH', - `/workflows/${id}${forceSave ? '?forceSave=true' : ''}`, - data as unknown as IDataObject, - ); - }, + async function removeTestWebhook(targetWorkflowId: string): Promise { + const rootStore = useRootStore(); + return await makeRestApiRequest( + rootStore.getRestApiContext, + 'DELETE', + `/test-webhook/${targetWorkflowId}`, + ); + } - async runWorkflow(startRunData: IStartRunData): Promise { + async function loadCurrentWorkflowExecutions( + requestFilter: ExecutionsQueryFilter, + ): Promise { + let retrievedActiveExecutions: IExecutionsCurrentSummaryExtended[] = []; + + if (!requestFilter.workflowId) { + return []; + } + + try { const rootStore = useRootStore(); - - if (startRunData.workflowData.settings === null) { - EventReporter.info('Detected workflow payload with settings as null'); - startRunData.workflowData.settings = undefined; - } - - try { - return await makeRestApiRequest( + if ((!requestFilter.status || !requestFilter.finished) && isEmpty(requestFilter.metadata)) { + retrievedActiveExecutions = await workflowsApi.getActiveExecutions( rootStore.getRestApiContext, - 'POST', - '/workflows/run', - startRunData as unknown as IDataObject, - ); - } catch (error) { - if (error.response?.status === 413) { - throw new ResponseError(i18n.baseText('workflowRun.showError.payloadTooLarge'), { - errorCode: 413, - httpStatusCode: 413, - }); - } - throw error; - } - }, - - async removeTestWebhook(workflowId: string): Promise { - const rootStore = useRootStore(); - return await makeRestApiRequest( - rootStore.getRestApiContext, - 'DELETE', - `/test-webhook/${workflowId}`, - ); - }, - async loadCurrentWorkflowExecutions( - requestFilter: ExecutionsQueryFilter, - ): Promise { - let activeExecutions = []; - - if (!requestFilter.workflowId) { - return []; - } - try { - const rootStore = useRootStore(); - if ((!requestFilter.status || !requestFilter.finished) && isEmpty(requestFilter.metadata)) { - activeExecutions = await getActiveExecutions(rootStore.getRestApiContext, { + { workflowId: requestFilter.workflowId, - }); - } - const finishedExecutions = await getExecutions(rootStore.getRestApiContext, requestFilter); - this.finishedExecutionsCount = finishedExecutions.count; - return [...activeExecutions, ...(finishedExecutions.results || [])]; - } catch (error) { - throw error; + }, + ); } - }, - - async fetchExecutionDataById(executionId: string): Promise { - const rootStore = useRootStore(); - return await getExecutionData(rootStore.getRestApiContext, executionId); - }, - - deleteExecution(execution: ExecutionSummary): void { - this.currentWorkflowExecutions.splice(this.currentWorkflowExecutions.indexOf(execution), 1); - }, - - addToCurrentExecutions(executions: ExecutionSummary[]): void { - executions.forEach((execution) => { - const exists = this.currentWorkflowExecutions.find((ex) => ex.id === execution.id); - if (!exists && execution.workflowId === this.workflowId) { - this.currentWorkflowExecutions.push(execution); - } - }); - }, - // Returns all the available timezones - async getExecutionEvents(id: string): Promise { - const rootStore = useRootStore(); - return await makeRestApiRequest( + const retrievedFinishedExecutions = await workflowsApi.getExecutions( rootStore.getRestApiContext, - 'GET', - '/eventbus/execution/' + id, + requestFilter, ); - }, - // Binary data - getBinaryUrl( - binaryDataId: string, - action: 'view' | 'download', - fileName: string, - mimeType: string, - ): string { - const rootStore = useRootStore(); - let restUrl = rootStore.getRestUrl; - if (restUrl.startsWith('/')) restUrl = window.location.origin + restUrl; - const url = new URL(`${restUrl}/binary-data`); - url.searchParams.append('id', binaryDataId); - url.searchParams.append('action', action); - if (fileName) url.searchParams.append('fileName', fileName); - if (mimeType) url.searchParams.append('mimeType', mimeType); - return url.toString(); - }, + finishedExecutionsCount.value = retrievedFinishedExecutions.count; + return [...retrievedActiveExecutions, ...(retrievedFinishedExecutions.results || [])]; + } catch (error) { + throw error; + } + } - setNodePristine(nodeName: string, isPristine: boolean): void { - this.nodeMetadata = { - ...this.nodeMetadata, - [nodeName]: { - ...this.nodeMetadata[nodeName], - pristine: isPristine, - }, - }; - }, + async function fetchExecutionDataById(executionId: string): Promise { + const rootStore = useRootStore(); + return await workflowsApi.getExecutionData(rootStore.getRestApiContext, executionId); + } - resetChatMessages(): void { - this.chatMessages = []; - }, + function deleteExecution(execution: ExecutionSummary): void { + currentWorkflowExecutions.value.splice(currentWorkflowExecutions.value.indexOf(execution), 1); + } - appendChatMessage(message: string): void { - this.chatMessages.push(message); - }, + function addToCurrentExecutions(executions: ExecutionSummary[]): void { + executions.forEach((execution) => { + const exists = currentWorkflowExecutions.value.find((ex) => ex.id === execution.id); + if (!exists && execution.workflowId === workflowId.value) { + currentWorkflowExecutions.value.push(execution); + } + }); + } - checkIfNodeHasChatParent(nodeName: string): boolean { - const workflow = this.getCurrentWorkflow(); - const parents = workflow.getParentNodes(nodeName, 'main'); + async function getExecutionEvents(id: string): Promise { + const rootStore = useRootStore(); + return await makeRestApiRequest( + rootStore.getRestApiContext, + 'GET', + `/eventbus/execution/${id}`, + ); + } - const matchedChatNode = parents.find((parent) => { - const parentNodeType = this.getNodeByName(parent)?.type; + function getBinaryUrl( + binaryDataId: string, + action: 'view' | 'download', + fileName: string, + mimeType: string, + ): string { + const rootStore = useRootStore(); + let restUrl = rootStore.getRestUrl; + if (restUrl.startsWith('/')) restUrl = window.location.origin + restUrl; + const url = new URL(`${restUrl}/binary-data`); + url.searchParams.append('id', binaryDataId); + url.searchParams.append('action', action); + if (fileName) url.searchParams.append('fileName', fileName); + if (mimeType) url.searchParams.append('mimeType', mimeType); + return url.toString(); + } - return parentNodeType === CHAT_TRIGGER_NODE_TYPE; - }); + function setNodePristine(nodeName: string, isPristine: boolean): void { + nodeMetadata.value = { + ...nodeMetadata.value, + [nodeName]: { + ...nodeMetadata.value[nodeName], + pristine: isPristine, + }, + }; + } - return !!matchedChatNode; - }, - }, + function resetChatMessages(): void { + chatMessages.value = []; + } + + function appendChatMessage(message: string): void { + chatMessages.value.push(message); + } + + function checkIfNodeHasChatParent(nodeName: string): boolean { + const workflow = getCurrentWorkflow(); + const parents = workflow.getParentNodes(nodeName, 'main'); + + const matchedChatNode = parents.find((parent) => { + const parentNodeType = getNodeByName(parent)?.type; + + return parentNodeType === CHAT_TRIGGER_NODE_TYPE; + }); + + return !!matchedChatNode; + } + + return { + workflow, + usedCredentials, + activeWorkflows, + activeExecutions, + activeWorkflowExecution, + currentWorkflowExecutions, + finishedExecutionsCount, + workflowExecutionData, + workflowExecutionPairedItemMappings, + activeExecutionId, + subWorkflowExecutionError, + executionWaitingForWebhook, + executingNode, + workflowsById, + nodeMetadata, + isInDebugMode, + chatMessages, + workflowName, + workflowId, + workflowVersionId, + workflowSettings, + workflowTags, + allWorkflows, + isNewWorkflow, + isWorkflowActive, + workflowTriggerNodes, + currentWorkflowHasWebhookNode, + getWorkflowRunData, + getWorkflowResultDataByNodeName, + allConnections, + allNodes, + canvasNames, + nodesByName, + nodesIssuesExist, + pinnedWorkflowData, + shouldReplaceInputDataWithPinData, + executedNode, + getAllLoadedFinishedExecutions, + getWorkflowExecution, + getTotalFinishedExecutionsCount, + getPastChatMessages, + outgoingConnectionsByNodeName, + nodeHasOutputConnection, + isNodeInOutgoingNodeConnections, + getWorkflowById, + getNodeByName, + getNodeById, + getParametersLastUpdate, + isNodePristine, + isNodeExecuting, + getExecutionDataById, + getPinDataSize, + getNodeTypes, + getNodes, + getWorkflow, + getCurrentWorkflow, + getWorkflowFromUrl, + getActivationError, + fetchAllWorkflows, + fetchWorkflow, + getNewWorkflowData, + resetWorkflow, + resetState, + addExecutingNode, + removeExecutingNode, + setWorkflowId, + setUsedCredentials, + setWorkflowName, + setWorkflowVersionId, + replaceInvalidWorkflowCredentials, + setWorkflows, + deleteWorkflow, + addWorkflow, + setWorkflowActive, + setWorkflowInactive, + fetchActiveWorkflows, + setActive, + getDuplicateCurrentWorkflowName, + setWorkflowExecutionData, + setWorkflowExecutionRunData, + setWorkflowSettings, + setWorkflowPinData, + setWorkflowTagIds, + addWorkflowTagIds, + removeWorkflowTagId, + setWorkflowMetadata, + addToWorkflowMetadata, + setWorkflow, + pinData, + unpinData, + addConnection, + removeConnection, + removeAllConnections, + removeAllNodeConnection, + renameNodeSelectedAndExecution, + resetAllNodesIssues, + updateNodeAtIndex, + setNodeIssue, + addNode, + removeNode, + removeAllNodes, + updateNodeProperties, + setNodeValue, + setNodeParameters, + setLastNodeParameters, + addNodeExecutionData, + clearNodeExecutionData, + pinDataByNodeName, + activeNode, + addActiveExecution, + finishActiveExecution, + setActiveExecutions, + getPastExecutions, + getActiveExecutions, + getExecution, + createNewWorkflow, + updateWorkflow, + runWorkflow, + removeTestWebhook, + loadCurrentWorkflowExecutions, + fetchExecutionDataById, + deleteExecution, + addToCurrentExecutions, + getExecutionEvents, + getBinaryUrl, + setNodePristine, + resetChatMessages, + appendChatMessage, + checkIfNodeHasChatParent, + }; });