From e16fbaa4a4ee975530c9ab04edfb70026249c87b Mon Sep 17 00:00:00 2001 From: Shireen Missi <94372015+ShireenMissi@users.noreply.github.com> Date: Wed, 28 May 2025 16:52:39 +0100 Subject: [PATCH 1/3] fix(Microsoft SharePoint Node): Add back the support for cred only node (#15806) --- ...icrosoftSharePointOAuth2Api.credentials.ts | 13 +++- .../credentials/icons/microsoftSharePoint.svg | 59 +++++++++++++++++++ 2 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 packages/nodes-base/credentials/icons/microsoftSharePoint.svg diff --git a/packages/nodes-base/credentials/MicrosoftSharePointOAuth2Api.credentials.ts b/packages/nodes-base/credentials/MicrosoftSharePointOAuth2Api.credentials.ts index 76b1d3fedc2ff..09abbe2ecda17 100644 --- a/packages/nodes-base/credentials/MicrosoftSharePointOAuth2Api.credentials.ts +++ b/packages/nodes-base/credentials/MicrosoftSharePointOAuth2Api.credentials.ts @@ -1,14 +1,25 @@ -import type { ICredentialType, INodeProperties } from 'n8n-workflow'; +import type { Icon, ICredentialType, INodeProperties } from 'n8n-workflow'; export class MicrosoftSharePointOAuth2Api implements ICredentialType { name = 'microsoftSharePointOAuth2Api'; extends = ['microsoftOAuth2Api']; + icon: Icon = { + light: 'file:icons/microsoftSharePoint.svg', + dark: 'file:icons/microsoftSharePoint.svg', + }; + displayName = 'Microsoft SharePoint OAuth2 API'; documentationUrl = 'microsoft'; + httpRequestNode = { + name: 'Microsoft SharePoint', + docsUrl: 'https://learn.microsoft.com/en-us/sharepoint/dev/apis/sharepoint-rest-graph', + apiBaseUrlPlaceholder: 'https://{subdomain}.sharepoint.com/_api/v2.0/', + }; + properties: INodeProperties[] = [ { displayName: 'Scope', diff --git a/packages/nodes-base/credentials/icons/microsoftSharePoint.svg b/packages/nodes-base/credentials/icons/microsoftSharePoint.svg new file mode 100644 index 0000000000000..8baca491054bb --- /dev/null +++ b/packages/nodes-base/credentials/icons/microsoftSharePoint.svg @@ -0,0 +1,59 @@ + + + + + + + + + + +]> + + + + + + + + + + + + + + + + + + + + + + From 8b76694769302a1c388707ac80c3f03d90c4f749 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20G=C3=B3mez=20Morales?= Date: Tue, 27 May 2025 14:30:18 +0200 Subject: [PATCH 2/3] fix(editor): Handle Insights calculations to prevent Infinity numbers (#15727) --- .../features/insights/insights.utils.test.ts | 4 ++-- .../src/features/insights/insights.utils.ts | 17 ++++++++++++----- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/packages/frontend/editor-ui/src/features/insights/insights.utils.test.ts b/packages/frontend/editor-ui/src/features/insights/insights.utils.test.ts index 4468eef47ec29..e3fe15b2de1d7 100644 --- a/packages/frontend/editor-ui/src/features/insights/insights.utils.test.ts +++ b/packages/frontend/editor-ui/src/features/insights/insights.utils.test.ts @@ -81,8 +81,8 @@ describe('Insights Transformers', () => { }); it('should return Infinity if value equals deviation (and thus previous value is 0)', () => { - expect(transformInsightsDeviation.total(10, 10)).toBe(Infinity); - expect(transformInsightsDeviation.failed(5, 5)).toBe(Infinity); + expect(transformInsightsDeviation.total(10, 10)).toBe(null); + expect(transformInsightsDeviation.failed(5, 5)).toBe(null); }); it('should return 0 if deviation is 0 and value is not 0', () => { diff --git a/packages/frontend/editor-ui/src/features/insights/insights.utils.ts b/packages/frontend/editor-ui/src/features/insights/insights.utils.ts index bb493e572bc49..88986285b90df 100644 --- a/packages/frontend/editor-ui/src/features/insights/insights.utils.ts +++ b/packages/frontend/editor-ui/src/features/insights/insights.utils.ts @@ -21,14 +21,21 @@ export const transformInsightsValues: Record value - deviation; +const getDeviation = (value: number, deviation: number): number | null => { + if (value === 0 && deviation === 0) return 0; + + const previousValue = getPreviousValue(value, deviation); + if (previousValue === 0) return null; // avoid division by zero + + return (deviation / previousValue) * 100; +}; + export const transformInsightsDeviation: Record< InsightsSummaryType, - (value: number, deviation: number) => number + (value: number, deviation: number) => number | null > = { - total: (value: number, deviation: number) => - value === 0 && deviation === 0 ? 0 : (deviation / getPreviousValue(value, deviation)) * 100, - failed: (value: number, deviation: number) => - value === 0 && deviation === 0 ? 0 : (deviation / getPreviousValue(value, deviation)) * 100, + total: getDeviation, + failed: getDeviation, timeSaved: (_: number, deviation: number) => transformInsightsTimeSaved(deviation), averageRunTime: (_: number, deviation: number) => transformInsightsAverageRunTime(deviation), failureRate: (_: number, deviation: number) => transformInsightsFailureRate(deviation), From ea890bc20445596d518255f72e7c40d62c87ebbb Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 29 May 2025 17:10:44 +0100 Subject: [PATCH 3/3] :rocket: Release 1.95.2 (#15823) Co-authored-by: ShireenMissi <94372015+ShireenMissi@users.noreply.github.com> Co-authored-by: shortstacked Co-authored-by: Eugene Molodkin --- CHANGELOG.md | 10 +++ cypress/composables/ndv.ts | 16 +++++ cypress/composables/workflow.ts | 23 ++++++- .../e2e/233-AI-switch-to-logs-on-error.cy.ts | 2 +- cypress/e2e/30-langchain.cy.ts | 63 ++++++++++--------- cypress/e2e/4-node-creator.cy.ts | 2 +- cypress/e2e/48-subworkflow-inputs.cy.ts | 2 +- package.json | 2 +- packages/@n8n/nodes-langchain/package.json | 2 +- packages/cli/package.json | 2 +- packages/frontend/editor-ui/package.json | 2 +- packages/nodes-base/package.json | 2 +- 12 files changed, 88 insertions(+), 40 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2fc722a4bb11f..47c4c103def07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +## [1.95.2](https://github.com/n8n-io/n8n/compare/n8n@1.95.1...n8n@1.95.2) (2025-05-29) + + +### Bug Fixes + +* **editor:** Handle Insights calculations to prevent Infinity numbers ([#15727](https://github.com/n8n-io/n8n/issues/15727)) ([8b76694](https://github.com/n8n-io/n8n/commit/8b76694769302a1c388707ac80c3f03d90c4f749)) +* **Microsoft SharePoint Node:** Add back the support for cred only node ([#15806](https://github.com/n8n-io/n8n/issues/15806)) ([e16fbaa](https://github.com/n8n-io/n8n/commit/e16fbaa4a4ee975530c9ab04edfb70026249c87b)) + + + ## [1.95.1](https://github.com/n8n-io/n8n/compare/n8n@1.95.0...n8n@1.95.1) (2025-05-27) diff --git a/cypress/composables/ndv.ts b/cypress/composables/ndv.ts index 7fbf07e3a5713..22eb423b9ae6e 100644 --- a/cypress/composables/ndv.ts +++ b/cypress/composables/ndv.ts @@ -36,6 +36,10 @@ export function getInputSelect() { return cy.getByTestId('ndv-input-select').find('input'); } +export function getInputLinkRun() { + return getInputPanel().findChildByTestId('link-run'); +} + export function getMainPanel() { return cy.getByTestId('node-parameters'); } @@ -68,6 +72,14 @@ export function getInputTbodyCell(row: number, col: number) { return getInputTableRows().eq(row).find('td').eq(col); } +export function getInputRunSelector() { + return getInputPanel().findChildByTestId('run-selector'); +} + +export function getInputPanelItemsCount() { + return getInputPanel().getByTestId('ndv-items-count'); +} + export function getOutputPanelDataContainer() { return getOutputPanel().findChildByTestId('ndv-data-container'); } @@ -329,3 +341,7 @@ export function resetHoverState() { export function setInputDisplayMode(mode: 'Schema' | 'Table' | 'JSON' | 'Binary') { getInputPanel().findChildByTestId('ndv-run-data-display-mode').contains(mode).click(); } + +export function toggleInputRunLinking() { + getInputLinkRun().click(); +} diff --git a/cypress/composables/workflow.ts b/cypress/composables/workflow.ts index a641379f9bebb..ff00150e88d3b 100644 --- a/cypress/composables/workflow.ts +++ b/cypress/composables/workflow.ts @@ -221,35 +221,52 @@ export function executeWorkflowAndWait(waitForSuccessBannerToDisappear = true) { } } +/** + * @param nodeDisplayName - The name of the node to add to the canvas + * @param plusButtonClick - Whether to click the plus button to open the node creator + * @param preventNdvClose - Whether to prevent the Ndv from closing + * @param action - The action to select in the node creator + * @param useExactMatch - Whether to use an exact match for the node name will use selector instead of enter key + */ export function addNodeToCanvas( nodeDisplayName: string, plusButtonClick = true, preventNdvClose?: boolean, action?: string, + useExactMatch = false, ) { if (plusButtonClick) { getNodeCreatorPlusButton().click(); } getNodeCreatorSearchBar().type(nodeDisplayName); - getNodeCreatorSearchBar().type('{enter}'); + + if (useExactMatch) { + cy.getByTestId('node-creator-item-name').contains(nodeDisplayName).click(); + } else { + getNodeCreatorSearchBar().type('{enter}'); + } + cy.wait(500); + cy.get('body').then((body) => { if (body.find('[data-test-id=node-creator]').length > 0) { if (action) { cy.contains(action).click(); } else { - // Select the first action cy.get('[data-keyboard-nav-type="action"]').eq(0).click(); } } }); - if (!preventNdvClose) cy.get('body').type('{esc}'); + if (!preventNdvClose) { + cy.get('body').type('{esc}'); + } } export function navigateToNewWorkflowPage(preventNodeViewUnload = true) { cy.visit(ROUTES.NEW_WORKFLOW_PAGE); + cy.getByTestId('node-creator-plus-button').should('be.visible'); cy.waitForLoad(); cy.window().then((win) => { win.preventNodeViewBeforeUnload = preventNodeViewUnload; diff --git a/cypress/e2e/233-AI-switch-to-logs-on-error.cy.ts b/cypress/e2e/233-AI-switch-to-logs-on-error.cy.ts index e637b4ecd8c55..508f606cb6be9 100644 --- a/cypress/e2e/233-AI-switch-to-logs-on-error.cy.ts +++ b/cypress/e2e/233-AI-switch-to-logs-on-error.cy.ts @@ -139,7 +139,7 @@ function createRunDataWithError(inputMessage: string) { function setupTestWorkflow(chatTrigger: boolean = false) { // Setup test workflow with AI Agent, Postgres Memory Node (source of error), Calculator Tool, and OpenAI Chat Model if (chatTrigger) { - addNodeToCanvas(MANUAL_CHAT_TRIGGER_NODE_NAME, true); + addNodeToCanvas(MANUAL_CHAT_TRIGGER_NODE_NAME, true, false, undefined, true); } else { addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME, true); } diff --git a/cypress/e2e/30-langchain.cy.ts b/cypress/e2e/30-langchain.cy.ts index ccbe4f141ccfa..9b6daf2a19ec6 100644 --- a/cypress/e2e/30-langchain.cy.ts +++ b/cypress/e2e/30-langchain.cy.ts @@ -21,6 +21,7 @@ import { sendManualChatMessage, } from '../composables/modals/chat-modal'; import { setCredentialValues } from '../composables/modals/credential-modal'; +import * as ndv from '../composables/ndv'; import { clickCreateNewCredential, clickExecuteNode, @@ -29,6 +30,7 @@ import { getOutputPanelTable, checkParameterCheckboxInputByName, } from '../composables/ndv'; +import * as workflow from '../composables/workflow'; import { addLanguageModelNodeToParent, addMemoryNodeToParent, @@ -44,7 +46,7 @@ import { disableNode, getExecuteWorkflowButton, } from '../composables/workflow'; -import { NDV, WorkflowPage } from '../pages'; +import { WorkflowPage } from '../pages'; import { createMockNodeExecutionData, runMockWorkflowExecution } from '../utils'; describe('Langchain Integration', () => { @@ -132,7 +134,7 @@ describe('Langchain Integration', () => { }); it('should be able to open and execute Basic LLM Chain node', () => { - addNodeToCanvas(MANUAL_CHAT_TRIGGER_NODE_NAME, true); + addNodeToCanvas(MANUAL_CHAT_TRIGGER_NODE_NAME, true, false, undefined, true); addNodeToCanvas(BASIC_LLM_CHAIN_NODE_NAME, true); addLanguageModelNodeToParent( @@ -171,7 +173,7 @@ describe('Langchain Integration', () => { }); it('should be able to open and execute Agent node', () => { - addNodeToCanvas(MANUAL_CHAT_TRIGGER_NODE_NAME, true); + addNodeToCanvas(MANUAL_CHAT_TRIGGER_NODE_NAME, true, false, undefined, true); addNodeToCanvas(AGENT_NODE_NAME, true); addLanguageModelNodeToParent( @@ -211,7 +213,7 @@ describe('Langchain Integration', () => { }); it('should add and use Manual Chat Trigger node together with Agent node', () => { - addNodeToCanvas(MANUAL_CHAT_TRIGGER_NODE_NAME, true); + addNodeToCanvas(MANUAL_CHAT_TRIGGER_NODE_NAME, true, false, undefined, true); addNodeToCanvas(AGENT_NODE_NAME, true); addLanguageModelNodeToParent( @@ -362,7 +364,7 @@ describe('Langchain Integration', () => { getNodes().should('have.length', 3); }); it('should not auto-add nodes if ChatTrigger is already present', () => { - addNodeToCanvas(MANUAL_CHAT_TRIGGER_NODE_NAME, true); + addNodeToCanvas(MANUAL_CHAT_TRIGGER_NODE_NAME, true, false, undefined, true); addNodeToCanvas(AGENT_NODE_NAME, true); addNodeToCanvas(AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME, true); @@ -372,7 +374,6 @@ describe('Langchain Integration', () => { it('should render runItems for sub-nodes and allow switching between them', () => { const workflowPage = new WorkflowPage(); - const ndv = new NDV(); cy.visit(workflowPage.url); cy.createFixtureWorkflow('In_memory_vector_store_fake_embeddings.json'); @@ -380,51 +381,56 @@ describe('Langchain Integration', () => { workflowPage.actions.deselectAll(); workflowPage.actions.executeNode('Populate VS'); + workflow.getNodesWithSpinner().should('not.exist'); const assertInputOutputText = (text: string, assertion: 'exist' | 'not.exist') => { - ndv.getters.outputPanel().contains(text).should(assertion); - ndv.getters.inputPanel().contains(text).should(assertion); + ndv.getOutputPanel().contains(text).should(assertion); + ndv.getOutputPanel().contains(text).should(assertion); }; workflowPage.actions.openNode('Character Text Splitter'); - ndv.getters.outputRunSelector().should('exist'); - ndv.getters.inputRunSelector().should('exist'); - ndv.getters.inputRunSelector().find('input').should('include.value', '3 of 3'); - ndv.getters.outputRunSelector().find('input').should('include.value', '3 of 3'); + + // Wait for the input panel to switch to Debugging mode + ndv.getInputPanelItemsCount().should('not.exist'); + + ndv.getOutputRunSelector().should('exist'); + ndv.getInputRunSelector().should('exist'); + ndv.getInputRunSelector().find('input').should('include.value', '3 of 3'); + ndv.getOutputRunSelector().find('input').should('include.value', '3 of 3'); assertInputOutputText('Kyiv', 'exist'); assertInputOutputText('Berlin', 'not.exist'); assertInputOutputText('Prague', 'not.exist'); - ndv.actions.changeOutputRunSelector('2 of 3'); + ndv.changeOutputRunSelector('2 of 3'); assertInputOutputText('Berlin', 'exist'); assertInputOutputText('Kyiv', 'not.exist'); assertInputOutputText('Prague', 'not.exist'); - ndv.actions.changeOutputRunSelector('1 of 3'); + ndv.changeOutputRunSelector('1 of 3'); assertInputOutputText('Prague', 'exist'); assertInputOutputText('Berlin', 'not.exist'); assertInputOutputText('Kyiv', 'not.exist'); - ndv.actions.toggleInputRunLinking(); - ndv.actions.changeOutputRunSelector('2 of 3'); - ndv.getters.inputRunSelector().find('input').should('include.value', '1 of 3'); - ndv.getters.outputRunSelector().find('input').should('include.value', '2 of 3'); - ndv.getters.inputPanel().contains('Prague').should('exist'); - ndv.getters.inputPanel().contains('Berlin').should('not.exist'); + ndv.toggleInputRunLinking(); + ndv.changeOutputRunSelector('2 of 3'); + ndv.getInputRunSelector().find('input').should('include.value', '1 of 3'); + ndv.getOutputRunSelector().find('input').should('include.value', '2 of 3'); + ndv.getInputPanel().contains('Prague').should('exist'); + ndv.getInputPanel().contains('Berlin').should('not.exist'); - ndv.getters.outputPanel().contains('Berlin').should('exist'); - ndv.getters.outputPanel().contains('Prague').should('not.exist'); + ndv.getOutputPanel().contains('Berlin').should('exist'); + ndv.getOutputPanel().contains('Prague').should('not.exist'); - ndv.actions.toggleInputRunLinking(); - ndv.getters.inputRunSelector().find('input').should('include.value', '1 of 3'); - ndv.getters.outputRunSelector().find('input').should('include.value', '1 of 3'); + ndv.toggleInputRunLinking(); + ndv.getInputRunSelector().find('input').should('include.value', '1 of 3'); + ndv.getOutputRunSelector().find('input').should('include.value', '1 of 3'); assertInputOutputText('Prague', 'exist'); assertInputOutputText('Berlin', 'not.exist'); assertInputOutputText('Kyiv', 'not.exist'); }); it('should show tool info notice if no existing tools were used during execution', () => { - addNodeToCanvas(MANUAL_CHAT_TRIGGER_NODE_NAME, true); + addNodeToCanvas(MANUAL_CHAT_TRIGGER_NODE_NAME, true, false, undefined, true); addNodeToCanvas(AGENT_NODE_NAME, true); addLanguageModelNodeToParent( @@ -469,7 +475,7 @@ describe('Langchain Integration', () => { }); it('should not show tool info notice if tools were used during execution', () => { - addNodeToCanvas(MANUAL_CHAT_TRIGGER_NODE_NAME, true); + addNodeToCanvas(MANUAL_CHAT_TRIGGER_NODE_NAME, true, false, undefined, true); addNodeToCanvas(AGENT_NODE_NAME, true, true); getRunDataInfoCallout().should('not.exist'); clickGetBackToCanvas(); @@ -523,7 +529,6 @@ describe('Langchain Integration', () => { it('should execute up to Node 1 when using partial execution', () => { const workflowPage = new WorkflowPage(); - const ndv = new NDV(); cy.visit(workflowPage.url); cy.createFixtureWorkflow('Test_workflow_chat_partial_execution.json'); @@ -531,7 +536,7 @@ describe('Langchain Integration', () => { getManualChatModal().find('main').should('not.exist'); openNode('Node 1'); - ndv.actions.execute(); + ndv.clickExecuteNode(); getManualChatModal().find('main').should('exist'); sendManualChatMessage('Test'); diff --git a/cypress/e2e/4-node-creator.cy.ts b/cypress/e2e/4-node-creator.cy.ts index 77e8167080809..995b77ab3ed53 100644 --- a/cypress/e2e/4-node-creator.cy.ts +++ b/cypress/e2e/4-node-creator.cy.ts @@ -565,7 +565,7 @@ describe('Node Creator', () => { }); it('should add node directly for sub-connection as tool', () => { - addNodeToCanvas(MANUAL_CHAT_TRIGGER_NODE_NAME, true); + addNodeToCanvas(MANUAL_CHAT_TRIGGER_NODE_NAME, true, false, undefined, true); addNodeToCanvas(AGENT_NODE_NAME, true, true); clickGetBackToCanvas(); diff --git a/cypress/e2e/48-subworkflow-inputs.cy.ts b/cypress/e2e/48-subworkflow-inputs.cy.ts index fa8d3e45106c9..9b994c6ab0833 100644 --- a/cypress/e2e/48-subworkflow-inputs.cy.ts +++ b/cypress/e2e/48-subworkflow-inputs.cy.ts @@ -70,7 +70,7 @@ describe('Sub-workflow creation and typed usage', () => { // NAVIGATE TO CHILD WORKFLOW // ************************** // Close NDV before opening the node creator - cy.get('body').type('{esc}'); + clickGetBackToCanvas(); openNode('When Executed by Another Workflow'); }); diff --git a/package.json b/package.json index 89e917fe0b879..86a7d71f0a792 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "n8n-monorepo", - "version": "1.95.1", + "version": "1.95.2", "private": true, "engines": { "node": ">=20.15", diff --git a/packages/@n8n/nodes-langchain/package.json b/packages/@n8n/nodes-langchain/package.json index 6cdc46d792086..0a8f882854ddc 100644 --- a/packages/@n8n/nodes-langchain/package.json +++ b/packages/@n8n/nodes-langchain/package.json @@ -1,6 +1,6 @@ { "name": "@n8n/n8n-nodes-langchain", - "version": "1.95.0", + "version": "1.95.1", "description": "", "main": "index.js", "scripts": { diff --git a/packages/cli/package.json b/packages/cli/package.json index d859eed3350e1..c1e60c5ebb082 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "n8n", - "version": "1.95.1", + "version": "1.95.2", "description": "n8n Workflow Automation Tool", "main": "dist/index", "types": "dist/index.d.ts", diff --git a/packages/frontend/editor-ui/package.json b/packages/frontend/editor-ui/package.json index e9c296e7d324b..ec6086231980e 100644 --- a/packages/frontend/editor-ui/package.json +++ b/packages/frontend/editor-ui/package.json @@ -1,6 +1,6 @@ { "name": "n8n-editor-ui", - "version": "1.95.1", + "version": "1.95.2", "description": "Workflow Editor UI for n8n", "main": "index.js", "scripts": { diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 68b53e6e0383e..8b517bb491bc6 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -1,6 +1,6 @@ { "name": "n8n-nodes-base", - "version": "1.94.0", + "version": "1.94.1", "description": "Base nodes of n8n", "main": "index.js", "scripts": {