fix(editor): Preserve custom Form Trigger path on workflow re-import (#30053)

This commit is contained in:
Michael Kret 2026-05-14 20:49:39 +03:00 committed by GitHub
parent f1fd79f830
commit c3cf5c7057
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 196 additions and 14 deletions

View File

@ -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) => {

View File

@ -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;
}
}