From be4f54de157dde60e7ae6b0611fa599a059cd17f Mon Sep 17 00:00:00 2001 From: oleg Date: Thu, 30 May 2024 16:53:33 +0200 Subject: [PATCH] feat(editor): Node Creator AI nodes improvements (#9484) Signed-off-by: Oleg Ivaniv --- cypress/constants.ts | 2 +- cypress/e2e/19-execution.cy.ts | 2 +- cypress/e2e/4-node-creator.cy.ts | 6 +- cypress/e2e/41-editors.cy.ts | 2 +- cypress/e2e/5-ndv.cy.ts | 2 +- cypress/fixtures/Floating_Nodes.json | 4 +- cypress/fixtures/Lots_of_nodes.json | 4 +- .../fixtures/Multiple_trigger_node_rerun.json | 6 +- cypress/fixtures/NDV-test-switch_reorder.json | 4 +- cypress/fixtures/Node_IO_filter.json | 6 +- cypress/fixtures/Suggested_Templates.json | 16 +- ...flow_pairedItem_incomplete_manual_bug.json | 4 +- ...kflow-actions_import_nodes_empty_name.json | 4 +- cypress/fixtures/Test_workflow_5.json | 4 +- cypress/fixtures/Test_workflow_filter.json | 4 +- .../fixtures/Test_workflow_ndv_run_error.json | 4 +- .../fixtures/Test_workflow_ndv_version.json | 2 +- ...al_execution_with_missing_credentials.json | 4 +- .../fixtures/Test_workflow_schema_test.json | 4 +- ...Test_workflow_schema_test_pinned_data.json | 4 +- .../Test_workflow_webhook_with_pin_data.json | 4 +- .../fixtures/Test_workflow_xml_output.json | 4 +- ..._with_paired_item_in_multi_input_node.json | 4 +- .../open_node_creator_for_connection.json | 4 +- .../workflow-with-unknown-credentials.json | 4 +- .../fixtures/workflow-with-unknown-nodes.json | 4 +- .../nodes/agents/Agent/Agent.node.ts | 2 +- .../OpenAiAssistant/OpenAiAssistant.node.ts | 2 +- .../nodes/chains/ChainLLM/ChainLlm.node.ts | 2 +- .../ChainRetrievalQA/ChainRetrievalQa.node.ts | 2 +- .../ChainSummarization.node.ts | 2 +- .../MemoryManager/MemoryManager.node.ts | 2 +- .../OutputParserStructured.node.ts | 73 ++++++++ .../trigger/ChatTrigger/ChatTrigger.node.ts | 3 - .../OpenAi/actions/versionDescription.ts | 2 +- .../N8nNodeCreatorNode/NodeCreatorNode.vue | 17 +- packages/design-system/src/types/index.ts | 1 + .../src/types/node-creator-node.ts | 6 + packages/editor-ui/src/Interface.ts | 26 ++- .../Node/NodeCreator/ItemTypes/LinkItem.vue | 32 ++++ .../Node/NodeCreator/ItemTypes/NodeItem.vue | 1 + .../Node/NodeCreator/Modes/NodesMode.vue | 11 +- .../NodeCreator/Renderers/ItemsRenderer.vue | 41 +++++ .../__tests__/NodesListPanel.test.ts | 4 +- .../NodeCreator/composables/useViewStacks.ts | 165 ++++++++++++++++-- .../src/components/Node/NodeCreator/utils.ts | 69 ++++++-- .../components/Node/NodeCreator/viewsData.ts | 101 +++++++---- packages/editor-ui/src/constants.ts | 11 +- .../src/plugins/i18n/locales/en.json | 26 ++- packages/editor-ui/src/stores/canvas.store.ts | 24 ++- .../editor-ui/src/stores/templates.store.ts | 16 +- .../utils/__tests__/pairedItemUtils.test.ts | 22 +-- packages/editor-ui/src/views/NodeView.vue | 46 +++-- .../ConvertToFile/test/toText.workflow.json | 4 +- .../nodes/GraphQL/test/workflow.json | 4 +- .../nodes/Jwt/test/jwt.workflow.json | 6 +- .../nodes/ManualTrigger/ManualTrigger.node.ts | 4 +- .../test/node/workflow.keep_non_matches.json | 4 +- .../Switch/V3/test/switch.regex.workflow.json | 4 +- .../workflow/test/TelemetryHelpers.test.ts | 6 +- .../WorkflowDataProxy/errors_run.json | 8 +- .../WorkflowDataProxy/errors_workflow.json | 4 +- 62 files changed, 661 insertions(+), 204 deletions(-) create mode 100644 packages/design-system/src/types/node-creator-node.ts create mode 100644 packages/editor-ui/src/components/Node/NodeCreator/ItemTypes/LinkItem.vue diff --git a/cypress/constants.ts b/cypress/constants.ts index 551c244102..39c755738c 100644 --- a/cypress/constants.ts +++ b/cypress/constants.ts @@ -35,7 +35,7 @@ export const INSTANCE_MEMBERS = [ ]; export const MANUAL_TRIGGER_NODE_NAME = 'Manual Trigger'; -export const MANUAL_TRIGGER_NODE_DISPLAY_NAME = 'When clicking "Test workflow"'; +export const MANUAL_TRIGGER_NODE_DISPLAY_NAME = 'When clicking ‘Test workflow’'; export const MANUAL_CHAT_TRIGGER_NODE_NAME = 'Chat Trigger'; export const SCHEDULE_TRIGGER_NODE_NAME = 'Schedule Trigger'; export const CODE_NODE_NAME = 'Code'; diff --git a/cypress/e2e/19-execution.cy.ts b/cypress/e2e/19-execution.cy.ts index 98c0909b4d..84b71e0885 100644 --- a/cypress/e2e/19-execution.cy.ts +++ b/cypress/e2e/19-execution.cy.ts @@ -510,7 +510,7 @@ describe('Execution', () => { cy.wait('@workflowRun').then((interception) => { expect(interception.request.body).to.have.property('runData').that.is.an('object'); - const expectedKeys = ['When clicking "Test workflow"', 'fetch 5 random users']; + const expectedKeys = ['When clicking ‘Test workflow’', 'fetch 5 random users']; expect(Object.keys(interception.request.body.runData)).to.have.lengthOf(expectedKeys.length); expect(interception.request.body.runData).to.include.all.keys(expectedKeys); diff --git a/cypress/e2e/4-node-creator.cy.ts b/cypress/e2e/4-node-creator.cy.ts index 6955c95463..bb47ef4765 100644 --- a/cypress/e2e/4-node-creator.cy.ts +++ b/cypress/e2e/4-node-creator.cy.ts @@ -35,7 +35,7 @@ describe('Node Creator', () => { nodeCreatorFeature.actions.openNodeCreator(); nodeCreatorFeature.getters.searchBar().find('input').type('manual'); - nodeCreatorFeature.getters.creatorItem().should('have.length', 2); + nodeCreatorFeature.getters.creatorItem().should('have.length', 1); nodeCreatorFeature.getters.searchBar().find('input').clear().type('manual123'); nodeCreatorFeature.getters.creatorItem().should('have.length', 0); nodeCreatorFeature.getters @@ -159,7 +159,7 @@ describe('Node Creator', () => { it('should have "Triggers" section collapsed when opening actions view from Regular root view', () => { nodeCreatorFeature.actions.openNodeCreator(); - nodeCreatorFeature.getters.getCreatorItem('Manually').click(); + nodeCreatorFeature.getters.getCreatorItem('Trigger manually').click(); nodeCreatorFeature.actions.openNodeCreator(); nodeCreatorFeature.getters.searchBar().find('input').clear().type('n8n'); @@ -308,7 +308,7 @@ describe('Node Creator', () => { nodeCreatorFeature.getters.getCategoryItem('Actions').click(); nodeCreatorFeature.getters.getCreatorItem('Create a credential').click(); NDVModal.actions.close(); - WorkflowPage.actions.deleteNode('When clicking "Test workflow"'); + WorkflowPage.actions.deleteNode('When clicking ‘Test workflow’'); WorkflowPage.getters.canvasNodePlusEndpointByName('n8n').click(); nodeCreatorFeature.getters.searchBar().find('input').clear().type('n8n'); nodeCreatorFeature.getters.getCreatorItem('n8n').click(); diff --git a/cypress/e2e/41-editors.cy.ts b/cypress/e2e/41-editors.cy.ts index b50aa66872..0c44c51185 100644 --- a/cypress/e2e/41-editors.cy.ts +++ b/cypress/e2e/41-editors.cy.ts @@ -38,7 +38,7 @@ describe('Editors', () => { }); ndv.actions.close(); - workflowPage.actions.openNode('When clicking "Test workflow"'); + workflowPage.actions.openNode('When clicking ‘Test workflow’'); ndv.actions.setPinnedData([{ table: 'test_table' }]); ndv.actions.close(); diff --git a/cypress/e2e/5-ndv.cy.ts b/cypress/e2e/5-ndv.cy.ts index c322def1b8..0ebd859174 100644 --- a/cypress/e2e/5-ndv.cy.ts +++ b/cypress/e2e/5-ndv.cy.ts @@ -652,7 +652,7 @@ describe('NDV', () => { ndv.getters.backToCanvas().click(); workflowPage.actions.executeWorkflow(); // Manual tigger node should show success indicator - workflowPage.actions.openNode('When clicking "Test workflow"'); + workflowPage.actions.openNode('When clicking ‘Test workflow’'); ndv.getters.nodeRunSuccessIndicator().should('exist'); // Code node should show error ndv.getters.backToCanvas().click(); diff --git a/cypress/fixtures/Floating_Nodes.json b/cypress/fixtures/Floating_Nodes.json index 2ffc1b3fde..6624c53ac6 100644 --- a/cypress/fixtures/Floating_Nodes.json +++ b/cypress/fixtures/Floating_Nodes.json @@ -4,7 +4,7 @@ { "parameters": {}, "id": "d0eda550-2526-42a1-aa19-dee411c8acf9", - "name": "When clicking \"Test workflow\"", + "name": "When clicking ‘Test workflow’", "type": "n8n-nodes-base.manualTrigger", "typeVersion": 1, "position": [ @@ -91,7 +91,7 @@ ], "pinData": {}, "connections": { - "When clicking \"Test workflow\"": { + "When clicking ‘Test workflow’": { "main": [ [ { diff --git a/cypress/fixtures/Lots_of_nodes.json b/cypress/fixtures/Lots_of_nodes.json index 7b3ad507c8..11152fb496 100644 --- a/cypress/fixtures/Lots_of_nodes.json +++ b/cypress/fixtures/Lots_of_nodes.json @@ -4,7 +4,7 @@ { "parameters": {}, "id": "369fe424-dd3b-4399-9de3-50bd4ce1f75b", - "name": "When clicking \"Test workflow\"", + "name": "When clicking ‘Test workflow’", "type": "n8n-nodes-base.manualTrigger", "typeVersion": 1, "position": [ @@ -570,7 +570,7 @@ ], "pinData": {}, "connections": { - "When clicking \"Test workflow\"": { + "When clicking ‘Test workflow’": { "main": [ [ { diff --git a/cypress/fixtures/Multiple_trigger_node_rerun.json b/cypress/fixtures/Multiple_trigger_node_rerun.json index f956be3742..c5b34aaa26 100644 --- a/cypress/fixtures/Multiple_trigger_node_rerun.json +++ b/cypress/fixtures/Multiple_trigger_node_rerun.json @@ -4,7 +4,7 @@ { "parameters": {}, "id": "5ae8991f-08a2-4b27-b61c-85e3b8a83693", - "name": "When clicking \"Test workflow\"", + "name": "When clicking ‘Test workflow’", "type": "n8n-nodes-base.manualTrigger", "typeVersion": 1, "position": [ @@ -78,14 +78,14 @@ } } ], - "When clicking \"Test workflow\"": [ + "When clicking ‘Test workflow’": [ { "json": {} } ] }, "connections": { - "When clicking \"Test workflow\"": { + "When clicking ‘Test workflow’": { "main": [ [ { diff --git a/cypress/fixtures/NDV-test-switch_reorder.json b/cypress/fixtures/NDV-test-switch_reorder.json index cf970434f3..8e06d9dc02 100644 --- a/cypress/fixtures/NDV-test-switch_reorder.json +++ b/cypress/fixtures/NDV-test-switch_reorder.json @@ -4,7 +4,7 @@ { "parameters": {}, "id": "b3f0815d-b733-413f-ab3f-74e48277bd3a", - "name": "When clicking \"Test workflow\"", + "name": "When clicking ‘Test workflow’", "type": "n8n-nodes-base.manualTrigger", "typeVersion": 1, "position": [ @@ -160,7 +160,7 @@ ] }, "connections": { - "When clicking \"Test workflow\"": { + "When clicking ‘Test workflow’": { "main": [ [ { diff --git a/cypress/fixtures/Node_IO_filter.json b/cypress/fixtures/Node_IO_filter.json index 61be5d58d8..885c76a2b9 100644 --- a/cypress/fixtures/Node_IO_filter.json +++ b/cypress/fixtures/Node_IO_filter.json @@ -4,7 +4,7 @@ { "parameters": {}, "id": "46770685-44d1-4aad-9107-1d790cf26b50", - "name": "When clicking \"Test workflow\"", + "name": "When clicking ‘Test workflow’", "type": "n8n-nodes-base.manualTrigger", "typeVersion": 1, "position": [ @@ -74,7 +74,7 @@ } ], "pinData": { - "When clicking \"Test workflow\"": [ + "When clicking ‘Test workflow’": [ { "json": { "id": "654cfa05fa51480dcb543b1a", @@ -599,7 +599,7 @@ ] }, "connections": { - "When clicking \"Test workflow\"": { + "When clicking ‘Test workflow’": { "main": [ [ { diff --git a/cypress/fixtures/Suggested_Templates.json b/cypress/fixtures/Suggested_Templates.json index 3f69c4b1a9..308806c6c6 100644 --- a/cypress/fixtures/Suggested_Templates.json +++ b/cypress/fixtures/Suggested_Templates.json @@ -42,7 +42,7 @@ { "parameters": {}, "id": "551313bb-1e01-4133-9956-e6f09968f2ce", - "name": "When clicking \"Test workflow\"", + "name": "When clicking ‘Test workflow’", "type": "n8n-nodes-base.manualTrigger", "typeVersion": 1, "position": [ @@ -92,7 +92,7 @@ } ], "connections": { - "When clicking \"Test workflow\"": { + "When clicking ‘Test workflow’": { "main": [ [ { @@ -191,7 +191,7 @@ { "parameters": {}, "id": "551313bb-1e01-4133-9956-e6f09968f2ce", - "name": "When clicking \"Test workflow\"", + "name": "When clicking ‘Test workflow’", "type": "n8n-nodes-base.manualTrigger", "typeVersion": 1, "position": [ @@ -241,7 +241,7 @@ } ], "connections": { - "When clicking \"Test workflow\"": { + "When clicking ‘Test workflow’": { "main": [ [ { @@ -374,7 +374,7 @@ { "parameters": {}, "id": "551313bb-1e01-4133-9956-e6f09968f2ce", - "name": "When clicking \"Test workflow\"", + "name": "When clicking ‘Test workflow’", "type": "n8n-nodes-base.manualTrigger", "typeVersion": 1, "position": [ @@ -424,7 +424,7 @@ } ], "connections": { - "When clicking \"Test workflow\"": { + "When clicking ‘Test workflow’": { "main": [ [ { @@ -524,7 +524,7 @@ { "parameters": {}, "id": "551313bb-1e01-4133-9956-e6f09968f2ce", - "name": "When clicking \"Test workflow\"", + "name": "When clicking ‘Test workflow’", "type": "n8n-nodes-base.manualTrigger", "typeVersion": 1, "position": [ @@ -574,7 +574,7 @@ } ], "connections": { - "When clicking \"Test workflow\"": { + "When clicking ‘Test workflow’": { "main": [ [ { diff --git a/cypress/fixtures/Test_Workflow_pairedItem_incomplete_manual_bug.json b/cypress/fixtures/Test_Workflow_pairedItem_incomplete_manual_bug.json index f740bc1df6..60875681ab 100644 --- a/cypress/fixtures/Test_Workflow_pairedItem_incomplete_manual_bug.json +++ b/cypress/fixtures/Test_Workflow_pairedItem_incomplete_manual_bug.json @@ -4,7 +4,7 @@ { "parameters": {}, "id": "f26332f3-c61a-4843-94bd-64a73ad161ff", - "name": "When clicking \"Test workflow\"", + "name": "When clicking ‘Test workflow’", "type": "n8n-nodes-base.manualTrigger", "typeVersion": 1, "position": [ @@ -105,7 +105,7 @@ ], "pinData": {}, "connections": { - "When clicking \"Test workflow\"": { + "When clicking ‘Test workflow’": { "main": [ [ { diff --git a/cypress/fixtures/Test_workflow-actions_import_nodes_empty_name.json b/cypress/fixtures/Test_workflow-actions_import_nodes_empty_name.json index e07e32119f..cb7b4dcf20 100644 --- a/cypress/fixtures/Test_workflow-actions_import_nodes_empty_name.json +++ b/cypress/fixtures/Test_workflow-actions_import_nodes_empty_name.json @@ -19,7 +19,7 @@ { "parameters": {}, "id": "449ab540-d9d7-480d-b131-05e9989a69cd", - "name": "When clicking \"Test workflow\"", + "name": "When clicking ‘Test workflow’", "type": "n8n-nodes-base.manualTrigger", "typeVersion": 1, "position": [ @@ -42,7 +42,7 @@ } ], "connections": { - "When clicking \"Test workflow\"": { + "When clicking ‘Test workflow’": { "main": [ [ { diff --git a/cypress/fixtures/Test_workflow_5.json b/cypress/fixtures/Test_workflow_5.json index 5771e197d9..f3bf74634b 100644 --- a/cypress/fixtures/Test_workflow_5.json +++ b/cypress/fixtures/Test_workflow_5.json @@ -40,7 +40,7 @@ { "parameters": {}, "id": "ef63cdc5-50bc-4525-9873-7e7f7589a60e", - "name": "When clicking \"Test workflow\"", + "name": "When clicking ‘Test workflow’", "type": "n8n-nodes-base.manualTrigger", "typeVersion": 1, "position": [ @@ -199,7 +199,7 @@ ] ] }, - "When clicking \"Test workflow\"": { + "When clicking ‘Test workflow’": { "main": [ [ { diff --git a/cypress/fixtures/Test_workflow_filter.json b/cypress/fixtures/Test_workflow_filter.json index e5aad93388..cf3ab886f9 100644 --- a/cypress/fixtures/Test_workflow_filter.json +++ b/cypress/fixtures/Test_workflow_filter.json @@ -4,7 +4,7 @@ { "parameters": {}, "id": "f332a7d1-31b4-4e78-b31e-9e8db945bf3f", - "name": "When clicking \"Test workflow\"", + "name": "When clicking ‘Test workflow’", "type": "n8n-nodes-base.manualTrigger", "typeVersion": 1, "position": [ @@ -99,7 +99,7 @@ ], "pinData": {}, "connections": { - "When clicking \"Test workflow\"": { + "When clicking ‘Test workflow’": { "main": [ [ { diff --git a/cypress/fixtures/Test_workflow_ndv_run_error.json b/cypress/fixtures/Test_workflow_ndv_run_error.json index 45a045851d..a42347dccf 100644 --- a/cypress/fixtures/Test_workflow_ndv_run_error.json +++ b/cypress/fixtures/Test_workflow_ndv_run_error.json @@ -30,7 +30,7 @@ { "parameters": {}, "id": "4f4c6527-d565-448a-96bd-8f5414caf8cc", - "name": "When clicking \"Test workflow\"", + "name": "When clicking ‘Test workflow’", "type": "n8n-nodes-base.manualTrigger", "typeVersion": 1, "position": [ @@ -136,7 +136,7 @@ ] ] }, - "When clicking \"Test workflow\"": { + "When clicking ‘Test workflow’": { "main": [ [ { diff --git a/cypress/fixtures/Test_workflow_ndv_version.json b/cypress/fixtures/Test_workflow_ndv_version.json index 7f3ba16f43..d682708eb8 100644 --- a/cypress/fixtures/Test_workflow_ndv_version.json +++ b/cypress/fixtures/Test_workflow_ndv_version.json @@ -3,7 +3,7 @@ "nodes": [ { "id": "2acca986-10a6-451e-b20a-86e95b50e627", - "name": "When clicking \"Test workflow\"", + "name": "When clicking ‘Test workflow’", "type": "n8n-nodes-base.manualTrigger", "typeVersion": 1, "position": [460, 460] diff --git a/cypress/fixtures/Test_workflow_partial_execution_with_missing_credentials.json b/cypress/fixtures/Test_workflow_partial_execution_with_missing_credentials.json index 2a9e75e11b..c02f01e59c 100644 --- a/cypress/fixtures/Test_workflow_partial_execution_with_missing_credentials.json +++ b/cypress/fixtures/Test_workflow_partial_execution_with_missing_credentials.json @@ -7,7 +7,7 @@ { "parameters": {}, "id": "09e4325e-ede1-40cf-a1ba-58612bbc7f1b", - "name": "When clicking \"Test workflow\"", + "name": "When clicking ‘Test workflow’", "type": "n8n-nodes-base.manualTrigger", "typeVersion": 1, "position": [ @@ -77,7 +77,7 @@ } ], "connections": { - "When clicking \"Test workflow\"": { + "When clicking ‘Test workflow’": { "main": [ [ { diff --git a/cypress/fixtures/Test_workflow_schema_test.json b/cypress/fixtures/Test_workflow_schema_test.json index 8c83c4f20e..0252fb893e 100644 --- a/cypress/fixtures/Test_workflow_schema_test.json +++ b/cypress/fixtures/Test_workflow_schema_test.json @@ -47,7 +47,7 @@ { "parameters": {}, "id": "58512a93-dabf-4584-817f-27c608c1bdd5", - "name": "When clicking \"Test workflow\"", + "name": "When clicking ‘Test workflow’", "type": "n8n-nodes-base.manualTrigger", "typeVersion": 1, "position": [ @@ -69,7 +69,7 @@ ] ] }, - "When clicking \"Test workflow\"": { + "When clicking ‘Test workflow’": { "main": [ [ { diff --git a/cypress/fixtures/Test_workflow_schema_test_pinned_data.json b/cypress/fixtures/Test_workflow_schema_test_pinned_data.json index 8bd5ef783d..73f6b62cff 100644 --- a/cypress/fixtures/Test_workflow_schema_test_pinned_data.json +++ b/cypress/fixtures/Test_workflow_schema_test_pinned_data.json @@ -47,7 +47,7 @@ { "parameters": {}, "id": "3dc7cf26-ff25-4437-b9fd-0e8b127ebec9", - "name": "When clicking \"Test workflow\"", + "name": "When clicking ‘Test workflow’", "type": "n8n-nodes-base.manualTrigger", "typeVersion": 1, "position": [ @@ -552,7 +552,7 @@ ] ] }, - "When clicking \"Test workflow\"": { + "When clicking ‘Test workflow’": { "main": [ [ { diff --git a/cypress/fixtures/Test_workflow_webhook_with_pin_data.json b/cypress/fixtures/Test_workflow_webhook_with_pin_data.json index fb632bcf36..503b723e5b 100644 --- a/cypress/fixtures/Test_workflow_webhook_with_pin_data.json +++ b/cypress/fixtures/Test_workflow_webhook_with_pin_data.json @@ -4,7 +4,7 @@ { "parameters": {}, "id": "0a60e507-7f34-41c0-a0f9-697d852033b6", - "name": "When clicking \"Test workflow\"", + "name": "When clicking ‘Test workflow’", "type": "n8n-nodes-base.manualTrigger", "typeVersion": 1, "position": [ @@ -93,7 +93,7 @@ ] }, "connections": { - "When clicking \"Test workflow\"": { + "When clicking ‘Test workflow’": { "main": [ [ { diff --git a/cypress/fixtures/Test_workflow_xml_output.json b/cypress/fixtures/Test_workflow_xml_output.json index 17449bc56d..871156fab2 100644 --- a/cypress/fixtures/Test_workflow_xml_output.json +++ b/cypress/fixtures/Test_workflow_xml_output.json @@ -6,7 +6,7 @@ { "parameters": {}, "id": "8108d313-8b03-4aa4-963d-cd1c0fe8f85c", - "name": "When clicking \"Test workflow\"", + "name": "When clicking ‘Test workflow’", "type": "n8n-nodes-base.manualTrigger", "typeVersion": 1, "position": [ @@ -37,7 +37,7 @@ } ], "connections": { - "When clicking \"Test workflow\"": { + "When clicking ‘Test workflow’": { "main": [ [ { diff --git a/cypress/fixtures/expression_with_paired_item_in_multi_input_node.json b/cypress/fixtures/expression_with_paired_item_in_multi_input_node.json index ffb7005f4f..d2b16c5656 100644 --- a/cypress/fixtures/expression_with_paired_item_in_multi_input_node.json +++ b/cypress/fixtures/expression_with_paired_item_in_multi_input_node.json @@ -6,7 +6,7 @@ { "parameters": {}, "id": "bcb6abdf-d34b-4ea7-a8ed-58155b708c43", - "name": "When clicking \"Test workflow\"", + "name": "When clicking ‘Test workflow’", "type": "n8n-nodes-base.manualTrigger", "typeVersion": 1, "position": [ @@ -90,7 +90,7 @@ } ], "connections": { - "When clicking \"Test workflow\"": { + "When clicking ‘Test workflow’": { "main": [ [ { diff --git a/cypress/fixtures/open_node_creator_for_connection.json b/cypress/fixtures/open_node_creator_for_connection.json index 78827d4083..3f693e538c 100644 --- a/cypress/fixtures/open_node_creator_for_connection.json +++ b/cypress/fixtures/open_node_creator_for_connection.json @@ -4,7 +4,7 @@ { "parameters": {}, "id": "25ff0c17-7064-4e14-aec6-45c71d63201b", - "name": "When clicking \"Test workflow\"", + "name": "When clicking ‘Test workflow’", "type": "n8n-nodes-base.manualTrigger", "typeVersion": 1, "position": [ @@ -107,4 +107,4 @@ }, "id": "L3tgfoW660UOSuX6", "tags": [] -} \ No newline at end of file +} diff --git a/cypress/fixtures/workflow-with-unknown-credentials.json b/cypress/fixtures/workflow-with-unknown-credentials.json index 142422227c..9a04cd87e5 100644 --- a/cypress/fixtures/workflow-with-unknown-credentials.json +++ b/cypress/fixtures/workflow-with-unknown-credentials.json @@ -27,7 +27,7 @@ { "parameters": {}, "id": "acdd1bdc-c642-4ea6-ad67-f4201b640cfa", - "name": "When clicking \"Test workflow\"", + "name": "When clicking ‘Test workflow’", "type": "n8n-nodes-base.manualTrigger", "typeVersion": 1, "position": [ @@ -37,7 +37,7 @@ } ], "connections": { - "When clicking \"Test workflow\"": { + "When clicking ‘Test workflow’": { "main": [ [ { diff --git a/cypress/fixtures/workflow-with-unknown-nodes.json b/cypress/fixtures/workflow-with-unknown-nodes.json index 5ea0189e50..3406e512d7 100644 --- a/cypress/fixtures/workflow-with-unknown-nodes.json +++ b/cypress/fixtures/workflow-with-unknown-nodes.json @@ -6,7 +6,7 @@ { "parameters": {}, "id": "40720511-19b6-4421-bdb0-3fb6efef4bc5", - "name": "When clicking \"Test workflow\"", + "name": "When clicking ‘Test workflow’", "type": "n8n-nodes-base.manualTrigger", "typeVersion": 1, "position": [ @@ -64,7 +64,7 @@ } ], "connections": { - "When clicking \"Test workflow\"": { + "When clicking ‘Test workflow’": { "main": [ [ { diff --git a/packages/@n8n/nodes-langchain/nodes/agents/Agent/Agent.node.ts b/packages/@n8n/nodes-langchain/nodes/agents/Agent/Agent.node.ts index 1d15d71840..acbdf28cf4 100644 --- a/packages/@n8n/nodes-langchain/nodes/agents/Agent/Agent.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/agents/Agent/Agent.node.ts @@ -247,7 +247,7 @@ export class Agent implements INodeType { alias: ['LangChain'], categories: ['AI'], subcategories: { - AI: ['Agents'], + AI: ['Agents', 'Root Nodes'], }, resources: { primaryDocumentation: [ diff --git a/packages/@n8n/nodes-langchain/nodes/agents/OpenAiAssistant/OpenAiAssistant.node.ts b/packages/@n8n/nodes-langchain/nodes/agents/OpenAiAssistant/OpenAiAssistant.node.ts index 449fcd41c4..5db0025932 100644 --- a/packages/@n8n/nodes-langchain/nodes/agents/OpenAiAssistant/OpenAiAssistant.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/agents/OpenAiAssistant/OpenAiAssistant.node.ts @@ -31,7 +31,7 @@ export class OpenAiAssistant implements INodeType { alias: ['LangChain'], categories: ['AI'], subcategories: { - AI: ['Agents'], + AI: ['Agents', 'Root Nodes'], }, resources: { primaryDocumentation: [ diff --git a/packages/@n8n/nodes-langchain/nodes/chains/ChainLLM/ChainLlm.node.ts b/packages/@n8n/nodes-langchain/nodes/chains/ChainLLM/ChainLlm.node.ts index b177755591..a03b2d9a49 100644 --- a/packages/@n8n/nodes-langchain/nodes/chains/ChainLLM/ChainLlm.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/chains/ChainLLM/ChainLlm.node.ts @@ -257,7 +257,7 @@ export class ChainLlm implements INodeType { alias: ['LangChain'], categories: ['AI'], subcategories: { - AI: ['Chains'], + AI: ['Chains', 'Root Nodes'], }, resources: { primaryDocumentation: [ diff --git a/packages/@n8n/nodes-langchain/nodes/chains/ChainRetrievalQA/ChainRetrievalQa.node.ts b/packages/@n8n/nodes-langchain/nodes/chains/ChainRetrievalQA/ChainRetrievalQa.node.ts index 8647db9b95..da3f4ccc0d 100644 --- a/packages/@n8n/nodes-langchain/nodes/chains/ChainRetrievalQA/ChainRetrievalQa.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/chains/ChainRetrievalQA/ChainRetrievalQa.node.ts @@ -30,7 +30,7 @@ export class ChainRetrievalQa implements INodeType { alias: ['LangChain'], categories: ['AI'], subcategories: { - AI: ['Chains'], + AI: ['Chains', 'Root Nodes'], }, resources: { primaryDocumentation: [ diff --git a/packages/@n8n/nodes-langchain/nodes/chains/ChainSummarization/ChainSummarization.node.ts b/packages/@n8n/nodes-langchain/nodes/chains/ChainSummarization/ChainSummarization.node.ts index 8cc64c6e4f..cd47eb6a15 100644 --- a/packages/@n8n/nodes-langchain/nodes/chains/ChainSummarization/ChainSummarization.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/chains/ChainSummarization/ChainSummarization.node.ts @@ -16,7 +16,7 @@ export class ChainSummarization extends VersionedNodeType { alias: ['LangChain'], categories: ['AI'], subcategories: { - AI: ['Chains'], + AI: ['Chains', 'Root Nodes'], }, resources: { primaryDocumentation: [ diff --git a/packages/@n8n/nodes-langchain/nodes/memory/MemoryManager/MemoryManager.node.ts b/packages/@n8n/nodes-langchain/nodes/memory/MemoryManager/MemoryManager.node.ts index ef20fb041f..83918616c1 100644 --- a/packages/@n8n/nodes-langchain/nodes/memory/MemoryManager/MemoryManager.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/memory/MemoryManager/MemoryManager.node.ts @@ -77,7 +77,7 @@ export class MemoryManager implements INodeType { codex: { categories: ['AI'], subcategories: { - AI: ['Miscellaneous'], + AI: ['Miscellaneous', 'Root Nodes'], }, resources: { primaryDocumentation: [ diff --git a/packages/@n8n/nodes-langchain/nodes/output_parser/OutputParserStructured/OutputParserStructured.node.ts b/packages/@n8n/nodes-langchain/nodes/output_parser/OutputParserStructured/OutputParserStructured.node.ts index 354ba8fbb0..dc40a47c4a 100644 --- a/packages/@n8n/nodes-langchain/nodes/output_parser/OutputParserStructured/OutputParserStructured.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/output_parser/OutputParserStructured/OutputParserStructured.node.ts @@ -146,6 +146,79 @@ export class OutputParserStructured implements INodeType { } }`, }, + { + displayName: 'Schema Type', + name: 'schemaType', + type: 'options', + noDataExpression: true, + options: [ + { + name: 'Generate From JSON Example', + value: 'fromJson', + description: 'Generate a schema from an example JSON object', + }, + { + name: 'Define Below', + value: 'manual', + description: 'Define the JSON schema manually', + }, + ], + default: 'fromJson', + description: 'How to specify the schema for the function', + displayOptions: { + show: { + '@version': [{ _cnd: { gte: 1.2 } }], + }, + }, + }, + { + displayName: 'JSON Example', + name: 'jsonSchemaExample', + type: 'json', + default: `{ + "state": "California", + "cities": ["Los Angeles", "San Francisco", "San Diego"] +}`, + noDataExpression: true, + typeOptions: { + rows: 10, + }, + displayOptions: { + show: { + schemaType: ['fromJson'], + }, + }, + description: 'Example JSON object to use to generate the schema', + }, + { + displayName: 'Input Schema', + name: 'inputSchema', + type: 'json', + default: `{ + "type": "object", + "properties": { + "state": { + "type": "string" + }, + "cities": { + "type": "array", + "items": { + "type": "string" + } + } + } +}`, + noDataExpression: true, + typeOptions: { + rows: 10, + }, + displayOptions: { + show: { + schemaType: ['manual'], + }, + }, + description: 'Schema to use for the function', + }, { displayName: 'JSON Schema', name: 'jsonSchema', diff --git a/packages/@n8n/nodes-langchain/nodes/trigger/ChatTrigger/ChatTrigger.node.ts b/packages/@n8n/nodes-langchain/nodes/trigger/ChatTrigger/ChatTrigger.node.ts index 386479e569..200fe9c5a0 100644 --- a/packages/@n8n/nodes-langchain/nodes/trigger/ChatTrigger/ChatTrigger.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/trigger/ChatTrigger/ChatTrigger.node.ts @@ -34,9 +34,6 @@ export class ChatTrigger implements INodeType { }, ], }, - subcategories: { - 'Core Nodes': ['Other Trigger Nodes'], - }, }, supportsCORS: true, maxNodes: 1, diff --git a/packages/@n8n/nodes-langchain/nodes/vendors/OpenAi/actions/versionDescription.ts b/packages/@n8n/nodes-langchain/nodes/vendors/OpenAi/actions/versionDescription.ts index 38aa1698a5..1143c6c097 100644 --- a/packages/@n8n/nodes-langchain/nodes/vendors/OpenAi/actions/versionDescription.ts +++ b/packages/@n8n/nodes-langchain/nodes/vendors/OpenAi/actions/versionDescription.ts @@ -79,7 +79,7 @@ export const versionDescription: INodeTypeDescription = { alias: ['LangChain', 'ChatGPT', 'DallE'], categories: ['AI'], subcategories: { - AI: ['Agents', 'Miscellaneous'], + AI: ['Agents', 'Miscellaneous', 'Root Nodes'], }, resources: { primaryDocumentation: [ diff --git a/packages/design-system/src/components/N8nNodeCreatorNode/NodeCreatorNode.vue b/packages/design-system/src/components/N8nNodeCreatorNode/NodeCreatorNode.vue index a6cb79897b..877b79277f 100644 --- a/packages/design-system/src/components/N8nNodeCreatorNode/NodeCreatorNode.vue +++ b/packages/design-system/src/components/N8nNodeCreatorNode/NodeCreatorNode.vue @@ -1,5 +1,6 @@ + + diff --git a/packages/editor-ui/src/components/Node/NodeCreator/ItemTypes/NodeItem.vue b/packages/editor-ui/src/components/Node/NodeCreator/ItemTypes/NodeItem.vue index 7db599d107..1a20ed2b9a 100644 --- a/packages/editor-ui/src/components/Node/NodeCreator/ItemTypes/NodeItem.vue +++ b/packages/editor-ui/src/components/Node/NodeCreator/ItemTypes/NodeItem.vue @@ -8,6 +8,7 @@ :show-action-arrow="showActionArrow" :is-trigger="isTrigger" :data-test-id="dataTestId" + :tag="nodeType.tag" @dragstart="onDragStart" @dragend="onDragEnd" > diff --git a/packages/editor-ui/src/components/Node/NodeCreator/Modes/NodesMode.vue b/packages/editor-ui/src/components/Node/NodeCreator/Modes/NodesMode.vue index 34f5b703bc..3adfd023d6 100644 --- a/packages/editor-ui/src/components/Node/NodeCreator/Modes/NodesMode.vue +++ b/packages/editor-ui/src/components/Node/NodeCreator/Modes/NodesMode.vue @@ -139,6 +139,13 @@ function onSelected(item: INodeCreateElement) { searchItems: mergedNodes, }); } + + if (item.type === 'link') { + window.open(item.properties.url, '_blank'); + telemetry.trackNodesPanel('nodeCreateList.onLinkSelected', { + link: item.properties.url, + }); + } } function subcategoriesMapper(item: INodeCreateElement) { @@ -195,13 +202,13 @@ function onKeySelect(activeItemId: string) { registerKeyHook('MainViewArrowRight', { keyboardKeys: ['ArrowRight', 'Enter'], - condition: (type) => ['subcategory', 'node', 'view'].includes(type), + condition: (type) => ['subcategory', 'node', 'link', 'view'].includes(type), handler: onKeySelect, }); registerKeyHook('MainViewArrowLeft', { keyboardKeys: ['ArrowLeft'], - condition: (type) => ['subcategory', 'node', 'view'].includes(type), + condition: (type) => ['subcategory', 'node', 'link', 'view'].includes(type), handler: arrowLeft, }); diff --git a/packages/editor-ui/src/components/Node/NodeCreator/Renderers/ItemsRenderer.vue b/packages/editor-ui/src/components/Node/NodeCreator/Renderers/ItemsRenderer.vue index d4b4bce771..c2327dcfbc 100644 --- a/packages/editor-ui/src/components/Node/NodeCreator/Renderers/ItemsRenderer.vue +++ b/packages/editor-ui/src/components/Node/NodeCreator/Renderers/ItemsRenderer.vue @@ -8,6 +8,7 @@ import SubcategoryItem from '../ItemTypes/SubcategoryItem.vue'; import LabelItem from '../ItemTypes/LabelItem.vue'; import ActionItem from '../ItemTypes/ActionItem.vue'; import ViewItem from '../ItemTypes/ViewItem.vue'; +import LinkItem from '../ItemTypes/LinkItem.vue'; import CategorizedItemsRenderer from './CategorizedItemsRenderer.vue'; export interface Props { @@ -147,6 +148,8 @@ watch( [$style.active]: activeItemId === item.uuid, [$style.iteratorItem]: true, [$style[item.type]]: true, + // Borderless is only applied to views + [$style.borderless]: item.type === 'view' && item.properties.borderless === true, }" data-test-id="item-iterator-item" :data-keyboard-nav-type="item.type !== 'label' ? item.type : undefined" @@ -175,6 +178,12 @@ watch( :view="item.properties" :class="$style.viewItem" /> + + @@ -223,12 +232,14 @@ watch( display: none; } } + .view { position: relative; &:last-child { margin-top: var(--spacing-s); padding-top: var(--spacing-xs); + &:after { content: ''; position: absolute; @@ -241,4 +252,34 @@ watch( } } } +.link { + position: relative; + + &:last-child { + margin-bottom: var(--spacing-s); + padding-bottom: var(--spacing-xs); + + &:after { + content: ''; + position: absolute; + left: var(--spacing-s); + right: var(--spacing-s); + top: 0; + margin: auto; + bottom: 0; + border-bottom: 1px solid var(--color-foreground-base); + } + } +} + +.borderless { + &:last-child { + margin-top: 0; + padding-top: 0; + + &:after { + content: none; + } + } +} diff --git a/packages/editor-ui/src/components/Node/NodeCreator/__tests__/NodesListPanel.test.ts b/packages/editor-ui/src/components/Node/NodeCreator/__tests__/NodesListPanel.test.ts index 3c36faa611..ac4b2757f4 100644 --- a/packages/editor-ui/src/components/Node/NodeCreator/__tests__/NodesListPanel.test.ts +++ b/packages/editor-ui/src/components/Node/NodeCreator/__tests__/NodesListPanel.test.ts @@ -76,7 +76,7 @@ describe('NodesListPanel', () => { await fireEvent.click(container.querySelector('.backButton')!); await nextTick(); - expect(screen.queryAllByTestId('item-iterator-item')).toHaveLength(7); + expect(screen.queryAllByTestId('item-iterator-item')).toHaveLength(8); }); it('should render regular nodes', async () => { @@ -136,7 +136,7 @@ describe('NodesListPanel', () => { await nextTick(); expect(screen.getByText('What happens next?')).toBeInTheDocument(); - expect(screen.queryAllByTestId('item-iterator-item')).toHaveLength(6); + expect(screen.queryAllByTestId('item-iterator-item')).toHaveLength(5); screen.getByText('Action in an app').click(); await nextTick(); diff --git a/packages/editor-ui/src/components/Node/NodeCreator/composables/useViewStacks.ts b/packages/editor-ui/src/components/Node/NodeCreator/composables/useViewStacks.ts index 3078cf3884..057d0dfe5c 100644 --- a/packages/editor-ui/src/components/Node/NodeCreator/composables/useViewStacks.ts +++ b/packages/editor-ui/src/components/Node/NodeCreator/composables/useViewStacks.ts @@ -1,19 +1,29 @@ -import type { INodeCreateElement, NodeFilterType, SimplifiedNodeType } from '@/Interface'; +import type { + INodeCreateElement, + NodeCreateElement, + NodeFilterType, + SimplifiedNodeType, +} from '@/Interface'; import { + AI_CATEGORY_ROOT_NODES, AI_CODE_NODE_TYPE, + AI_NODE_CREATOR_VIEW, AI_OTHERS_NODE_CREATOR_VIEW, + AI_SUBCATEGORY, DEFAULT_SUBCATEGORY, TRIGGER_NODE_CREATOR_VIEW, } from '@/constants'; import { defineStore } from 'pinia'; import { v4 as uuid } from 'uuid'; import { computed, nextTick, ref } from 'vue'; +import difference from 'lodash-es/difference'; import { useNodeCreatorStore } from '@/stores/nodeCreator.store'; import { flattenCreateElements, groupItemsInSections, + isAINode, searchNodes, sortNodeCreateElements, subcategorizeItems, @@ -27,6 +37,7 @@ import { useKeyboardNavigation } from './useKeyboardNavigation'; import { useNodeTypesStore } from '@/stores/nodeTypes.store'; import type { INodeInputFilter, NodeConnectionType } from 'n8n-workflow'; +import { useCanvasStore } from '@/stores/canvas.store'; interface ViewStack { uuid?: string; @@ -60,11 +71,12 @@ interface ViewStack { export const useViewStacks = defineStore('nodeCreatorViewStacks', () => { const nodeCreatorStore = useNodeCreatorStore(); const { getActiveItemIndex } = useKeyboardNavigation(); + const i18n = useI18n(); const viewStacks = ref([]); const activeStackItems = computed(() => { - const stack = viewStacks.value[viewStacks.value.length - 1]; + const stack = getLastActiveStack(); if (!stack?.baselineItems) { return stack.items ? extendItemsWithUUID(stack.items) : []; @@ -76,13 +88,24 @@ export const useViewStacks = defineStore('nodeCreatorViewStacks', () => { ? searchBaseItems.value : flattenCreateElements(stack.baselineItems ?? []); - return extendItemsWithUUID(searchNodes(stack.search || '', searchBase)); + const canvasHasAINodes = useCanvasStore().aiNodes.length > 0; + const filteredNodes = + isAiRootView(stack) || canvasHasAINodes ? searchBase : filterOutAiNodes(searchBase); + + const searchResults = extendItemsWithUUID(searchNodes(stack.search || '', filteredNodes)); + + const groupedNodes = groupIfAiNodes(searchResults, false) ?? searchResults; + // Set the active index to the second item if there's a section + // as the first item is collapsable + stack.activeIndex = groupedNodes.some((node) => node.type === 'section') ? 1 : 0; + + return groupedNodes; } - return extendItemsWithUUID(stack.baselineItems); + return extendItemsWithUUID(groupIfAiNodes(stack.baselineItems, true)); }); const activeViewStack = computed(() => { - const stack = viewStacks.value[viewStacks.value.length - 1]; + const stack = getLastActiveStack(); if (!stack) return {}; const flatBaselineItems = flattenCreateElements(stack.baselineItems ?? []); @@ -99,34 +122,148 @@ export const useViewStacks = defineStore('nodeCreatorViewStacks', () => { ); const searchBaseItems = computed(() => { - const stack = viewStacks.value[viewStacks.value.length - 1]; + const stack = getLastActiveStack(); if (!stack?.searchItems) return []; return stack.searchItems.map((item) => transformNodeType(item, stack.subcategory)); }); + function getLastActiveStack() { + return viewStacks.value[viewStacks.value.length - 1]; + } + // Generate a delta between the global search results(all nodes) and the stack search results const globalSearchItemsDiff = computed(() => { - const stack = viewStacks.value[viewStacks.value.length - 1]; + const stack = getLastActiveStack(); if (!stack?.search) return []; const allNodes = nodeCreatorStore.mergedNodes.map((item) => transformNodeType(item)); - const globalSearchResult = extendItemsWithUUID(searchNodes(stack.search || '', allNodes)); + // Apply filtering for AI nodes if the current view is not the AI root view + const filteredNodes = isAiRootView(stack) ? allNodes : filterOutAiNodes(allNodes); - return globalSearchResult.filter((item) => { - return !activeStackItems.value.find((activeItem) => activeItem.key === item.key); + let globalSearchResult: INodeCreateElement[] = extendItemsWithUUID( + searchNodes(stack.search || '', filteredNodes), + ); + if (isAiRootView(stack)) { + globalSearchResult = groupIfAiNodes(globalSearchResult); + } + + const filteredItems = globalSearchResult.filter((item) => { + return !activeStackItems.value.find((activeItem) => { + if (activeItem.type === 'section') { + const matchingSectionItem = activeItem.children.some( + (sectionItem) => sectionItem.key === item.key, + ); + return matchingSectionItem; + } + + return activeItem.key === item.key; + }); }); + + // Filter out empty sections if all of their children are filtered out + const filteredSections = filteredItems.filter((item) => { + if (item.type === 'section') { + const hasVisibleChildren = item.children.some((child) => + activeStackItems.value.some((filteredItem) => filteredItem.key === child.key), + ); + + return hasVisibleChildren; + } + + return true; + }); + + return filteredSections; }); const itemsBySubcategory = computed(() => subcategorizeItems(nodeCreatorStore.mergedNodes)); + function isAiRootView(stack: ViewStack) { + return stack.rootView === AI_NODE_CREATOR_VIEW; + } + + function groupIfAiNodes(items: INodeCreateElement[], sortAlphabetically = true) { + const aiNodes = items.filter((node): node is NodeCreateElement => isAINode(node)); + + if (aiNodes.length > 0) { + const sectionsMap = new Map(); + aiNodes.forEach((node) => { + const section = node.properties.codex?.subcategories?.[AI_SUBCATEGORY]?.[0]; + + if (section) { + const currentItems = sectionsMap.get(section)?.items ?? []; + const isSubnodesSection = + !node.properties.codex?.subcategories?.[AI_SUBCATEGORY].includes( + AI_CATEGORY_ROOT_NODES, + ); + + sectionsMap.set(section, { + key: section, + title: isSubnodesSection + ? `${section} (${i18n.baseText('nodeCreator.subnodes')})` + : section, + items: [...currentItems, node.key], + }); + } + }); + + const nonAiNodes = difference(items, aiNodes); + const nonAiTriggerNodes = nonAiNodes.filter( + (item) => item.type === 'node' && useNodeTypesStore().isTriggerNode(item.properties.name), + ); + + const nonAiRegularNodes = difference(nonAiNodes, nonAiTriggerNodes); + + if (nonAiNodes.length > 0) { + let sectionKey = ''; + if (nonAiRegularNodes.length && nonAiTriggerNodes.length) { + sectionKey = i18n.baseText('nodeCreator.actionsCategory.regularAndTriggers'); + } else { + sectionKey = nonAiRegularNodes.length + ? i18n.baseText('nodeCreator.actionsCategory.regularNodes') + : i18n.baseText('nodeCreator.actionsCategory.triggerNodes'); + } + + const nodesKeys = nonAiNodes.map((node) => node.key); + + sectionsMap.set(sectionKey, { + key: sectionKey, + title: sectionKey, + items: [...nodesKeys], + }); + } + // Convert sectionsMap to array of sections + const sections = Array.from(sectionsMap.values()); + + return groupItemsInSections(items, sections, sortAlphabetically); + } + + return items; + } + + function filterOutAiNodes(items: INodeCreateElement[]) { + const filteredSearchBase = items.filter((item) => { + if (item.type === 'node') { + const isAICategory = item.properties.codex?.categories?.includes(AI_SUBCATEGORY) === true; + + if (!isAICategory) return true; + + const isRootNodeSubcategory = + item.properties.codex?.subcategories?.[AI_SUBCATEGORY]?.includes(AI_CATEGORY_ROOT_NODES); + + return isRootNodeSubcategory; + } + return true; + }); + return filteredSearchBase; + } + async function gotoCompatibleConnectionView( connectionType: NodeConnectionType, isOutput?: boolean, filter?: INodeInputFilter, ) { - const i18n = useI18n(); - let nodesByConnectionType: { [key: string]: string[] }; let relatedAIView: { properties: NodeViewItem['properties'] } | undefined; @@ -185,7 +322,7 @@ export const useViewStacks = defineStore('nodeCreatorViewStacks', () => { } function setStackBaselineItems() { - const stack = viewStacks.value[viewStacks.value.length - 1]; + const stack = getLastActiveStack(); if (!stack || !activeViewStack.value.uuid) return; let stackItems = stack?.items ?? []; @@ -258,7 +395,7 @@ export const useViewStacks = defineStore('nodeCreatorViewStacks', () => { } function updateCurrentViewStack(stack: Partial) { - const currentStack = viewStacks.value[viewStacks.value.length - 1]; + const currentStack = getLastActiveStack(); const matchedIndex = viewStacks.value.findIndex((s) => s.uuid === currentStack.uuid); if (!currentStack) return; diff --git a/packages/editor-ui/src/components/Node/NodeCreator/utils.ts b/packages/editor-ui/src/components/Node/NodeCreator/utils.ts index dbde77d1f2..ac16811929 100644 --- a/packages/editor-ui/src/components/Node/NodeCreator/utils.ts +++ b/packages/editor-ui/src/components/Node/NodeCreator/utils.ts @@ -6,12 +6,18 @@ import type { INodeCreateElement, SectionCreateElement, } from '@/Interface'; -import { AI_SUBCATEGORY, CORE_NODES_CATEGORY, DEFAULT_SUBCATEGORY } from '@/constants'; +import { + AI_CATEGORY_AGENTS, + AI_SUBCATEGORY, + CORE_NODES_CATEGORY, + DEFAULT_SUBCATEGORY, +} from '@/constants'; import { v4 as uuidv4 } from 'uuid'; import { sublimeSearch } from '@/utils/sortUtils'; -import { i18n } from '@/plugins/i18n'; import type { NodeViewItemSection } from './viewsData'; +import { i18n } from '@/plugins/i18n'; +import { sortBy } from 'lodash-es'; export function transformNodeType( node: SimplifiedNodeType, @@ -70,6 +76,7 @@ export function sortNodeCreateElements(nodes: INodeCreateElement[]) { export function searchNodes(searchFilter: string, items: INodeCreateElement[]) { // In order to support the old search we need to remove the 'trigger' part const trimmedFilter = searchFilter.toLowerCase().replace('trigger', '').trimEnd(); + const result = ( sublimeSearch(trimmedFilter, items, [ { key: 'properties.displayName', weight: 1.3 }, @@ -83,38 +90,72 @@ export function searchNodes(searchFilter: string, items: INodeCreateElement[]) { export function flattenCreateElements(items: INodeCreateElement[]): INodeCreateElement[] { return items.map((item) => (item.type === 'section' ? item.children : item)).flat(); } +export function isAINode(node: INodeCreateElement) { + const isNode = node.type === 'node'; + if (!isNode) return false; + if (node.properties.codex?.categories?.includes(AI_SUBCATEGORY)) { + const isAgentSubcategory = + node.properties.codex?.subcategories?.[AI_SUBCATEGORY]?.includes(AI_CATEGORY_AGENTS); + + return !isAgentSubcategory; + } + + return false; +} export function groupItemsInSections( items: INodeCreateElement[], sections: string[] | NodeViewItemSection[], + sortAlphabetically = true, ): INodeCreateElement[] { const filteredSections = sections.filter( (section): section is NodeViewItemSection => typeof section === 'object', ); - const itemsBySection = items.reduce((acc: Record, item) => { - const section = filteredSections.find((s) => s.items.includes(item.key)); - const key = section?.key ?? 'other'; - acc[key] = [...(acc[key] ?? []), item]; - return acc; - }, {}); + const itemsBySection = (items2: INodeCreateElement[]) => + items2.reduce((acc: Record, item) => { + const section = filteredSections.find((s) => s.items.includes(item.key)); - const result: SectionCreateElement[] = filteredSections - .map( + const key = section?.key ?? 'other'; + if (key) { + acc[key] = [...(acc[key] ?? []), item]; + } + return acc; + }, {}); + + const mapNewSections = ( + newSections: NodeViewItemSection[], + children: Record, + ) => + newSections.map( (section): SectionCreateElement => ({ type: 'section', key: section.key, title: section.title, - children: sortNodeCreateElements(itemsBySection[section.key] ?? []), + children: sortAlphabetically + ? sortNodeCreateElements(children[section.key] ?? []) + : children[section.key] ?? [], }), - ) + ); + + const nonAINodes = items.filter((item) => !isAINode(item)); + const AINodes = items.filter((item) => isAINode(item)); + + const nonAINodesBySection = itemsBySection(nonAINodes); + const nonAINodesSections = mapNewSections(filteredSections, nonAINodesBySection); + + const AINodesBySection = itemsBySection(AINodes); + + const AINodesSections = mapNewSections(sortBy(filteredSections, ['title']), AINodesBySection); + + const result = [...nonAINodesSections, ...AINodesSections] .concat({ type: 'section', key: 'other', title: i18n.baseText('nodeCreator.sectionNames.other'), - children: sortNodeCreateElements(itemsBySection.other ?? []), + children: sortNodeCreateElements(nonAINodesBySection.other ?? []), }) - .filter((section) => section.children.length > 0); + .filter((section) => section.type !== 'section' || section.children.length > 0); if (result.length <= 1) { return items; diff --git a/packages/editor-ui/src/components/Node/NodeCreator/viewsData.ts b/packages/editor-ui/src/components/Node/NodeCreator/viewsData.ts index da309ec5dd..d97a7329b8 100644 --- a/packages/editor-ui/src/components/Node/NodeCreator/viewsData.ts +++ b/packages/editor-ui/src/components/Node/NodeCreator/viewsData.ts @@ -5,10 +5,10 @@ import { EXECUTE_WORKFLOW_TRIGGER_NODE_TYPE, FORM_TRIGGER_NODE_TYPE, MANUAL_TRIGGER_NODE_TYPE, + MANUAL_CHAT_TRIGGER_NODE_TYPE, SCHEDULE_TRIGGER_NODE_TYPE, REGULAR_NODE_CREATOR_VIEW, TRANSFORM_DATA_SUBCATEGORY, - FILES_SUBCATEGORY, FLOWS_CONTROL_SUBCATEGORY, TRIGGER_NODE_CREATOR_VIEW, EMAIL_IMAP_NODE_TYPE, @@ -52,6 +52,8 @@ import { EMAIL_SEND_NODE_TYPE, EDIT_IMAGE_NODE_TYPE, COMPRESSION_NODE_TYPE, + AI_CODE_TOOL_LANGCHAIN_NODE_TYPE, + AI_WORKFLOW_TOOL_LANGCHAIN_NODE_TYPE, } from '@/constants'; import { useI18n } from '@/composables/useI18n'; import { useNodeTypesStore } from '@/stores/nodeTypes.store'; @@ -76,13 +78,17 @@ export interface NodeViewItem { iconProps?: { color?: string; }; + url?: string; connectionType?: NodeConnectionType; panelClass?: string; group?: string[]; sections?: NodeViewItemSection[]; description?: string; displayName?: string; - tag?: string; + tag?: { + type: string; + text: string; + }; forceIncludeNodes?: string[]; iconData?: { type: string; @@ -141,12 +147,24 @@ export function AIView(_nodes: SimplifiedNodeType[]): NodeView { value: AI_NODE_CREATOR_VIEW, title: i18n.baseText('nodeCreator.aiPanel.aiNodes'), subtitle: i18n.baseText('nodeCreator.aiPanel.selectAiNode'), - info: i18n.baseText('nodeCreator.aiPanel.infoBox', { - interpolate: { link: templatesStore.getWebsiteCategoryURL('ai') }, - }), items: [ - ...chainNodes, + { + key: 'ai_templates_root', + type: 'link', + properties: { + title: i18n.baseText('nodeCreator.aiPanel.linkItem.title'), + icon: 'box-open', + description: i18n.baseText('nodeCreator.aiPanel.linkItem.description'), + name: 'ai_templates_root', + url: templatesStore.getWebsiteCategoryURL(undefined, 'AdvancedAI'), + tag: { + type: 'info', + text: i18n.baseText('nodeCreator.triggerHelperPanel.manualTriggerTag'), + }, + }, + }, ...agentNodes, + ...chainNodes, { key: AI_OTHERS_NODE_CREATOR_VIEW, type: 'view', @@ -159,6 +177,7 @@ export function AIView(_nodes: SimplifiedNodeType[]): NodeView { ], }; } + export function AINodesView(_nodes: SimplifiedNodeType[]): NodeView { const i18n = useI18n(); @@ -232,12 +251,20 @@ export function AINodesView(_nodes: SimplifiedNodeType[]): NodeView { }, }, { - key: AI_CATEGORY_TOOLS, type: 'subcategory', + key: AI_CATEGORY_TOOLS, + category: CORE_NODES_CATEGORY, properties: { title: AI_CATEGORY_TOOLS, icon: 'tools', ...getAISubcategoryProperties(NodeConnectionType.AiTool), + sections: [ + { + key: 'popular', + title: i18n.baseText('nodeCreator.sectionNames.popular'), + items: [AI_WORKFLOW_TOOL_LANGCHAIN_NODE_TYPE, AI_CODE_TOOL_LANGCHAIN_NODE_TYPE], + }, + ], }, }, { @@ -278,6 +305,18 @@ export function TriggerView() { title: i18n.baseText('nodeCreator.triggerHelperPanel.selectATrigger'), subtitle: i18n.baseText('nodeCreator.triggerHelperPanel.selectATriggerDescription'), items: [ + { + key: MANUAL_TRIGGER_NODE_TYPE, + type: 'node', + category: [CORE_NODES_CATEGORY], + properties: { + group: [], + name: MANUAL_TRIGGER_NODE_TYPE, + displayName: i18n.baseText('nodeCreator.triggerHelperPanel.manualTriggerDisplayName'), + description: i18n.baseText('nodeCreator.triggerHelperPanel.manualTriggerDescription'), + icon: 'fa:mouse-pointer', + }, + }, { key: DEFAULT_SUBCATEGORY, type: 'subcategory', @@ -331,18 +370,6 @@ export function TriggerView() { }, }, }, - { - key: MANUAL_TRIGGER_NODE_TYPE, - type: 'node', - category: [CORE_NODES_CATEGORY], - properties: { - group: [], - name: MANUAL_TRIGGER_NODE_TYPE, - displayName: i18n.baseText('nodeCreator.triggerHelperPanel.manualTriggerDisplayName'), - description: i18n.baseText('nodeCreator.triggerHelperPanel.manualTriggerDescription'), - icon: 'fa:mouse-pointer', - }, - }, { key: EXECUTE_WORKFLOW_TRIGGER_NODE_TYPE, type: 'node', @@ -355,6 +382,18 @@ export function TriggerView() { icon: 'fa:sign-out-alt', }, }, + { + key: MANUAL_CHAT_TRIGGER_NODE_TYPE, + type: 'node', + category: [CORE_NODES_CATEGORY], + properties: { + group: [], + name: MANUAL_CHAT_TRIGGER_NODE_TYPE, + displayName: i18n.baseText('nodeCreator.triggerHelperPanel.manualChatTriggerDisplayName'), + description: i18n.baseText('nodeCreator.triggerHelperPanel.manualChatTriggerDescription'), + icon: 'fa:comments', + }, + }, { type: 'subcategory', key: OTHER_TRIGGER_NODES_SUBCATEGORY, @@ -447,22 +486,6 @@ export function RegularView(nodes: SimplifiedNodeType[]) { ], }, }, - { - type: 'subcategory', - key: FILES_SUBCATEGORY, - category: CORE_NODES_CATEGORY, - properties: { - title: FILES_SUBCATEGORY, - icon: 'file-alt', - sections: [ - { - key: 'popular', - title: i18n.baseText('nodeCreator.sectionNames.popular'), - items: [CONVERT_TO_FILE_NODE_TYPE, EXTRACT_FROM_FILE_NODE_TYPE], - }, - ], - }, - }, { type: 'subcategory', key: HELPERS_SUBCATEGORY, @@ -491,9 +514,13 @@ export function RegularView(nodes: SimplifiedNodeType[]) { title: i18n.baseText('nodeCreator.aiPanel.langchainAiNodes'), icon: 'robot', description: i18n.baseText('nodeCreator.aiPanel.nodesForAi'), - tag: i18n.baseText('nodeCreator.aiPanel.newTag'), + tag: { + type: 'success', + text: i18n.baseText('nodeCreator.aiPanel.newTag'), + }, + borderless: true, }, - }); + } as NodeViewItem); view.items.push({ key: TRIGGER_NODE_CREATOR_VIEW, diff --git a/packages/editor-ui/src/constants.ts b/packages/editor-ui/src/constants.ts index b433c3edac..f4e1c8bb28 100644 --- a/packages/editor-ui/src/constants.ts +++ b/packages/editor-ui/src/constants.ts @@ -264,8 +264,10 @@ export const AI_CATEGORY_RETRIEVERS = 'Retrievers'; export const AI_CATEGORY_EMBEDDING = 'Embeddings'; export const AI_CATEGORY_DOCUMENT_LOADERS = 'Document Loaders'; export const AI_CATEGORY_TEXT_SPLITTERS = 'Text Splitters'; +export const AI_CATEGORY_ROOT_NODES = 'Root Nodes'; export const AI_UNCATEGORIZED_CATEGORY = 'Miscellaneous'; - +export const AI_CODE_TOOL_LANGCHAIN_NODE_TYPE = '@n8n/n8n-nodes-langchain.toolCode'; +export const AI_WORKFLOW_TOOL_LANGCHAIN_NODE_TYPE = '@n8n/n8n-nodes-langchain.toolWorkflow'; export const REQUEST_NODE_FORM_URL = 'https://n8n-community.typeform.com/to/K1fBVTZ3'; // Node Connection Types @@ -674,10 +676,17 @@ export const AI_ASSISTANT_EXPERIMENT = { variant: 'variant', }; +export const CANVAS_AUTO_ADD_MANUAL_TRIGGER_EXPERIMENT = { + name: '20_canvas_auto_add_manual_trigger', + control: 'control', + variant: 'variant', +}; + export const EXPERIMENTS_TO_TRACK = [ ASK_AI_EXPERIMENT.name, TEMPLATE_CREDENTIAL_SETUP_EXPERIMENT, AI_ASSISTANT_EXPERIMENT.name, + CANVAS_AUTO_ADD_MANUAL_TRIGGER_EXPERIMENT.name, ]; export const MFA_AUTHENTICATION_REQUIRED_ERROR_CODE = 998; diff --git a/packages/editor-ui/src/plugins/i18n/locales/en.json b/packages/editor-ui/src/plugins/i18n/locales/en.json index 04042d988c..951a80c0a4 100644 --- a/packages/editor-ui/src/plugins/i18n/locales/en.json +++ b/packages/editor-ui/src/plugins/i18n/locales/en.json @@ -928,9 +928,9 @@ "ndv.output.of": " of ", "ndv.output.pageSize": "Page Size", "ndv.output.run": "Run", - "ndv.output.runNodeHint": "Test this node to output data", + "ndv.output.runNodeHint": "Execute this node to view data", "ndv.output.runNodeHintSubNode": "Output will appear here once the parent node is run", - "ndv.output.insertTestData": "insert test data", + "ndv.output.insertTestData": "set mock data", "ndv.output.staleDataWarning.regular": "Node parameters have changed.
Test node again to refresh output.", "ndv.output.staleDataWarning.pinData": "Node parameter changes will not affect pinned output data.", "ndv.output.tooMuchData.message": "The node contains {size} MB of data. Displaying it may cause problems.
If you do decide to display it, avoid the JSON view.", @@ -986,6 +986,9 @@ "nodeCreator.actionsCategory.onNewEvent": "On new {event} event", "nodeCreator.actionsCategory.onEvent": "On {event}", "nodeCreator.actionsCategory.triggers": "Triggers", + "nodeCreator.actionsCategory.triggerNodes": "Trigger Nodes", + "nodeCreator.actionsCategory.regularNodes": "Regular Nodes", + "nodeCreator.actionsCategory.regularAndTriggers": "Regular & Trigger Nodes", "nodeCreator.actionsCategory.searchActions": "Search {node} Actions...", "nodeCreator.actionsCategory.noMatchingActions": "No matching Actions. Reset search", "nodeCreator.actionsCategory.noMatchingTriggers": "No matching Triggers. Reset search", @@ -996,6 +999,7 @@ "nodeCreator.actionsTooltip.actionsPerformStep": "Actions perform a step once your workflow has already started. Learn more", "nodeCreator.actionsCallout.noTriggerItems": "No {nodeName} Triggers available. Users often combine the following Triggers with {nodeName} Actions.", "nodeCreator.categoryNames.otherCategories": "Results in other categories", + "nodeCreator.subnodes": "sub-nodes", "nodeCreator.noResults.dontWorryYouCanProbablyDoItWithThe": "Don’t worry, you can probably do it with the", "nodeCreator.noResults.httpRequest": "HTTP Request", "nodeCreator.noResults.node": "node", @@ -1007,10 +1011,10 @@ "nodeCreator.searchBar.searchNodes": "Search nodes...", "nodeCreator.subcategoryDescriptions.appTriggerNodes": "Runs the flow when something happens in an app like Telegram, Notion or Airtable", "nodeCreator.subcategoryDescriptions.appRegularNodes": "Do something in an app or service like Google Sheets, Telegram or Notion", - "nodeCreator.subcategoryDescriptions.dataTransformation": "Manipulate data, run JavaScript code, etc.", + "nodeCreator.subcategoryDescriptions.dataTransformation": "Manipulate, filter or convert data", "nodeCreator.subcategoryDescriptions.files": "CSV, XLS, XML, text, images, etc.", - "nodeCreator.subcategoryDescriptions.flow": "IF, Switch, Wait, Compare and Merge data, etc.", - "nodeCreator.subcategoryDescriptions.helpers": "Code, HTTP Requests (API Calls), Webhook, and other helpers", + "nodeCreator.subcategoryDescriptions.flow": "Branch, merge or loop the flow, etc.", + "nodeCreator.subcategoryDescriptions.helpers": "Run code, make HTTP requests, set webhooks, etc.", "nodeCreator.subcategoryDescriptions.otherTriggerNodes": "Runs the flow on workflow errors, file changes, etc.", "nodeCreator.subcategoryDescriptions.agents": "Autonomous entities that interact and make decisions.", "nodeCreator.subcategoryDescriptions.chains": "Structured assemblies for specific tasks.", @@ -1054,11 +1058,14 @@ "nodeCreator.triggerHelperPanel.scheduleTriggerDisplayName": "On a schedule", "nodeCreator.triggerHelperPanel.scheduleTriggerDescription": "Runs the flow every day, hour, or custom interval", "nodeCreator.triggerHelperPanel.webhookTriggerDisplayName": "On webhook call", - "nodeCreator.triggerHelperPanel.webhookTriggerDescription": "Runs the flow when another app sends a webhook", + "nodeCreator.triggerHelperPanel.webhookTriggerDescription": "Runs the flow on receiving an HTTP request", "nodeCreator.triggerHelperPanel.formTriggerDisplayName": "On form submission", "nodeCreator.triggerHelperPanel.formTriggerDescription": "Runs the flow when an n8n generated webform is submitted", - "nodeCreator.triggerHelperPanel.manualTriggerDisplayName": "Manually", - "nodeCreator.triggerHelperPanel.manualTriggerDescription": "Runs the flow on clicking a button in n8n", + "nodeCreator.triggerHelperPanel.manualTriggerDisplayName": "Trigger manually", + "nodeCreator.triggerHelperPanel.manualTriggerDescription": "Runs the flow on clicking a button in n8n. Good for getting started quickly", + "nodeCreator.triggerHelperPanel.manualChatTriggerDisplayName": "On chat message", + "nodeCreator.triggerHelperPanel.manualChatTriggerDescription": "Runs the flow when a user sends a chat message. For use with AI nodes", + "nodeCreator.triggerHelperPanel.manualTriggerTag": "Recommended", "nodeCreator.triggerHelperPanel.whatHappensNext": "What happens next?", "nodeCreator.triggerHelperPanel.selectATrigger": "What triggers this workflow?", "nodeCreator.triggerHelperPanel.selectATriggerDescription": "A trigger is a step that starts your workflow", @@ -1072,7 +1079,8 @@ "nodeCreator.aiPanel.newTag": "New", "nodeCreator.aiPanel.langchainAiNodes": "Advanced AI", "nodeCreator.aiPanel.title": "When should this workflow run?", - "nodeCreator.aiPanel.infoBox": "Check out our templates for workflow examples and inspiration.", + "nodeCreator.aiPanel.linkItem.description": "See what's possible and get started 5x faster", + "nodeCreator.aiPanel.linkItem.title": "AI Templates", "nodeCreator.aiPanel.scheduleTriggerDisplayName": "On a schedule", "nodeCreator.aiPanel.scheduleTriggerDescription": "Runs the flow every day, hour, or custom interval", "nodeCreator.aiPanel.webhookTriggerDisplayName": "On webhook call", diff --git a/packages/editor-ui/src/stores/canvas.store.ts b/packages/editor-ui/src/stores/canvas.store.ts index b5f57fc42c..93e0975e57 100644 --- a/packages/editor-ui/src/stores/canvas.store.ts +++ b/packages/editor-ui/src/stores/canvas.store.ts @@ -15,7 +15,7 @@ import { scaleReset, scaleSmaller, } from '@/utils/canvasUtils'; -import { START_NODE_TYPE } from '@/constants'; +import { MANUAL_TRIGGER_NODE_TYPE, START_NODE_TYPE } from '@/constants'; import type { BeforeStartEventParams, BrowserJsPlumbInstance, @@ -61,6 +61,9 @@ export const useCanvasStore = defineStore('canvas', () => { (node) => node.type === START_NODE_TYPE || nodeTypesStore.isTriggerNode(node.type), ), ); + const aiNodes = computed(() => + nodes.value.filter((node) => node.type.includes('langchain')), + ); const isDemo = ref(false); const nodeViewScale = ref(1); const canvasAddButtonPosition = ref([1, 1]); @@ -91,6 +94,23 @@ export const useCanvasStore = defineStore('canvas', () => { }; }; + const getAutoAddManualTriggerNode = (): INodeUi | null => { + const manualTriggerNode = nodeTypesStore.getNodeType(MANUAL_TRIGGER_NODE_TYPE); + + if (!manualTriggerNode) { + console.error('Could not find the manual trigger node'); + return null; + } + return { + id: uuid(), + name: manualTriggerNode.defaults.name?.toString() ?? manualTriggerNode.displayName, + type: MANUAL_TRIGGER_NODE_TYPE, + parameters: {}, + position: canvasAddButtonPosition.value, + typeVersion: 1, + }; + }; + const getNodesWithPlaceholderNode = (): INodeUi[] => triggerNodes.value.length > 0 ? nodes.value : [getPlaceholderTriggerNodeUI(), ...nodes.value]; @@ -298,6 +318,7 @@ export const useCanvasStore = defineStore('canvas', () => { newNodeInsertPosition, jsPlumbInstance, isLoading: loadingService.isLoading, + aiNodes, startLoading: loadingService.startLoading, setLoadingText: loadingService.setLoadingText, stopLoading: loadingService.stopLoading, @@ -311,5 +332,6 @@ export const useCanvasStore = defineStore('canvas', () => { zoomToFit, wheelScroll, initInstance, + getAutoAddManualTriggerNode, }; }); diff --git a/packages/editor-ui/src/stores/templates.store.ts b/packages/editor-ui/src/stores/templates.store.ts index 43b219d261..c67a603aea 100644 --- a/packages/editor-ui/src/stores/templates.store.ts +++ b/packages/editor-ui/src/stores/templates.store.ts @@ -121,7 +121,7 @@ export const useTemplatesStore = defineStore(STORES.TEMPLATES, { * Constructs URLSearchParams object based on the default parameters for the template repository * and provided additional parameters */ - websiteTemplateRepositoryParameters() { + websiteTemplateRepositoryParameters(roleOverride?: string) { const rootStore = useRootStore(); const userStore = useUsersStore(); const workflowsStore = useWorkflowsStore(); @@ -133,6 +133,7 @@ export const useTemplatesStore = defineStore(STORES.TEMPLATES, { }; const userRole: string | undefined = userStore.currentUserCloudInfo?.role ?? userStore.currentUser?.personalizationAnswers?.role; + if (userRole) { defaultParameters.utm_user_role = userRole; } @@ -156,10 +157,15 @@ export const useTemplatesStore = defineStore(STORES.TEMPLATES, { * Construct the URL for the template category page on the website for a given category id */ getWebsiteCategoryURL() { - return (id: string) => { - return `${TEMPLATES_URLS.BASE_WEBSITE_URL}/?${this.websiteTemplateRepositoryParameters({ - categories: id, - }).toString()}`; + return (id?: string, roleOverride?: string) => { + const payload: Record = {}; + if (id) { + payload.categories = id; + } + if (roleOverride) { + payload.utm_user_role = roleOverride; + } + return `${TEMPLATES_URLS.BASE_WEBSITE_URL}/?${this.websiteTemplateRepositoryParameters(payload).toString()}`; }; }, }, diff --git a/packages/editor-ui/src/utils/__tests__/pairedItemUtils.test.ts b/packages/editor-ui/src/utils/__tests__/pairedItemUtils.test.ts index 12074d941f..6d41511c70 100644 --- a/packages/editor-ui/src/utils/__tests__/pairedItemUtils.test.ts +++ b/packages/editor-ui/src/utils/__tests__/pairedItemUtils.test.ts @@ -11,7 +11,7 @@ const MOCK_EXECUTION: Partial = { startData: {}, resultData: { runData: { - 'When clicking "Test workflow"': [ + 'When clicking ‘Test workflow’': [ { startTime: 1706027170005, executionTime: 0, @@ -24,7 +24,7 @@ const MOCK_EXECUTION: Partial = { { startTime: 1706027170005, executionTime: 1, - source: [{ previousNode: 'When clicking "Test workflow"' }], + source: [{ previousNode: 'When clicking ‘Test workflow’' }], executionStatus: 'success', data: { main: [ @@ -258,54 +258,54 @@ describe('pairedItemUtils', () => { const actual = getPairedItemsMapping(MOCK_EXECUTION); const expected = { DebugHelper_r0_o0_i0: new Set([ - 'When clicking "Test workflow"_r0_o0_i0', + 'When clicking ‘Test workflow’_r0_o0_i0', 'If_r0_o0_i0', 'Edit Fields_r1_o0_i0', 'Edit Fields1_r1_o0_i0', ]), DebugHelper_r0_o0_i1: new Set([ - 'When clicking "Test workflow"_r0_o0_i0', + 'When clicking ‘Test workflow’_r0_o0_i0', 'If_r0_o1_i0', 'Edit Fields_r0_o0_i0', 'Edit Fields1_r0_o0_i0', ]), 'Edit Fields1_r0_o0_i0': new Set([ - 'When clicking "Test workflow"_r0_o0_i0', + 'When clicking ‘Test workflow’_r0_o0_i0', 'DebugHelper_r0_o0_i1', 'If_r0_o1_i0', 'Edit Fields_r0_o0_i0', ]), 'Edit Fields1_r1_o0_i0': new Set([ - 'When clicking "Test workflow"_r0_o0_i0', + 'When clicking ‘Test workflow’_r0_o0_i0', 'DebugHelper_r0_o0_i0', 'If_r0_o0_i0', 'Edit Fields_r1_o0_i0', ]), 'Edit Fields_r0_o0_i0': new Set([ - 'When clicking "Test workflow"_r0_o0_i0', + 'When clicking ‘Test workflow’_r0_o0_i0', 'DebugHelper_r0_o0_i1', 'If_r0_o1_i0', 'Edit Fields1_r0_o0_i0', ]), 'Edit Fields_r1_o0_i0': new Set([ - 'When clicking "Test workflow"_r0_o0_i0', + 'When clicking ‘Test workflow’_r0_o0_i0', 'DebugHelper_r0_o0_i0', 'If_r0_o0_i0', 'Edit Fields1_r1_o0_i0', ]), If_r0_o0_i0: new Set([ - 'When clicking "Test workflow"_r0_o0_i0', + 'When clicking ‘Test workflow’_r0_o0_i0', 'DebugHelper_r0_o0_i0', 'Edit Fields_r1_o0_i0', 'Edit Fields1_r1_o0_i0', ]), If_r0_o1_i0: new Set([ - 'When clicking "Test workflow"_r0_o0_i0', + 'When clicking ‘Test workflow’_r0_o0_i0', 'DebugHelper_r0_o0_i1', 'Edit Fields_r0_o0_i0', 'Edit Fields1_r0_o0_i0', ]), - 'When clicking "Test workflow"_r0_o0_i0': new Set([ + 'When clicking ‘Test workflow’_r0_o0_i0': new Set([ 'DebugHelper_r0_o0_i0', 'DebugHelper_r0_o0_i1', 'If_r0_o0_i0', diff --git a/packages/editor-ui/src/views/NodeView.vue b/packages/editor-ui/src/views/NodeView.vue index 0b8d4a8e91..518610f734 100644 --- a/packages/editor-ui/src/views/NodeView.vue +++ b/packages/editor-ui/src/views/NodeView.vue @@ -250,6 +250,7 @@ import { UPDATE_WEBHOOK_ID_NODE_TYPES, TIME, AI_ASSISTANT_LOCAL_STORAGE_KEY, + CANVAS_AUTO_ADD_MANUAL_TRIGGER_EXPERIMENT, } from '@/constants'; import useGlobalLinkActions from '@/composables/useGlobalLinkActions'; @@ -395,6 +396,7 @@ import type { ProjectSharingData } from '@/features/projects/projects.types'; import { useAIStore } from '@/stores/ai.store'; import { useStorage } from '@/composables/useStorage'; import { isJSPlumbEndpointElement } from '@/utils/typeGuards'; +import { usePostHog } from '@/stores/posthog.store'; import { ProjectTypes } from '@/features/projects/projects.utils'; interface AddNodeOptions { @@ -944,6 +946,17 @@ export default defineComponent({ action: this.openSelectiveNodeCreator, }); + this.registerCustomAction({ + key: 'showNodeCreator', + action: () => { + this.ndvStore.activeNodeName = null; + + void this.$nextTick(() => { + this.showTriggerCreator(NODE_CREATOR_OPEN_SOURCES.TAB); + }); + }, + }); + this.readOnlyEnvRouteCheck(); this.canvasStore.isDemo = this.isDemo; }, @@ -1177,12 +1190,6 @@ export default defineComponent({ ? this.$locale.baseText('nodeView.addOrEnableTriggerNode') : this.$locale.baseText('nodeView.addATriggerNodeFirst'); - this.registerCustomAction({ - key: 'showNodeCreator', - action: () => - this.showTriggerCreator(NODE_CREATOR_OPEN_SOURCES.NO_TRIGGER_EXECUTION_TOOLTIP), - }); - const notice = this.showMessage({ type: 'info', title: this.$locale.baseText('nodeView.cantExecuteNoTrigger'), @@ -1257,9 +1264,15 @@ export default defineComponent({ }, showTriggerCreator(source: NodeCreatorOpenSource) { if (this.createNodeActive) return; + + this.ndvStore.activeNodeName = null; this.nodeCreatorStore.setSelectedView(TRIGGER_NODE_CREATOR_VIEW); this.nodeCreatorStore.setShowScrim(true); - this.onToggleNodeCreator({ source, createNodeActive: true }); + this.onToggleNodeCreator({ + source, + createNodeActive: true, + nodeCreatorView: TRIGGER_NODE_CREATOR_VIEW, + }); }, async openExecution(executionId: string) { this.canvasStore.startLoading(); @@ -3659,6 +3672,7 @@ export default defineComponent({ this.workflowsStore.workflow.scopes = scopes; }, async newWorkflow(): Promise { + const { getVariant } = usePostHog(); this.canvasStore.startLoading(); this.resetWorkspace(); this.workflowData = await this.workflowsStore.getNewWorkflowData( @@ -3670,15 +3684,24 @@ export default defineComponent({ this.uiStore.stateIsDirty = false; this.canvasStore.setZoomLevel(1, [0, 0]); - await this.tryToAddWelcomeSticky(); + this.canvasStore.zoomToFit(); this.uiStore.nodeViewInitialized = true; this.historyStore.reset(); this.executionsStore.activeExecution = null; this.makeNewWorkflowShareable(); this.canvasStore.stopLoading(); - }, - async tryToAddWelcomeSticky(): Promise { - this.canvasStore.zoomToFit(); + + // Pre-populate the canvas with the manual trigger node if the experiment is enabled and the user is in the variant group + if ( + getVariant(CANVAS_AUTO_ADD_MANUAL_TRIGGER_EXPERIMENT.name) === + CANVAS_AUTO_ADD_MANUAL_TRIGGER_EXPERIMENT.variant + ) { + const manualTriggerNode = this.canvasStore.getAutoAddManualTriggerNode(); + if (manualTriggerNode) { + await this.addNodes([manualTriggerNode]); + this.uiStore.lastSelectedNode = manualTriggerNode.name; + } + } }, async initView(): Promise { if (this.$route.params.action === 'workflowSave') { @@ -5375,4 +5398,3 @@ export default defineComponent({ ); } -, IRun, IPushDataExecutionFinished diff --git a/packages/nodes-base/nodes/Files/ConvertToFile/test/toText.workflow.json b/packages/nodes-base/nodes/Files/ConvertToFile/test/toText.workflow.json index 3a855ccf9a..ea9b0fc6a9 100644 --- a/packages/nodes-base/nodes/Files/ConvertToFile/test/toText.workflow.json +++ b/packages/nodes-base/nodes/Files/ConvertToFile/test/toText.workflow.json @@ -4,7 +4,7 @@ { "parameters": {}, "id": "59f5ae0f-52f7-4bc8-b325-29d2b0d810f8", - "name": "When clicking \"Test workflow\"", + "name": "When clicking ‘Test workflow’", "type": "n8n-nodes-base.manualTrigger", "typeVersion": 1, "position": [ @@ -217,7 +217,7 @@ ] }, "connections": { - "When clicking \"Test workflow\"": { + "When clicking ‘Test workflow’": { "main": [ [ { diff --git a/packages/nodes-base/nodes/GraphQL/test/workflow.json b/packages/nodes-base/nodes/GraphQL/test/workflow.json index f78c655da4..690171afc5 100644 --- a/packages/nodes-base/nodes/GraphQL/test/workflow.json +++ b/packages/nodes-base/nodes/GraphQL/test/workflow.json @@ -7,7 +7,7 @@ { "parameters": {}, "id": "fb826323-2e48-4f11-bb0e-e12de32e22ee", - "name": "When clicking \"Test workflow\"", + "name": "When clicking ‘Test workflow’", "type": "n8n-nodes-base.manualTrigger", "typeVersion": 1, "position": [180, 160] @@ -26,7 +26,7 @@ } ], "connections": { - "When clicking \"Test workflow\"": { + "When clicking ‘Test workflow’": { "main": [ [ { diff --git a/packages/nodes-base/nodes/Jwt/test/jwt.workflow.json b/packages/nodes-base/nodes/Jwt/test/jwt.workflow.json index 12cba9fefc..b87ef4e7e1 100644 --- a/packages/nodes-base/nodes/Jwt/test/jwt.workflow.json +++ b/packages/nodes-base/nodes/Jwt/test/jwt.workflow.json @@ -4,7 +4,7 @@ { "parameters": {}, "id": "fcc3e9dc-90c9-4b26-9b44-e661e0ebf658", - "name": "When clicking \"Test workflow\"", + "name": "When clicking ‘Test workflow’", "type": "n8n-nodes-base.manualTrigger", "typeVersion": 1, "position": [ @@ -305,7 +305,7 @@ ] }, "connections": { - "When clicking \"Test workflow\"": { + "When clicking ‘Test workflow’": { "main": [ [ { @@ -409,4 +409,4 @@ }, "id": "H0sZEXDuE7VIP5vz", "tags": [] -} \ No newline at end of file +} diff --git a/packages/nodes-base/nodes/ManualTrigger/ManualTrigger.node.ts b/packages/nodes-base/nodes/ManualTrigger/ManualTrigger.node.ts index 4072f6f69f..29f1dfd74f 100644 --- a/packages/nodes-base/nodes/ManualTrigger/ManualTrigger.node.ts +++ b/packages/nodes-base/nodes/ManualTrigger/ManualTrigger.node.ts @@ -16,7 +16,7 @@ export class ManualTrigger implements INodeType { eventTriggerDescription: '', maxNodes: 1, defaults: { - name: 'When clicking "Test workflow"', + name: 'When clicking ‘Test workflow’', color: '#909298', }, @@ -25,7 +25,7 @@ export class ManualTrigger implements INodeType { properties: [ { displayName: - 'This node is where a manual workflow execution starts. To make one, go back to the canvas and click test workflow’', + 'This node is where the workflow execution starts (when you click the ‘test’ button on the canvas).

Explore other ways to trigger your workflow (e.g on a schedule, or a webhook)', name: 'notice', type: 'notice', default: '', diff --git a/packages/nodes-base/nodes/Merge/test/node/workflow.keep_non_matches.json b/packages/nodes-base/nodes/Merge/test/node/workflow.keep_non_matches.json index 8c2f85552a..617e26b7b2 100644 --- a/packages/nodes-base/nodes/Merge/test/node/workflow.keep_non_matches.json +++ b/packages/nodes-base/nodes/Merge/test/node/workflow.keep_non_matches.json @@ -4,7 +4,7 @@ { "parameters": {}, "id": "94003e55-6c4e-492f-802a-49f4fb5b5f4b", - "name": "When clicking \"Test Workflow\"", + "name": "When clicking ‘Test workflow’", "type": "n8n-nodes-base.manualTrigger", "typeVersion": 1, "position": [ @@ -485,7 +485,7 @@ ] }, "connections": { - "When clicking \"Test Workflow\"": { + "When clicking ‘Test workflow’": { "main": [ [ { diff --git a/packages/nodes-base/nodes/Switch/V3/test/switch.regex.workflow.json b/packages/nodes-base/nodes/Switch/V3/test/switch.regex.workflow.json index 1876c5e233..45269dcfc7 100644 --- a/packages/nodes-base/nodes/Switch/V3/test/switch.regex.workflow.json +++ b/packages/nodes-base/nodes/Switch/V3/test/switch.regex.workflow.json @@ -4,7 +4,7 @@ { "parameters": {}, "id": "1301e15e-7a64-44bf-bc4b-d60e7b8c629a", - "name": "When clicking \"Test workflow\"", + "name": "When clicking ‘Test workflow’", "type": "n8n-nodes-base.manualTrigger", "typeVersion": 1, "position": [ @@ -273,7 +273,7 @@ ] }, "connections": { - "When clicking \"Test workflow\"": { + "When clicking ‘Test workflow’": { "main": [ [ { diff --git a/packages/workflow/test/TelemetryHelpers.test.ts b/packages/workflow/test/TelemetryHelpers.test.ts index 11f741e79a..01f6c252ec 100644 --- a/packages/workflow/test/TelemetryHelpers.test.ts +++ b/packages/workflow/test/TelemetryHelpers.test.ts @@ -667,7 +667,7 @@ describe('generateNodesGraph', () => { { parameters: {}, id: 'fe69383c-e418-4f98-9c0e-924deafa7f93', - name: 'When clicking "Test workflow"', + name: 'When clicking ‘Test workflow’', type: 'n8n-nodes-base.manualTrigger', typeVersion: 1, position: [540, 220], @@ -692,7 +692,7 @@ describe('generateNodesGraph', () => { }, ], connections: { - 'When clicking "Test workflow"': { + 'When clicking ‘Test workflow’': { main: [ [ { @@ -758,7 +758,7 @@ describe('generateNodesGraph', () => { is_pinned: false, }, nameIndices: { - 'When clicking "Test workflow"': '0', + 'When clicking ‘Test workflow’': '0', Chain: '1', Model: '2', }, diff --git a/packages/workflow/test/fixtures/WorkflowDataProxy/errors_run.json b/packages/workflow/test/fixtures/WorkflowDataProxy/errors_run.json index 6ee89f092b..28864f7c4a 100644 --- a/packages/workflow/test/fixtures/WorkflowDataProxy/errors_run.json +++ b/packages/workflow/test/fixtures/WorkflowDataProxy/errors_run.json @@ -3,7 +3,7 @@ "startData": {}, "resultData": { "runData": { - "When clicking \"Test workflow\"": [ + "When clicking ‘Test workflow’": [ { "startTime": 1707471743600, "executionTime": 1, @@ -29,7 +29,7 @@ "executionTime": 1, "source": [ { - "previousNode": "When clicking \"Test workflow\"" + "previousNode": "When clicking ‘Test workflow’" } ], "executionStatus": "success", @@ -956,7 +956,7 @@ "source": { "main": [ { - "previousNode": "When clicking \"Test workflow\"" + "previousNode": "When clicking ‘Test workflow’" } ] } @@ -999,7 +999,7 @@ "source": { "main": [ { - "previousNode": "When clicking \"Test workflow\"" + "previousNode": "When clicking ‘Test workflow’" } ] } diff --git a/packages/workflow/test/fixtures/WorkflowDataProxy/errors_workflow.json b/packages/workflow/test/fixtures/WorkflowDataProxy/errors_workflow.json index fd93f3374c..088946a843 100644 --- a/packages/workflow/test/fixtures/WorkflowDataProxy/errors_workflow.json +++ b/packages/workflow/test/fixtures/WorkflowDataProxy/errors_workflow.json @@ -4,7 +4,7 @@ { "parameters": {}, "id": "b5122d27-4bb5-4100-a69b-03b1dcac76c7", - "name": "When clicking \"Test workflow\"", + "name": "When clicking ‘Test workflow’", "type": "n8n-nodes-base.manualTrigger", "typeVersion": 1, "position": [740, 1680] @@ -561,7 +561,7 @@ ] }, "connections": { - "When clicking \"Test workflow\"": { + "When clicking ‘Test workflow’": { "main": [ [ {