From 3c9a1d2da3aa7614ce1beec07654a8b2423f99bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milorad=20FIlipovi=C4=87?= Date: Thu, 4 Apr 2024 09:59:02 +0200 Subject: [PATCH 1/8] fix(editor): Prevent saving workflow while another save is in progress (#9048) --- cypress/e2e/7-workflow-actions.cy.ts | 17 +++++++++++++++++ .../components/MainHeader/WorkflowDetails.vue | 7 ++++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/cypress/e2e/7-workflow-actions.cy.ts b/cypress/e2e/7-workflow-actions.cy.ts index 0f6705bf349ef..2cf451b5ad8bd 100644 --- a/cypress/e2e/7-workflow-actions.cy.ts +++ b/cypress/e2e/7-workflow-actions.cy.ts @@ -108,6 +108,23 @@ describe('Workflow Actions', () => { cy.wait('@saveWorkflow'); cy.wrap(null).then(() => expect(interceptCalledCount).to.eq(1)); }); + + it('should not save workflow twice when save is in progress', () => { + // This happens when users click save button from workflow name input + // In this case blur on the input saves the workflow and then click on the button saves it again + WorkflowPage.actions.visit(); + WorkflowPage.getters.workflowNameInput().invoke('val').then((oldName) => { + WorkflowPage.getters.workflowNameInputContainer().click(); + WorkflowPage.getters.workflowNameInput().type('{selectall}'); + WorkflowPage.getters.workflowNameInput().type('Test'); + WorkflowPage.getters.saveButton().click(); + WorkflowPage.getters.workflowNameInput().should('have.value', 'Test'); + cy.visit(WorkflowPages.url); + // There should be no workflow with the old name (duplicate save) + WorkflowPages.getters.workflowCards().contains(String(oldName)).should('not.exist'); + }); + }); + it('should copy nodes', () => { WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME); diff --git a/packages/editor-ui/src/components/MainHeader/WorkflowDetails.vue b/packages/editor-ui/src/components/MainHeader/WorkflowDetails.vue index 8658b685e30b9..e8836c651fd61 100644 --- a/packages/editor-ui/src/components/MainHeader/WorkflowDetails.vue +++ b/packages/editor-ui/src/components/MainHeader/WorkflowDetails.vue @@ -388,6 +388,10 @@ export default defineComponent({ }, methods: { async onSaveButtonClick() { + // If the workflow is saving, do not allow another save + if (this.isWorkflowSaving) { + return; + } let currentId = undefined; if (this.currentWorkflowId !== PLACEHOLDER_EMPTY_WORKFLOW_ID) { currentId = this.currentWorkflowId; @@ -497,11 +501,12 @@ export default defineComponent({ cb(true); return; } - + this.uiStore.addActiveAction('workflowSaving'); const saved = await this.workflowHelpers.saveCurrentWorkflow({ name }); if (saved) { this.isNameEditEnabled = false; } + this.uiStore.removeActiveAction('workflowSaving'); cb(saved); }, async handleFileImport(): Promise { From 71c54cba52f5de26bd9c086390313c211ad0e574 Mon Sep 17 00:00:00 2001 From: Tomi Turtiainen <10324676+tomi@users.noreply.github.com> Date: Thu, 4 Apr 2024 11:13:37 +0300 Subject: [PATCH 2/8] fix: Fix missing input panel in node details view (#9043) --- .../1338-ADO-ndv-missing-input-panel.cy.ts | 25 + cypress/fixtures/Test_ado_1338.json | 632 ++++++++++++++++++ .../src/composables/useNodeHelpers.ts | 23 +- 3 files changed, 665 insertions(+), 15 deletions(-) create mode 100644 cypress/e2e/1338-ADO-ndv-missing-input-panel.cy.ts create mode 100644 cypress/fixtures/Test_ado_1338.json diff --git a/cypress/e2e/1338-ADO-ndv-missing-input-panel.cy.ts b/cypress/e2e/1338-ADO-ndv-missing-input-panel.cy.ts new file mode 100644 index 0000000000000..046d4d809d1bd --- /dev/null +++ b/cypress/e2e/1338-ADO-ndv-missing-input-panel.cy.ts @@ -0,0 +1,25 @@ +import { v4 as uuid } from 'uuid'; +import { NDV, WorkflowPage as WorkflowPageClass } from '../pages'; + +const workflowPage = new WorkflowPageClass(); +const ndv = new NDV(); + +describe('ADO-1338-ndv-missing-input-panel', () => { + beforeEach(() => { + workflowPage.actions.visit(); + }); + + it('should show the input and output panels when node is missing input and output data', () => { + cy.createFixtureWorkflow('Test_ado_1338.json', uuid()); + + // Execute the workflow + workflowPage.getters.zoomToFitButton().click(); + workflowPage.getters.executeWorkflowButton().click(); + // Check success toast (works because Cypress waits enough for the element to show after the http request node has finished) + workflowPage.getters.successToast().should('be.visible'); + + workflowPage.actions.openNode('Discourse1'); + ndv.getters.inputPanel().should('be.visible'); + ndv.getters.outputPanel().should('be.visible'); + }); +}); diff --git a/cypress/fixtures/Test_ado_1338.json b/cypress/fixtures/Test_ado_1338.json new file mode 100644 index 0000000000000..0609ae6e55f75 --- /dev/null +++ b/cypress/fixtures/Test_ado_1338.json @@ -0,0 +1,632 @@ +{ + "meta": { + "instanceId": "2be09fdcb9594c0827fd4cee80f7e590c93297d9217685f34c2250fe3144ef0c" + }, + "nodes": [ + { + "parameters": {}, + "id": "6dace68e-0727-472d-a212-00863acb64d6", + "name": "When clicking \"Execute Workflow\"", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [ + -340, + 660 + ] + }, + { + "parameters": { + "resource": "user", + "operation": "getAll", + "flag": "new", + "returnAll": true, + "options": {} + }, + "id": "2465a943-0d2c-480d-a98a-a67e92151367", + "name": "Discourse", + "type": "n8n-nodes-base.discourse", + "typeVersion": 1, + "position": [ + -120, + 660 + ] + }, + { + "parameters": { + "conditions": { + "dateTime": [ + { + "value1": "={{ $json.user.created_at }}", + "operation": "before", + "value2": "={{ $today.minus(6,\"day\") }}" + } + ], + "number": [ + { + "value1": "={{ $json.user.accepted_answers }}", + "operation": "larger", + "value2": 1 + }, + { + "value1": "={{ $json.user.post_count }}", + "operation": "larger", + "value2": 4 + } + ] + } + }, + "id": "ce1b80bb-08db-42cf-b7d9-56df74044f5c", + "name": "Filter", + "type": "n8n-nodes-base.filter", + "typeVersion": 1, + "position": [ + 600, + 640 + ] + }, + { + "parameters": { + "resource": "user", + "operation": "get", + "username": "={{ $json.username }}" + }, + "id": "ad3c141b-7aee-449b-8254-f21815a3d124", + "name": "Discourse1", + "type": "n8n-nodes-base.discourse", + "typeVersion": 1, + "position": [ + 340, + 840 + ] + }, + { + "parameters": { + "batchSize": 5, + "options": {} + }, + "id": "97fa87d0-ba76-4156-aa40-6bccd4775cdc", + "name": "Loop Over Items", + "type": "n8n-nodes-base.splitInBatches", + "typeVersion": 3, + "position": [ + 100, + 660 + ], + "disabled": true + }, + { + "parameters": { + "amount": 4, + "unit": "seconds" + }, + "id": "4f7f4b5d-2e02-4479-a4ee-9818f5b3e6de", + "name": "Wait", + "type": "n8n-nodes-base.wait", + "typeVersion": 1, + "position": [ + 580, + 840 + ], + "webhookId": "6bbd5e21-6022-475d-ace1-2aeb73e899d2" + }, + { + "parameters": {}, + "id": "a6cfc3b9-0d7a-4d4e-99c4-eba5085947d0", + "name": "No Operation, do nothing", + "type": "n8n-nodes-base.noOp", + "typeVersion": 1, + "position": [ + 340, + 640 + ] + }, + { + "parameters": { + "content": "### filtering\n- Forum account older than 6 days\n- 2+ replies marked as answer\n- 5+ posts" + }, + "id": "580c80dc-cf95-413c-9465-29c9dc66ef6e", + "name": "Sticky Note", + "type": "n8n-nodes-base.stickyNote", + "typeVersion": 1, + "position": [ + 640, + 420 + ] + } + ], + "connections": { + "When clicking \"Execute Workflow\"": { + "main": [ + [ + { + "node": "Discourse", + "type": "main", + "index": 0 + } + ] + ] + }, + "Discourse": { + "main": [ + [ + { + "node": "Loop Over Items", + "type": "main", + "index": 0 + } + ] + ] + }, + "Discourse1": { + "main": [ + [ + { + "node": "Wait", + "type": "main", + "index": 0 + } + ] + ] + }, + "Loop Over Items": { + "main": [ + [ + { + "node": "No Operation, do nothing", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Discourse1", + "type": "main", + "index": 0 + } + ] + ] + }, + "Wait": { + "main": [ + [ + { + "node": "Loop Over Items", + "type": "main", + "index": 0 + } + ] + ] + }, + "No Operation, do nothing": { + "main": [ + [ + { + "node": "Filter", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "pinData": { + "Discourse": [ + { + "id": 1, + "user": "name" + } + ], + "Wait": [ + { + "user_badges": [], + "user": { + "id": 1, + "username": "User", + "name": "User", + "avatar_template": "/user_avatar/community.n8n.io/user/{size}/2.png", + "last_posted_at": "2023-11-02T16:16:05.615Z", + "last_seen_at": "2023-11-02T16:15:42.734Z", + "created_at": "2023-11-01T15:16:53.268Z", + "ignored": false, + "muted": false, + "can_ignore_user": true, + "can_mute_user": true, + "can_send_private_messages": true, + "can_send_private_message_to_user": true, + "trust_level": 0, + "moderator": false, + "admin": false, + "title": null, + "badge_count": 0, + "user_fields": { + "1": null + }, + "custom_fields": {}, + "time_read": 121, + "recent_time_read": 121, + "primary_group_id": null, + "primary_group_name": null, + "flair_group_id": null, + "flair_name": null, + "flair_url": null, + "flair_bg_color": null, + "flair_color": null, + "featured_topic": null, + "pending_posts_count": 0, + "staged": false, + "can_edit": true, + "can_edit_username": true, + "can_edit_email": true, + "can_edit_name": true, + "uploaded_avatar_id": 31486, + "has_title_badges": false, + "pending_count": 0, + "profile_view_count": 7, + "second_factor_enabled": false, + "can_upload_profile_header": true, + "can_upload_user_card_background": true, + "post_count": 1, + "can_be_deleted": true, + "can_delete_all_posts": true, + "locale": "en", + "muted_category_ids": [], + "regular_category_ids": [], + "watched_tags": [], + "watching_first_post_tags": [], + "tracked_tags": [], + "muted_tags": [], + "tracked_category_ids": [], + "watched_category_ids": [], + "watched_first_post_category_ids": [], + "system_avatar_upload_id": null, + "system_avatar_template": "/letter_avatar_proxy/v4/letter/c/3be4f8/{size}.png", + "custom_avatar_upload_id": 31486, + "custom_avatar_template": "/user_avatar/community.n8n.io/user/{size}/2.png", + "muted_usernames": [], + "ignored_usernames": [], + "allowed_pm_usernames": [], + "mailing_list_posts_per_day": 100, + "can_change_bio": true, + "can_change_location": true, + "can_change_website": true, + "can_change_tracking_preferences": true, + "user_api_keys": null, + "user_auth_tokens": [], + "user_notification_schedule": { + "enabled": false, + "day_0_start_time": 480, + "day_0_end_time": 1020, + "day_1_start_time": 480, + "day_1_end_time": 1020, + "day_2_start_time": 480, + "day_2_end_time": 1020, + "day_3_start_time": 480, + "day_3_end_time": 1020, + "day_4_start_time": 480, + "day_4_end_time": 1020, + "day_5_start_time": 480, + "day_5_end_time": 1020, + "day_6_start_time": 480, + "day_6_end_time": 1020 + }, + "use_logo_small_as_avatar": false, + "reminders_frequency": [ + { + "name": "discourse_assign.reminders_frequency.never", + "value": 0 + }, + { + "name": "discourse_assign.reminders_frequency.daily", + "value": 1440 + }, + { + "name": "discourse_assign.reminders_frequency.weekly", + "value": 10080 + }, + { + "name": "discourse_assign.reminders_frequency.monthly", + "value": 43200 + }, + { + "name": "discourse_assign.reminders_frequency.quarterly", + "value": 129600 + } + ], + "assign_icon": "user-plus", + "assign_path": "/u/User/activity/assigned", + "accepted_answers": 0, + "featured_user_badge_ids": [], + "invited_by": null, + "groups": [ + { + "id": 10, + "automatic": true, + "name": "trust_level_0", + "display_name": "trust_level_0", + "user_count": 9295, + "mentionable_level": 0, + "messageable_level": 0, + "visibility_level": 1, + "primary_group": false, + "title": null, + "grant_trust_level": null, + "incoming_email": null, + "has_messages": false, + "flair_url": null, + "flair_bg_color": null, + "flair_color": null, + "bio_raw": null, + "bio_cooked": null, + "bio_excerpt": null, + "public_admission": false, + "public_exit": false, + "allow_membership_requests": false, + "full_name": null, + "default_notification_level": 3, + "membership_request_template": null, + "members_visibility_level": 0, + "can_see_members": true, + "can_admin_group": true, + "publish_read_state": false + } + ], + "group_users": [ + { + "group_id": 10, + "user_id": 1, + "notification_level": 3 + } + ], + "user_option": { + "user_id": 1, + "mailing_list_mode": false, + "mailing_list_mode_frequency": 1, + "email_digests": false, + "email_level": 1, + "email_messages_level": 0, + "external_links_in_new_tab": true, + "color_scheme_id": null, + "dark_scheme_id": null, + "dynamic_favicon": true, + "enable_quoting": true, + "enable_defer": false, + "digest_after_minutes": 0, + "automatically_unpin_topics": true, + "auto_track_topics_after_msecs": 300000, + "notification_level_when_replying": 2, + "new_topic_duration_minutes": 2880, + "email_previous_replies": 2, + "email_in_reply_to": false, + "like_notification_frequency": 1, + "include_tl0_in_digests": false, + "theme_ids": [ + 7 + ], + "theme_key_seq": 0, + "allow_private_messages": true, + "enable_allowed_pm_users": false, + "homepage_id": null, + "hide_profile_and_presence": false, + "text_size": "normal", + "text_size_seq": 0, + "title_count_mode": "notifications", + "bookmark_auto_delete_preference": 3, + "timezone": "Europe/Berlin", + "skip_new_user_tips": false, + "default_calendar": "none_selected", + "oldest_search_log_date": null, + "seen_popups": [ + 1, + 3 + ] + } + } + }], + "No Operation, do nothing": [ + { + "user_badges": [], + "user": { + "id": 1, + "username": "User", + "name": "User", + "avatar_template": "/user_avatar/community.n8n.io/user/{size}/2.png", + "last_posted_at": "2023-11-02T16:16:05.615Z", + "last_seen_at": "2023-11-02T16:15:42.734Z", + "created_at": "2023-11-01T15:16:53.268Z", + "ignored": false, + "muted": false, + "can_ignore_user": true, + "can_mute_user": true, + "can_send_private_messages": true, + "can_send_private_message_to_user": true, + "trust_level": 0, + "moderator": false, + "admin": false, + "title": null, + "badge_count": 0, + "user_fields": { + "1": null + }, + "custom_fields": {}, + "time_read": 121, + "recent_time_read": 121, + "primary_group_id": null, + "primary_group_name": null, + "flair_group_id": null, + "flair_name": null, + "flair_url": null, + "flair_bg_color": null, + "flair_color": null, + "featured_topic": null, + "pending_posts_count": 0, + "staged": false, + "can_edit": true, + "can_edit_username": true, + "can_edit_email": true, + "can_edit_name": true, + "uploaded_avatar_id": 31486, + "has_title_badges": false, + "pending_count": 0, + "profile_view_count": 7, + "second_factor_enabled": false, + "can_upload_profile_header": true, + "can_upload_user_card_background": true, + "post_count": 1, + "can_be_deleted": true, + "can_delete_all_posts": true, + "locale": "en", + "muted_category_ids": [], + "regular_category_ids": [], + "watched_tags": [], + "watching_first_post_tags": [], + "tracked_tags": [], + "muted_tags": [], + "tracked_category_ids": [], + "watched_category_ids": [], + "watched_first_post_category_ids": [], + "system_avatar_upload_id": null, + "system_avatar_template": "/letter_avatar_proxy/v4/letter/c/3be4f8/{size}.png", + "custom_avatar_upload_id": 31486, + "custom_avatar_template": "/user_avatar/community.n8n.io/user/{size}/2.png", + "muted_usernames": [], + "ignored_usernames": [], + "allowed_pm_usernames": [], + "mailing_list_posts_per_day": 100, + "can_change_bio": true, + "can_change_location": true, + "can_change_website": true, + "can_change_tracking_preferences": true, + "user_api_keys": null, + "user_auth_tokens": [], + "user_notification_schedule": { + "enabled": false, + "day_0_start_time": 480, + "day_0_end_time": 1020, + "day_1_start_time": 480, + "day_1_end_time": 1020, + "day_2_start_time": 480, + "day_2_end_time": 1020, + "day_3_start_time": 480, + "day_3_end_time": 1020, + "day_4_start_time": 480, + "day_4_end_time": 1020, + "day_5_start_time": 480, + "day_5_end_time": 1020, + "day_6_start_time": 480, + "day_6_end_time": 1020 + }, + "use_logo_small_as_avatar": false, + "reminders_frequency": [ + { + "name": "discourse_assign.reminders_frequency.never", + "value": 0 + }, + { + "name": "discourse_assign.reminders_frequency.daily", + "value": 1440 + }, + { + "name": "discourse_assign.reminders_frequency.weekly", + "value": 10080 + }, + { + "name": "discourse_assign.reminders_frequency.monthly", + "value": 43200 + }, + { + "name": "discourse_assign.reminders_frequency.quarterly", + "value": 129600 + } + ], + "assign_icon": "user-plus", + "assign_path": "/u/User/activity/assigned", + "accepted_answers": 0, + "featured_user_badge_ids": [], + "invited_by": null, + "groups": [ + { + "id": 10, + "automatic": true, + "name": "trust_level_0", + "display_name": "trust_level_0", + "user_count": 9295, + "mentionable_level": 0, + "messageable_level": 0, + "visibility_level": 1, + "primary_group": false, + "title": null, + "grant_trust_level": null, + "incoming_email": null, + "has_messages": false, + "flair_url": null, + "flair_bg_color": null, + "flair_color": null, + "bio_raw": null, + "bio_cooked": null, + "bio_excerpt": null, + "public_admission": false, + "public_exit": false, + "allow_membership_requests": false, + "full_name": null, + "default_notification_level": 3, + "membership_request_template": null, + "members_visibility_level": 0, + "can_see_members": true, + "can_admin_group": true, + "publish_read_state": false + } + ], + "group_users": [ + { + "group_id": 10, + "user_id": 1, + "notification_level": 3 + } + ], + "user_option": { + "user_id": 1, + "mailing_list_mode": false, + "mailing_list_mode_frequency": 1, + "email_digests": false, + "email_level": 1, + "email_messages_level": 0, + "external_links_in_new_tab": true, + "color_scheme_id": null, + "dark_scheme_id": null, + "dynamic_favicon": true, + "enable_quoting": true, + "enable_defer": false, + "digest_after_minutes": 0, + "automatically_unpin_topics": true, + "auto_track_topics_after_msecs": 300000, + "notification_level_when_replying": 2, + "new_topic_duration_minutes": 2880, + "email_previous_replies": 2, + "email_in_reply_to": false, + "like_notification_frequency": 1, + "include_tl0_in_digests": false, + "theme_ids": [ + 7 + ], + "theme_key_seq": 0, + "allow_private_messages": true, + "enable_allowed_pm_users": false, + "homepage_id": null, + "hide_profile_and_presence": false, + "text_size": "normal", + "text_size_seq": 0, + "title_count_mode": "notifications", + "bookmark_auto_delete_preference": 3, + "timezone": "Europe/Berlin", + "skip_new_user_tips": false, + "default_calendar": "none_selected", + "oldest_search_log_date": null, + "seen_popups": [ + 1, + 3 + ] + } + } + }] + } +} diff --git a/packages/editor-ui/src/composables/useNodeHelpers.ts b/packages/editor-ui/src/composables/useNodeHelpers.ts index b7b431ffd4f27..7eb41633b1c95 100644 --- a/packages/editor-ui/src/composables/useNodeHelpers.ts +++ b/packages/editor-ui/src/composables/useNodeHelpers.ts @@ -562,7 +562,7 @@ export function useNodeHelpers() { let data: ITaskDataConnections | undefined = taskData.data; if (paneType === 'input' && taskData.inputOverride) { - data = taskData.inputOverride!; + data = taskData.inputOverride; } if (!data) { @@ -577,16 +577,7 @@ export function useNodeHelpers() { outputIndex: number, connectionType: ConnectionTypes = NodeConnectionType.Main, ): INodeExecutionData[] { - if ( - !connectionsData || - !connectionsData.hasOwnProperty(connectionType) || - connectionsData[connectionType] === undefined || - connectionsData[connectionType].length < outputIndex || - connectionsData[connectionType][outputIndex] === null - ) { - return []; - } - return connectionsData[connectionType][outputIndex] as INodeExecutionData[]; + return connectionsData?.[connectionType]?.[outputIndex] ?? []; } function getBinaryData( @@ -602,16 +593,18 @@ export function useNodeHelpers() { const runData: IRunData | null = workflowRunData; - if (!runData?.[node]?.[runIndex]?.data) { + const runDataOfNode = runData?.[node]?.[runIndex]?.data; + if (!runDataOfNode) { return []; } - const inputData = getInputData(runData[node][runIndex].data!, outputIndex, connectionType); + const inputData = getInputData(runDataOfNode, outputIndex, connectionType); const returnData: IBinaryKeyData[] = []; for (let i = 0; i < inputData.length; i++) { - if (inputData[i].hasOwnProperty('binary') && inputData[i].binary !== undefined) { - returnData.push(inputData[i].binary!); + const binaryDataInIdx = inputData[i]?.binary; + if (binaryDataInIdx !== undefined) { + returnData.push(binaryDataInIdx); } } From 0ac985133be546f068f7f25b340c3bfdecadc08e Mon Sep 17 00:00:00 2001 From: Val <68596159+valya@users.noreply.github.com> Date: Thu, 4 Apr 2024 10:02:37 +0100 Subject: [PATCH 3/8] fix: Workflows executed from other workflows not stopping (#9010) --- packages/cli/src/WorkflowExecuteAdditionalData.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/cli/src/WorkflowExecuteAdditionalData.ts b/packages/cli/src/WorkflowExecuteAdditionalData.ts index c5507e457c489..4ec7d31e3172e 100644 --- a/packages/cli/src/WorkflowExecuteAdditionalData.ts +++ b/packages/cli/src/WorkflowExecuteAdditionalData.ts @@ -851,7 +851,9 @@ async function executeWorkflow( workflowExecute, }; } - data = await workflowExecute.processRunExecutionData(workflow); + const execution = workflowExecute.processRunExecutionData(workflow); + activeExecutions.attachWorkflowExecution(executionId, execution); + data = await execution; } catch (error) { const executionError = error ? (error as ExecutionError) : undefined; const fullRunData: IRun = { From f6ce81e7da74f80f81909b24f9675f7abcdb4265 Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Thu, 4 Apr 2024 05:15:37 -0400 Subject: [PATCH 4/8] fix(editor): Canvas showing error toast when clicking outside of "import workflow by url" modal (#9001) --- cypress/e2e/39-import-workflow.cy.ts | 74 +++++++++++++++++++ cypress/e2e/7-workflow-actions.cy.ts | 26 ------- .../components/MainHeader/WorkflowDetails.vue | 4 + 3 files changed, 78 insertions(+), 26 deletions(-) create mode 100644 cypress/e2e/39-import-workflow.cy.ts diff --git a/cypress/e2e/39-import-workflow.cy.ts b/cypress/e2e/39-import-workflow.cy.ts new file mode 100644 index 0000000000000..831228fba35d9 --- /dev/null +++ b/cypress/e2e/39-import-workflow.cy.ts @@ -0,0 +1,74 @@ +import { WorkflowPage } from '../pages'; +import { MessageBox as MessageBoxClass } from '../pages/modals/message-box'; + +const workflowPage = new WorkflowPage(); +const messageBox = new MessageBoxClass(); + +before(() => { + cy.fixture('Onboarding_workflow.json').then((data) => { + cy.intercept('GET', '/rest/workflows/from-url*', { + body: { data }, + }).as('downloadWorkflowFromURL'); + }); +}); + +describe('Import workflow', () => { + describe('From URL', () => { + it('should import workflow', () => { + workflowPage.actions.visit(true); + workflowPage.getters.workflowMenu().click(); + workflowPage.getters.workflowMenuItemImportFromURLItem().click(); + + messageBox.getters.modal().should('be.visible'); + + messageBox.getters.content().type('https://fakepage.com/workflow.json'); + + messageBox.getters.confirm().click(); + + workflowPage.actions.zoomToFit(); + + workflowPage.getters.canvasNodes().should('have.length', 4); + + workflowPage.getters.errorToast().should('not.exist'); + + workflowPage.getters.successToast().should('not.exist'); + }); + + it('clicking outside modal should not show error toast', () => { + workflowPage.actions.visit(true); + + workflowPage.getters.workflowMenu().click(); + workflowPage.getters.workflowMenuItemImportFromURLItem().click(); + + cy.get('body').click(0, 0); + + workflowPage.getters.errorToast().should('not.exist'); + }); + + it('canceling modal should not show error toast', () => { + workflowPage.actions.visit(true); + + workflowPage.getters.workflowMenu().click(); + workflowPage.getters.workflowMenuItemImportFromURLItem().click(); + messageBox.getters.cancel().click(); + + workflowPage.getters.errorToast().should('not.exist'); + }); + }); + + describe('From File', () => { + it('should import workflow', () => { + workflowPage.actions.visit(true); + + workflowPage.getters.workflowMenu().click(); + workflowPage.getters.workflowMenuItemImportFromFile().click(); + workflowPage.getters + .workflowImportInput() + .selectFile('cypress/fixtures/Test_workflow-actions_paste-data.json', { force: true }); + cy.waitForLoad(false); + workflowPage.actions.zoomToFit(); + workflowPage.getters.canvasNodes().should('have.length', 5); + workflowPage.getters.nodeConnections().should('have.length', 5); + }); + }); +}); diff --git a/cypress/e2e/7-workflow-actions.cy.ts b/cypress/e2e/7-workflow-actions.cy.ts index 2cf451b5ad8bd..04dc441c14794 100644 --- a/cypress/e2e/7-workflow-actions.cy.ts +++ b/cypress/e2e/7-workflow-actions.cy.ts @@ -13,8 +13,6 @@ import { getVisibleSelect } from '../utils'; import { WorkflowExecutionsTab } from '../pages'; const NEW_WORKFLOW_NAME = 'Something else'; -const IMPORT_WORKFLOW_URL = - 'https://gist.githubusercontent.com/OlegIvaniv/010bd3f45c8a94f8eb7012e663a8b671/raw/3afea1aec15573cc168d9af7e79395bd76082906/test-workflow.json'; const DUPLICATE_WORKFLOW_NAME = 'Duplicated workflow'; const DUPLICATE_WORKFLOW_TAG = 'Duplicate'; @@ -146,30 +144,6 @@ describe('Workflow Actions', () => { }); }); - it('should import workflow from url', () => { - WorkflowPage.getters.workflowMenu().should('be.visible'); - WorkflowPage.getters.workflowMenu().click(); - WorkflowPage.getters.workflowMenuItemImportFromURLItem().should('be.visible'); - WorkflowPage.getters.workflowMenuItemImportFromURLItem().click(); - cy.get('.el-message-box').should('be.visible'); - cy.get('.el-message-box').find('input').type(IMPORT_WORKFLOW_URL); - cy.get('body').type('{enter}'); - cy.waitForLoad(false); - WorkflowPage.actions.zoomToFit(); - WorkflowPage.getters.canvasNodes().should('have.length', 2); - WorkflowPage.getters.nodeConnections().should('have.length', 1); - }); - - it('should import workflow from file', () => { - WorkflowPage.getters - .workflowImportInput() - .selectFile('cypress/fixtures/Test_workflow-actions_paste-data.json', { force: true }); - cy.waitForLoad(false); - WorkflowPage.actions.zoomToFit(); - WorkflowPage.getters.canvasNodes().should('have.length', 5); - WorkflowPage.getters.nodeConnections().should('have.length', 5); - }); - it('should update workflow settings', () => { cy.visit(WorkflowPages.url); WorkflowPages.getters.workflowCards().then((cards) => { diff --git a/packages/editor-ui/src/components/MainHeader/WorkflowDetails.vue b/packages/editor-ui/src/components/MainHeader/WorkflowDetails.vue index e8836c651fd61..b38f72235ce98 100644 --- a/packages/editor-ui/src/components/MainHeader/WorkflowDetails.vue +++ b/packages/editor-ui/src/components/MainHeader/WorkflowDetails.vue @@ -587,6 +587,10 @@ export default defineComponent({ }, )) as MessageBoxInputData; + if (promptResponse === 'cancel') { + return; + } + nodeViewEventBus.emit('importWorkflowUrl', { url: promptResponse.value }); } catch (e) {} break; From bc6575afbb106ea22ae1ff7b1b9057ccb665a964 Mon Sep 17 00:00:00 2001 From: Omar Ajoue Date: Thu, 4 Apr 2024 10:28:35 +0100 Subject: [PATCH 5/8] fix(editor): Rerun failed nodes in manual executions (#9050) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Iván Ovejero --- .../src/composables/useRunWorkflow.test.ts | 31 ++++++++++++++++++- .../src/composables/useRunWorkflow.ts | 9 ++++-- 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/packages/editor-ui/src/composables/useRunWorkflow.test.ts b/packages/editor-ui/src/composables/useRunWorkflow.test.ts index 04e83ef123462..4b429325a8d8e 100644 --- a/packages/editor-ui/src/composables/useRunWorkflow.test.ts +++ b/packages/editor-ui/src/composables/useRunWorkflow.test.ts @@ -7,7 +7,7 @@ import { useWorkflowsStore } from '@/stores/workflows.store'; import { useUIStore } from '@/stores/ui.store'; import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers'; import { useRouter } from 'vue-router'; -import type { IPinData, IRunData, Workflow } from 'n8n-workflow'; +import { ExpressionError, type IPinData, type IRunData, type Workflow } from 'n8n-workflow'; vi.mock('@/stores/n8nRoot.store', () => ({ useRootStore: vi.fn().mockReturnValue({ pushConnectionActive: true }), @@ -281,5 +281,34 @@ describe('useRunWorkflow({ router })', () => { expect(result.startNodeNames).toContain('node1'); expect(result.runData).toBeUndefined(); }); + + it('should rerun failed parent nodes, adding them to the returned list of start nodes and not adding their result to runData', () => { + const { consolidateRunDataAndStartNodes } = useRunWorkflow({ router }); + const directParentNodes = ['node1']; + const runData = { + node1: [ + { + error: new ExpressionError('error'), + }, + ], + } as unknown as IRunData; + const workflowMock = { + getParentNodes: vi.fn().mockReturnValue([]), + nodes: { + node1: { disabled: false }, + node2: { disabled: false }, + }, + } as unknown as Workflow; + + const result = consolidateRunDataAndStartNodes( + directParentNodes, + runData, + undefined, + workflowMock, + ); + + expect(result.startNodeNames).toContain('node1'); + expect(result.runData).toEqual(undefined); + }); }); }); diff --git a/packages/editor-ui/src/composables/useRunWorkflow.ts b/packages/editor-ui/src/composables/useRunWorkflow.ts index 4e2b73e3178be..68db3ecfe7f18 100644 --- a/packages/editor-ui/src/composables/useRunWorkflow.ts +++ b/packages/editor-ui/src/composables/useRunWorkflow.ts @@ -349,14 +349,19 @@ export function useRunWorkflow(useRunWorkflowOpts: { router: ReturnType Date: Thu, 4 Apr 2024 05:30:37 -0400 Subject: [PATCH 6/8] fix(editor): Issue showing Auth2 callback section when all properties are overriden (#8999) --- cypress/e2e/2-credentials.cy.ts | 25 +++++++++++++++++++ .../CredentialEdit/CredentialConfig.vue | 5 +++- .../CredentialEdit/CredentialEdit.vue | 23 ++++++++++++++++- 3 files changed, 51 insertions(+), 2 deletions(-) diff --git a/cypress/e2e/2-credentials.cy.ts b/cypress/e2e/2-credentials.cy.ts index ca1ca6e0147ff..008758aef2f75 100644 --- a/cypress/e2e/2-credentials.cy.ts +++ b/cypress/e2e/2-credentials.cy.ts @@ -236,4 +236,29 @@ describe('Credentials', () => { .find('input') .should('have.value', NEW_QUERY_AUTH_ACCOUNT_NAME); }); + + it('should not show OAuth redirect URL section when OAuth2 credentials are overridden', () => { + cy.intercept('/types/credentials.json', { middleware: true }, (req) => { + req.headers['cache-control'] = 'no-cache, no-store'; + + req.on('response', (res) => { + const credentials = res.body || []; + + const index = credentials.findIndex((c) => c.name === 'slackOAuth2Api'); + + credentials[index] = { + ...credentials[index], + __overwrittenProperties: ['clientId', 'clientSecret'], + }; + }); + }); + + workflowPage.actions.visit(true); + workflowPage.actions.addNodeToCanvas('Slack'); + workflowPage.actions.openNode('Slack'); + workflowPage.getters.nodeCredentialsSelect().click(); + getVisibleSelect().find('li').last().click(); + credentialsModal.getters.credentialAuthTypeRadioButtons().first().click(); + nodeDetailsView.getters.copyInput().should('not.exist'); + }); }); diff --git a/packages/editor-ui/src/components/CredentialEdit/CredentialConfig.vue b/packages/editor-ui/src/components/CredentialEdit/CredentialConfig.vue index 3ef3a88a55ab4..530d39ca4528a 100644 --- a/packages/editor-ui/src/components/CredentialEdit/CredentialConfig.vue +++ b/packages/editor-ui/src/components/CredentialEdit/CredentialConfig.vue @@ -78,7 +78,7 @@ /> { + const properties = this.credentialType.properties.filter((propertyData: INodeProperties) => { if (!this.displayCredentialParameter(propertyData)) { return false; } @@ -444,6 +454,17 @@ export default defineComponent({ !this.credentialType!.__overwrittenProperties.includes(propertyData.name) ); }); + + /** + * If after all credentials overrides are applied only "notice" + * properties are left, do not return them. This will avoid + * showing notices that refer to a property that was overridden. + */ + if (properties.every((p) => p.type === 'notice')) { + return []; + } + + return properties; }, requiredPropertiesFilled(): boolean { for (const property of this.credentialProperties) { From db4f8d49a3a87c4e893bb1496b0bc74bd804de64 Mon Sep 17 00:00:00 2001 From: Csaba Tuncsik Date: Thu, 4 Apr 2024 12:33:29 +0200 Subject: [PATCH 7/8] fix(editor): Fix execution with wait node (#9051) --- packages/editor-ui/src/mixins/pushConnection.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/editor-ui/src/mixins/pushConnection.ts b/packages/editor-ui/src/mixins/pushConnection.ts index d076923dd6036..f92dfc10fafad 100644 --- a/packages/editor-ui/src/mixins/pushConnection.ts +++ b/packages/editor-ui/src/mixins/pushConnection.ts @@ -319,7 +319,7 @@ export const pushConnection = defineComponent({ const workflow = this.workflowHelpers.getCurrentWorkflow(); if (runDataExecuted.waitTill !== undefined) { const workflowSettings = this.workflowsStore.workflowSettings; - const saveManualExecutions = this.rootStore.saveManualExecutions; + const saveManualExecutions = this.settingsStore.saveManualExecutions; const isSavingExecutions = workflowSettings.saveManualExecutions === undefined From 217b07d735feab535916cff4baa72e500e3b80ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Thu, 4 Apr 2024 13:28:20 +0200 Subject: [PATCH 8/8] fix(core): Ensure only leader handles waiting executions (#9014) --- packages/cli/src/WaitTracker.ts | 22 ++++++- packages/cli/src/commands/start.ts | 2 +- .../cli/src/services/orchestration.service.ts | 3 + .../orchestration/main/MultiMainSetup.ee.ts | 4 +- packages/cli/test/unit/WaitTracker.test.ts | 64 +++++++++++++++++-- 5 files changed, 85 insertions(+), 10 deletions(-) diff --git a/packages/cli/src/WaitTracker.ts b/packages/cli/src/WaitTracker.ts index 9acb96cfc082a..4e1bcb7585387 100644 --- a/packages/cli/src/WaitTracker.ts +++ b/packages/cli/src/WaitTracker.ts @@ -7,8 +7,9 @@ import { Container, Service } from 'typedi'; import type { IExecutionsStopData, IWorkflowExecutionDataProcess } from '@/Interfaces'; import { WorkflowRunner } from '@/WorkflowRunner'; import { ExecutionRepository } from '@db/repositories/execution.repository'; -import { OwnershipService } from './services/ownership.service'; +import { OwnershipService } from '@/services/ownership.service'; import { Logger } from '@/Logger'; +import { OrchestrationService } from '@/services/orchestration.service'; @Service() export class WaitTracker { @@ -26,7 +27,22 @@ export class WaitTracker { private readonly executionRepository: ExecutionRepository, private readonly ownershipService: OwnershipService, private readonly workflowRunner: WorkflowRunner, + readonly orchestrationService: OrchestrationService, ) { + const { isLeader, isMultiMainSetupEnabled, multiMainSetup } = orchestrationService; + + if (isLeader) this.startTracking(); + + if (isMultiMainSetupEnabled) { + multiMainSetup + .on('leader-takeover', () => this.startTracking()) + .on('leader-stepdown', () => this.stopTracking()); + } + } + + startTracking() { + this.logger.debug('Wait tracker started tracking waiting executions'); + // Poll every 60 seconds a list of upcoming executions this.mainTimer = setInterval(() => { void this.getWaitingExecutions(); @@ -174,7 +190,9 @@ export class WaitTracker { }); } - shutdown() { + stopTracking() { + this.logger.debug('Wait tracker shutting down'); + clearInterval(this.mainTimer); Object.keys(this.waitingExecutions).forEach((executionId) => { clearTimeout(this.waitingExecutions[executionId].timer); diff --git a/packages/cli/src/commands/start.ts b/packages/cli/src/commands/start.ts index ad09299089644..3b7ee763a3244 100644 --- a/packages/cli/src/commands/start.ts +++ b/packages/cli/src/commands/start.ts @@ -94,7 +94,7 @@ export class Start extends BaseCommand { // Stop with trying to activate workflows that could not be activated this.activeWorkflowRunner.removeAllQueuedWorkflowActivations(); - Container.get(WaitTracker).shutdown(); + Container.get(WaitTracker).stopTracking(); await this.externalHooks?.run('n8n.stop', []); diff --git a/packages/cli/src/services/orchestration.service.ts b/packages/cli/src/services/orchestration.service.ts index d80f4dee19d71..cc0724aaf14dc 100644 --- a/packages/cli/src/services/orchestration.service.ts +++ b/packages/cli/src/services/orchestration.service.ts @@ -39,6 +39,9 @@ export class OrchestrationService { return config.getEnv('redis.queueModeId'); } + /** + * Whether this instance is the leader in a multi-main setup. Always `true` in single-main setup. + */ get isLeader() { return config.getEnv('multiMainSetup.instanceType') === 'leader'; } diff --git a/packages/cli/src/services/orchestration/main/MultiMainSetup.ee.ts b/packages/cli/src/services/orchestration/main/MultiMainSetup.ee.ts index 8d9cd5da23ef2..eda788ae670ce 100644 --- a/packages/cli/src/services/orchestration/main/MultiMainSetup.ee.ts +++ b/packages/cli/src/services/orchestration/main/MultiMainSetup.ee.ts @@ -62,7 +62,7 @@ export class MultiMainSetup extends EventEmitter { if (config.getEnv('multiMainSetup.instanceType') === 'leader') { config.set('multiMainSetup.instanceType', 'follower'); - this.emit('leader-stepdown'); // lost leadership - stop triggers, pollers, pruning + this.emit('leader-stepdown'); // lost leadership - stop triggers, pollers, pruning, wait-tracking EventReporter.info('[Multi-main setup] Leader failed to renew leader key'); } @@ -97,7 +97,7 @@ export class MultiMainSetup extends EventEmitter { await this.redisPublisher.setExpiration(this.leaderKey, this.leaderKeyTtl); - this.emit('leader-takeover'); // gained leadership - start triggers, pollers, pruning + this.emit('leader-takeover'); // gained leadership - start triggers, pollers, pruning, wait-tracking } else { config.set('multiMainSetup.instanceType', 'follower'); } diff --git a/packages/cli/test/unit/WaitTracker.test.ts b/packages/cli/test/unit/WaitTracker.test.ts index 4bf43bb94059c..a3e26826e276b 100644 --- a/packages/cli/test/unit/WaitTracker.test.ts +++ b/packages/cli/test/unit/WaitTracker.test.ts @@ -2,11 +2,17 @@ import { WaitTracker } from '@/WaitTracker'; import { mock } from 'jest-mock-extended'; import type { ExecutionRepository } from '@/databases/repositories/execution.repository'; import type { IExecutionResponse } from '@/Interfaces'; +import type { OrchestrationService } from '@/services/orchestration.service'; +import type { MultiMainSetup } from '@/services/orchestration/main/MultiMainSetup.ee'; jest.useFakeTimers(); describe('WaitTracker', () => { const executionRepository = mock(); + const orchestrationService = mock({ + isLeader: true, + isMultiMainSetupEnabled: false, + }); const execution = mock({ id: '123', @@ -21,7 +27,7 @@ describe('WaitTracker', () => { it('should query DB for waiting executions', async () => { executionRepository.getWaitingExecutions.mockResolvedValue([execution]); - new WaitTracker(mock(), executionRepository, mock(), mock()); + new WaitTracker(mock(), executionRepository, mock(), mock(), orchestrationService); expect(executionRepository.getWaitingExecutions).toHaveBeenCalledTimes(1); }); @@ -29,7 +35,7 @@ describe('WaitTracker', () => { it('if no executions to start, should do nothing', () => { executionRepository.getWaitingExecutions.mockResolvedValue([]); - new WaitTracker(mock(), executionRepository, mock(), mock()); + new WaitTracker(mock(), executionRepository, mock(), mock(), orchestrationService); expect(executionRepository.findSingleExecution).not.toHaveBeenCalled(); }); @@ -37,7 +43,13 @@ describe('WaitTracker', () => { describe('if execution to start', () => { it('if not enough time passed, should not start execution', async () => { executionRepository.getWaitingExecutions.mockResolvedValue([execution]); - const waitTracker = new WaitTracker(mock(), executionRepository, mock(), mock()); + const waitTracker = new WaitTracker( + mock(), + executionRepository, + mock(), + mock(), + orchestrationService, + ); executionRepository.getWaitingExecutions.mockResolvedValue([execution]); await waitTracker.getWaitingExecutions(); @@ -51,7 +63,13 @@ describe('WaitTracker', () => { it('if enough time passed, should start execution', async () => { executionRepository.getWaitingExecutions.mockResolvedValue([]); - const waitTracker = new WaitTracker(mock(), executionRepository, mock(), mock()); + const waitTracker = new WaitTracker( + mock(), + executionRepository, + mock(), + mock(), + orchestrationService, + ); executionRepository.getWaitingExecutions.mockResolvedValue([execution]); await waitTracker.getWaitingExecutions(); @@ -68,7 +86,13 @@ describe('WaitTracker', () => { describe('startExecution()', () => { it('should query for execution to start', async () => { executionRepository.getWaitingExecutions.mockResolvedValue([]); - const waitTracker = new WaitTracker(mock(), executionRepository, mock(), mock()); + const waitTracker = new WaitTracker( + mock(), + executionRepository, + mock(), + mock(), + orchestrationService, + ); executionRepository.findSingleExecution.mockResolvedValue(execution); waitTracker.startExecution(execution.id); @@ -80,4 +104,34 @@ describe('WaitTracker', () => { }); }); }); + + describe('multi-main setup', () => { + it('should start tracking if leader', () => { + const orchestrationService = mock({ + isLeader: true, + isMultiMainSetupEnabled: true, + multiMainSetup: mock({ on: jest.fn().mockReturnThis() }), + }); + + executionRepository.getWaitingExecutions.mockResolvedValue([]); + + new WaitTracker(mock(), executionRepository, mock(), mock(), orchestrationService); + + expect(executionRepository.getWaitingExecutions).toHaveBeenCalledTimes(1); + }); + + it('should not start tracking if follower', () => { + const orchestrationService = mock({ + isLeader: false, + isMultiMainSetupEnabled: true, + multiMainSetup: mock({ on: jest.fn().mockReturnThis() }), + }); + + executionRepository.getWaitingExecutions.mockResolvedValue([]); + + new WaitTracker(mock(), executionRepository, mock(), mock(), orchestrationService); + + expect(executionRepository.getWaitingExecutions).not.toHaveBeenCalled(); + }); + }); });