From c3cf5c70573571008e7c8644378e833bfbea0b92 Mon Sep 17 00:00:00 2001 From: Michael Kret <88898367+michael-radency@users.noreply.github.com> Date: Thu, 14 May 2026 20:49:39 +0300 Subject: [PATCH] fix(editor): Preserve custom Form Trigger path on workflow re-import (#30053) --- .../composables/useCanvasOperations.test.ts | 193 +++++++++++++++++- .../app/composables/useCanvasOperations.ts | 17 +- 2 files changed, 196 insertions(+), 14 deletions(-) diff --git a/packages/frontend/editor-ui/src/app/composables/useCanvasOperations.test.ts b/packages/frontend/editor-ui/src/app/composables/useCanvasOperations.test.ts index ddace7f3e2e..d284fd55fc0 100644 --- a/packages/frontend/editor-ui/src/app/composables/useCanvasOperations.test.ts +++ b/packages/frontend/editor-ui/src/app/composables/useCanvasOperations.test.ts @@ -4680,7 +4680,7 @@ describe('useCanvasOperations', () => { }); it.each(UPDATE_WEBHOOK_ID_NODE_TYPES)( - 'should regenerate webhook ids for node type "%s" on pasting into canvas', + 'should regenerate webhook ids for node type "%s" on pasting into canvas and sync default paths', async (type) => { // This mock is needed for addImportedNodesToWorkflow to work vi.mocked(workflowDocumentStoreInstance.createWorkflowObject).mockReturnValue({ @@ -4699,7 +4699,7 @@ describe('useCanvasOperations', () => { position: [200, 200] as [number, number], webhookId: 'first-webhook', parameters: { - path: 'some-path', + path: 'first-webhook', }, }, { @@ -4711,7 +4711,7 @@ describe('useCanvasOperations', () => { webhookId: 'second-webhook', parameters: { options: { - path: 'some-path', + path: 'second-webhook', }, }, }, @@ -4728,21 +4728,200 @@ describe('useCanvasOperations', () => { const canvasOperations = useCanvasOperations(); - // This should not throw even when nodes can't be added due to maxNodes limit const workflow = await canvasOperations.importWorkflowData(workflowDataToImport, 'paste'); expect(workflow.nodes).toHaveLength(2); expect(workflow.nodes![0].name).toBe('Execute Workflow Trigger 1'); expect(workflow.nodes![0].webhookId).not.toBe('first-webhook'); - expect(workflow.nodes![0].parameters.path).not.toBe('some-path'); + expect(workflow.nodes![0].parameters.path).toBe(workflow.nodes![0].webhookId); expect(workflow.nodes![1].name).toBe('Execute Workflow Trigger 2'); expect(workflow.nodes![1].webhookId).not.toBe('second-webhook'); - expect((workflow.nodes![1].parameters.options as { path: string }).path).not.toBe( - 'some-path', + expect((workflow.nodes![1].parameters.options as { path: string }).path).toBe( + workflow.nodes![1].webhookId, ); }, ); + it.each(UPDATE_WEBHOOK_ID_NODE_TYPES)( + 'should preserve user-defined paths for node type "%s" on importing into canvas', + async (type) => { + // This mock is needed for addImportedNodesToWorkflow to work + vi.mocked(workflowDocumentStoreInstance.createWorkflowObject).mockReturnValue({ + nodes: {}, + connections: {}, + connectionsBySourceNode: {}, + renameNode: vi.fn(), + } as unknown as Workflow); + + const nodesToImport = [ + { + id: 'import-1', + name: 'Execute Workflow Trigger 1', + type, + typeVersion: 1, + position: [200, 200] as [number, number], + webhookId: 'first-webhook', + parameters: { + path: 'my-custom-path', + }, + }, + { + id: 'import-2', + name: 'Execute Workflow Trigger 2', + type, + typeVersion: 1, + position: [300, 300] as [number, number], + webhookId: 'second-webhook', + parameters: { + options: { + path: 'my-custom-form', + }, + }, + }, + ]; + + const workflowDataToImport = { + nodes: nodesToImport, + connections: {}, + }; + + const canvasOperations = useCanvasOperations(); + + const workflow = await canvasOperations.importWorkflowData(workflowDataToImport, 'file'); + + expect(workflow.nodes).toHaveLength(2); + expect(workflow.nodes![0].webhookId).not.toBe('first-webhook'); + expect(workflow.nodes![0].parameters.path).toBe('my-custom-path'); + expect(workflow.nodes![1].webhookId).not.toBe('second-webhook'); + expect((workflow.nodes![1].parameters.options as { path: string }).path).toBe( + 'my-custom-form', + ); + }, + ); + + it.each(UPDATE_WEBHOOK_ID_NODE_TYPES)( + 'should preserve user-defined paths for node type "%s" on pasting into canvas', + async (type) => { + vi.mocked(workflowDocumentStoreInstance.createWorkflowObject).mockReturnValue({ + nodes: {}, + connections: {}, + connectionsBySourceNode: {}, + renameNode: vi.fn(), + } as unknown as Workflow); + + const nodesToImport = [ + { + id: 'import-1', + name: 'Execute Workflow Trigger 1', + type, + typeVersion: 1, + position: [200, 200] as [number, number], + webhookId: 'first-webhook', + parameters: { + options: { + path: 'my-custom-form', + }, + }, + }, + ]; + + const workflowDataToImport = { + nodes: nodesToImport, + connections: {}, + }; + + const canvasOperations = useCanvasOperations(); + + const workflow = await canvasOperations.importWorkflowData(workflowDataToImport, 'paste'); + + expect(workflow.nodes).toHaveLength(1); + expect(workflow.nodes![0].webhookId).not.toBe('first-webhook'); + expect((workflow.nodes![0].parameters.options as { path: string }).path).toBe( + 'my-custom-form', + ); + }, + ); + + it.each(UPDATE_WEBHOOK_ID_NODE_TYPES)( + 'should regenerate webhook id for node type "%s" when no path is defined', + async (type) => { + vi.mocked(workflowDocumentStoreInstance.createWorkflowObject).mockReturnValue({ + nodes: {}, + connections: {}, + connectionsBySourceNode: {}, + renameNode: vi.fn(), + } as unknown as Workflow); + + const nodesToImport = [ + { + id: 'import-1', + name: 'Execute Workflow Trigger 1', + type, + typeVersion: 1, + position: [200, 200] as [number, number], + webhookId: 'first-webhook', + parameters: {}, + }, + ]; + + const workflowDataToImport = { + nodes: nodesToImport, + connections: {}, + }; + + const canvasOperations = useCanvasOperations(); + + const workflow = await canvasOperations.importWorkflowData(workflowDataToImport, 'paste'); + + expect(workflow.nodes).toHaveLength(1); + expect(workflow.nodes![0].webhookId).not.toBe('first-webhook'); + expect(workflow.nodes![0].parameters.path).toBeUndefined(); + expect(workflow.nodes![0].parameters.options).toBeUndefined(); + }, + ); + + it.each(UPDATE_WEBHOOK_ID_NODE_TYPES)( + 'should preserve user-defined parameters.path even when it equals the empty string for node type "%s"', + async (type) => { + vi.mocked(workflowDocumentStoreInstance.createWorkflowObject).mockReturnValue({ + nodes: {}, + connections: {}, + connectionsBySourceNode: {}, + renameNode: vi.fn(), + } as unknown as Workflow); + + const nodesToImport = [ + { + id: 'import-1', + name: 'Execute Workflow Trigger 1', + type, + typeVersion: 1, + position: [200, 200] as [number, number], + webhookId: 'first-webhook', + parameters: { + options: { + path: '', + }, + }, + }, + ]; + + const workflowDataToImport = { + nodes: nodesToImport, + connections: {}, + }; + + const canvasOperations = useCanvasOperations(); + + const workflow = await canvasOperations.importWorkflowData(workflowDataToImport, 'file'); + + expect(workflow.nodes).toHaveLength(1); + expect(workflow.nodes![0].webhookId).not.toBe('first-webhook'); + // Empty options.path is not equal to previous webhookId, so it should not be replaced + expect((workflow.nodes![0].parameters.options as { path: string }).path).toBe(''); + }, + ); + it.each([WEBHOOK_NODE_TYPE, MCP_TRIGGER_NODE_TYPE])( 'should not regenerate webhook ids for node type "%s" on pasting into canvas', async (type) => { diff --git a/packages/frontend/editor-ui/src/app/composables/useCanvasOperations.ts b/packages/frontend/editor-ui/src/app/composables/useCanvasOperations.ts index 4b93051a44b..713e0ed78f7 100644 --- a/packages/frontend/editor-ui/src/app/composables/useCanvasOperations.ts +++ b/packages/frontend/editor-ui/src/app/composables/useCanvasOperations.ts @@ -2672,14 +2672,17 @@ export function useCanvasOperations() { // Generate new webhookId if (node.webhookId && UPDATE_WEBHOOK_ID_NODE_TYPES.includes(node.type)) { - if (node.webhookId) { - nodeHelpers.assignWebhookId(node); + const previousWebhookId = node.webhookId; + const pathMatchedWebhookId = node.parameters.path === previousWebhookId; + const optionsPathMatchedWebhookId = + (node.parameters.options as IDataObject)?.path === previousWebhookId; - if (node.parameters.path) { - node.parameters.path = node.webhookId; - } else if ((node.parameters.options as IDataObject)?.path) { - (node.parameters.options as IDataObject).path = node.webhookId; - } + nodeHelpers.assignWebhookId(node); + + if (pathMatchedWebhookId) { + node.parameters.path = node.webhookId; + } else if (optionsPathMatchedWebhookId) { + (node.parameters.options as IDataObject).path = node.webhookId; } }