From f1e2df7d0753aa0f33cf299100a063bf89cc8b35 Mon Sep 17 00:00:00 2001 From: Alex Grozav Date: Mon, 11 Nov 2024 13:35:20 +0200 Subject: [PATCH 01/14] feat(editor): Improve workflow loading performance on new canvas (#11629) --- .../src/composables/useCanvasOperations.test.ts | 17 +++++++++++++++++ .../src/composables/useCanvasOperations.ts | 15 +++++++-------- .../editor-ui/src/stores/workflows.store.ts | 4 ++++ packages/editor-ui/src/views/NodeView.v2.vue | 15 +++++++++------ 4 files changed, 37 insertions(+), 14 deletions(-) diff --git a/packages/editor-ui/src/composables/useCanvasOperations.test.ts b/packages/editor-ui/src/composables/useCanvasOperations.test.ts index 3a90f05ff7b..bb301173c94 100644 --- a/packages/editor-ui/src/composables/useCanvasOperations.test.ts +++ b/packages/editor-ui/src/composables/useCanvasOperations.test.ts @@ -17,6 +17,7 @@ import { useHistoryStore } from '@/stores/history.store'; import { useNDVStore } from '@/stores/ndv.store'; import { createTestNode, + createTestWorkflow, createTestWorkflowObject, mockNode, mockNodeTypeDescription, @@ -2033,6 +2034,22 @@ describe('useCanvasOperations', () => { }, ); }); + + describe('initializeWorkspace', () => { + it('should initialize the workspace', () => { + const workflowsStore = mockedStore(useWorkflowsStore); + const workflow = createTestWorkflow({ + nodes: [createTestNode()], + connections: {}, + }); + + const { initializeWorkspace } = useCanvasOperations({ router }); + initializeWorkspace(workflow); + + expect(workflowsStore.setNodes).toHaveBeenCalled(); + expect(workflowsStore.setConnections).toHaveBeenCalled(); + }); + }); }); function buildImportNodes() { diff --git a/packages/editor-ui/src/composables/useCanvasOperations.ts b/packages/editor-ui/src/composables/useCanvasOperations.ts index 4b78da01b2a..23ac54a1c63 100644 --- a/packages/editor-ui/src/composables/useCanvasOperations.ts +++ b/packages/editor-ui/src/composables/useCanvasOperations.ts @@ -612,12 +612,11 @@ export function useCanvasOperations({ router }: { router: ReturnType { - workflowsStore.setNodePristine(nodeData.name, true); - if (!options.keepPristine) { uiStore.stateIsDirty = true; } + workflowsStore.setNodePristine(nodeData.name, true); nodeHelpers.matchCredentials(nodeData); nodeHelpers.updateNodeParameterIssues(nodeData); nodeHelpers.updateNodeCredentialIssues(nodeData); @@ -1378,15 +1377,15 @@ export function useCanvasOperations({ router }: { router: ReturnType { + nodeHelpers.matchCredentials(node); }); + + workflowsStore.setNodes(data.nodes); + workflowsStore.setConnections(data.connections); } /** diff --git a/packages/editor-ui/src/stores/workflows.store.ts b/packages/editor-ui/src/stores/workflows.store.ts index 0d5bb872f50..98a54ce1d12 100644 --- a/packages/editor-ui/src/stores/workflows.store.ts +++ b/packages/editor-ui/src/stores/workflows.store.ts @@ -1046,6 +1046,10 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => { function setNodes(nodes: INodeUi[]): void { workflow.value.nodes = nodes; + nodeMetadata.value = nodes.reduce((acc, node) => { + acc[node.name] = { pristine: true }; + return acc; + }, {}); } function setConnections(connections: IConnections): void { diff --git a/packages/editor-ui/src/views/NodeView.v2.vue b/packages/editor-ui/src/views/NodeView.v2.vue index 5d8905be821..8ce1e979412 100644 --- a/packages/editor-ui/src/views/NodeView.v2.vue +++ b/packages/editor-ui/src/views/NodeView.v2.vue @@ -354,7 +354,7 @@ async function initializeWorkspaceForExistingWorkflow(id: string) { try { const workflowData = await workflowsStore.fetchWorkflow(id); - await openWorkflow(workflowData); + openWorkflow(workflowData); if (workflowData.meta?.onboardingId) { trackOpenWorkflowFromOnboardingTemplate(); @@ -379,11 +379,11 @@ async function initializeWorkspaceForExistingWorkflow(id: string) { * Workflow */ -async function openWorkflow(data: IWorkflowDb) { +function openWorkflow(data: IWorkflowDb) { resetWorkspace(); workflowHelpers.setDocumentTitle(data.name, 'IDLE'); - await initializeWorkspace(data); + initializeWorkspace(data); void externalHooks.run('workflow.open', { workflowId: data.id, @@ -815,7 +815,8 @@ async function importWorkflowExact({ workflow: workflowData }: { workflow: IWork resetWorkspace(); await initializeData(); - await initializeWorkspace({ + + initializeWorkspace({ ...workflowData, nodes: NodeViewUtils.getFixedNodesList(workflowData.nodes), } as IWorkflowDb); @@ -1074,7 +1075,9 @@ async function openExecution(executionId: string) { } await initializeData(); - await initializeWorkspace(data.workflowData); + + initializeWorkspace(data.workflowData); + workflowsStore.setWorkflowExecutionData(data); uiStore.stateIsDirty = false; @@ -1254,7 +1257,7 @@ async function onSourceControlPull() { const workflowData = await workflowsStore.fetchWorkflow(workflowId.value); if (workflowData) { workflowHelpers.setDocumentTitle(workflowData.name, 'IDLE'); - await openWorkflow(workflowData); + openWorkflow(workflowData); } } } catch (error) { From 0a05aa48621a6757a542ba4655b2e25f1971bf36 Mon Sep 17 00:00:00 2001 From: Tomi Turtiainen <10324676+tomi@users.noreply.github.com> Date: Mon, 11 Nov 2024 13:55:55 +0200 Subject: [PATCH 02/14] fix(core): Handle `item`, `items` and `$node` correctly in JS task runner (no-changelog) (#11660) --- .../__tests__/built-ins-parser.test.ts | 16 ++++++++++++++++ .../built-ins-parser/built-ins-parser.ts | 13 ++++++++++++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/packages/@n8n/task-runner/src/js-task-runner/built-ins-parser/__tests__/built-ins-parser.test.ts b/packages/@n8n/task-runner/src/js-task-runner/built-ins-parser/__tests__/built-ins-parser.test.ts index 366a9188dea..58d314dc19e 100644 --- a/packages/@n8n/task-runner/src/js-task-runner/built-ins-parser/__tests__/built-ins-parser.test.ts +++ b/packages/@n8n/task-runner/src/js-task-runner/built-ins-parser/__tests__/built-ins-parser.test.ts @@ -62,6 +62,15 @@ describe('BuiltInsParser', () => { expect(state).toEqual(new BuiltInsParserState({ needs$input: true })); }); + + test.each([['items'], ['item']])( + 'should mark input as needed when %s is used', + (identifier) => { + const state = parseAndExpectOk(`return ${identifier};`); + + expect(state).toEqual(new BuiltInsParserState({ needs$input: true })); + }, + ); }); describe('$(...)', () => { @@ -135,6 +144,13 @@ describe('BuiltInsParser', () => { ); }); + describe('$node', () => { + it('should require all nodes when $node is used', () => { + const state = parseAndExpectOk('return $node["name"];'); + expect(state).toEqual(new BuiltInsParserState({ needsAllNodes: true, needs$input: true })); + }); + }); + describe('ECMAScript syntax', () => { describe('ES2020', () => { it('should parse optional chaining', () => { diff --git a/packages/@n8n/task-runner/src/js-task-runner/built-ins-parser/built-ins-parser.ts b/packages/@n8n/task-runner/src/js-task-runner/built-ins-parser/built-ins-parser.ts index dd2d849c6a9..9b9e4b34b22 100644 --- a/packages/@n8n/task-runner/src/js-task-runner/built-ins-parser/built-ins-parser.ts +++ b/packages/@n8n/task-runner/src/js-task-runner/built-ins-parser/built-ins-parser.ts @@ -125,8 +125,19 @@ export class BuiltInsParser { private visitIdentifier = (node: Identifier, state: BuiltInsParserState) => { if (node.name === '$env') { state.markEnvAsNeeded(); - } else if (node.name === '$input' || node.name === '$json') { + } else if ( + node.name === '$input' || + node.name === '$json' || + node.name === 'items' || + // item is deprecated but we still need to support it + node.name === 'item' + ) { state.markInputAsNeeded(); + } else if (node.name === '$node') { + // $node is legacy way of accessing any node's output. We need to + // support it for backward compatibility, but we're not gonna + // implement any optimizations + state.markNeedsAllNodes(); } else if (node.name === '$execution') { state.markExecutionAsNeeded(); } else if (node.name === '$prevNode') { From c0aa67b8f0cf646c130b431718274cf86b423514 Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Mon, 11 Nov 2024 07:02:26 -0500 Subject: [PATCH 03/14] fix(editor): Prevent deleting a sticky note while editing (no-changelog) (#11576) --- packages/editor-ui/src/components/Sticky.vue | 11 ++++++-- .../render-types/CanvasNodeStickyNote.test.ts | 26 +++++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/packages/editor-ui/src/components/Sticky.vue b/packages/editor-ui/src/components/Sticky.vue index 12ee2dfa89d..bac0e076967 100644 --- a/packages/editor-ui/src/components/Sticky.vue +++ b/packages/editor-ui/src/components/Sticky.vue @@ -62,6 +62,7 @@ const isTouchActive = ref(false); const forceActions = ref(false); const isColorPopoverVisible = ref(false); const stickOptions = ref(); +const isEditing = ref(false); const setForceActions = (value: boolean) => { forceActions.value = value; @@ -147,8 +148,13 @@ const workflowRunning = computed(() => uiStore.isActionActive.workflowRunning); const showActions = computed( () => - !(props.hideActions || props.isReadOnly || workflowRunning.value || isResizing.value) || - forceActions.value, + !( + props.hideActions || + isEditing.value || + props.isReadOnly || + workflowRunning.value || + isResizing.value + ) || forceActions.value, ); onMounted(() => { @@ -187,6 +193,7 @@ const changeColor = (index: number) => { }; const onEdit = (edit: boolean) => { + isEditing.value = edit; if (edit && !props.isActive && node.value) { ndvStore.activeNodeName = node.value.name; } else if (props.isActive && !edit) { diff --git a/packages/editor-ui/src/components/canvas/elements/nodes/render-types/CanvasNodeStickyNote.test.ts b/packages/editor-ui/src/components/canvas/elements/nodes/render-types/CanvasNodeStickyNote.test.ts index e56b3cef63b..e9d48541ff7 100644 --- a/packages/editor-ui/src/components/canvas/elements/nodes/render-types/CanvasNodeStickyNote.test.ts +++ b/packages/editor-ui/src/components/canvas/elements/nodes/render-types/CanvasNodeStickyNote.test.ts @@ -3,6 +3,7 @@ import { createComponentRenderer } from '@/__tests__/render'; import { createCanvasNodeProvide } from '@/__tests__/data'; import { createTestingPinia } from '@pinia/testing'; import { setActivePinia } from 'pinia'; +import { fireEvent } from '@testing-library/vue'; const renderComponent = createComponentRenderer(CanvasNodeStickyNote); @@ -42,4 +43,29 @@ describe('CanvasNodeStickyNote', () => { expect(resizeControls).toHaveLength(0); }); + + it('should disable sticky options when in edit mode', async () => { + const { container } = renderComponent({ + global: { + provide: { + ...createCanvasNodeProvide({ + id: 'sticky', + readOnly: false, + }), + }, + }, + }); + + const stickyTextarea = container.querySelector('.sticky-textarea'); + + if (!stickyTextarea) return; + + await fireEvent.dblClick(stickyTextarea); + + const stickyOptions = container.querySelector('.sticky-options'); + + if (!stickyOptions) return; + + expect(getComputedStyle(stickyOptions).display).toBe('none'); + }); }); From b0ba24cbbc55cebc26e9f62ead7396c4c8fbd062 Mon Sep 17 00:00:00 2001 From: Alex Grozav Date: Mon, 11 Nov 2024 14:58:26 +0200 Subject: [PATCH 04/14] fix(editor): Show node executing status shortly before switching to success on new canvas (#11675) --- .../nodes/render-types/parts/CanvasNodeStatusIcons.vue | 4 ++-- packages/editor-ui/src/composables/useCanvasNode.ts | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/editor-ui/src/components/canvas/elements/nodes/render-types/parts/CanvasNodeStatusIcons.vue b/packages/editor-ui/src/components/canvas/elements/nodes/render-types/parts/CanvasNodeStatusIcons.vue index b3413183dd7..a095d84e0b5 100644 --- a/packages/editor-ui/src/components/canvas/elements/nodes/render-types/parts/CanvasNodeStatusIcons.vue +++ b/packages/editor-ui/src/components/canvas/elements/nodes/render-types/parts/CanvasNodeStatusIcons.vue @@ -12,7 +12,7 @@ const { hasIssues, executionStatus, executionWaiting, - executionRunning, + executionRunningThrottled, hasRunData, runDataIterations, isDisabled, @@ -58,7 +58,7 @@ const hideNodeIssues = computed(() => false); // @TODO Implement this
diff --git a/packages/editor-ui/src/composables/useCanvasNode.ts b/packages/editor-ui/src/composables/useCanvasNode.ts index 56b27264bac..6b051992fb9 100644 --- a/packages/editor-ui/src/composables/useCanvasNode.ts +++ b/packages/editor-ui/src/composables/useCanvasNode.ts @@ -7,6 +7,7 @@ import { CanvasNodeKey } from '@/constants'; import { computed, inject } from 'vue'; import type { CanvasNodeData } from '@/types'; import { CanvasNodeRenderType, CanvasConnectionMode } from '@/types'; +import { refThrottled } from '@vueuse/core'; export function useCanvasNode() { const node = inject(CanvasNodeKey); @@ -58,6 +59,7 @@ export function useCanvasNode() { const executionStatus = computed(() => data.value.execution.status); const executionWaiting = computed(() => data.value.execution.waiting); const executionRunning = computed(() => data.value.execution.running); + const executionRunningThrottled = refThrottled(executionRunning, 300); const runDataOutputMap = computed(() => data.value.runData.outputMap); const runDataIterations = computed(() => data.value.runData.iterations); @@ -89,6 +91,7 @@ export function useCanvasNode() { executionStatus, executionWaiting, executionRunning, + executionRunningThrottled, render, eventBus, }; From cbdd535fe0cb4e032ea82f008dcf35cc5f2264c2 Mon Sep 17 00:00:00 2001 From: Michael Kret <88898367+michael-radency@users.noreply.github.com> Date: Mon, 11 Nov 2024 15:03:12 +0200 Subject: [PATCH 05/14] fix(Notion Node): Extract page url (#11643) Co-authored-by: Elias Meire --- .../nodes/Notion/shared/GenericFunctions.ts | 29 ++++- .../Notion/test/GenericFunctions.test.ts | 116 +++++++++++++++++- .../nodes/Notion/v2/NotionV2.node.ts | 19 +-- 3 files changed, 149 insertions(+), 15 deletions(-) diff --git a/packages/nodes-base/nodes/Notion/shared/GenericFunctions.ts b/packages/nodes-base/nodes/Notion/shared/GenericFunctions.ts index 2fd594756f9..68270ce05a4 100644 --- a/packages/nodes-base/nodes/Notion/shared/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Notion/shared/GenericFunctions.ts @@ -8,6 +8,7 @@ import type { ILoadOptionsFunctions, INode, INodeExecutionData, + INodeParameterResourceLocator, INodeProperties, IPairedItemData, IPollFunctions, @@ -23,7 +24,7 @@ import moment from 'moment-timezone'; import { validate as uuidValidate } from 'uuid'; import set from 'lodash/set'; import { filters } from './descriptions/Filters'; -import { blockUrlExtractionRegexp } from './constants'; +import { blockUrlExtractionRegexp, databasePageUrlValidationRegexp } from './constants'; function uuidValidateWithoutDashes(this: IExecuteFunctions, value: string) { if (uuidValidate(value)) return true; @@ -916,6 +917,32 @@ export function extractPageId(page = '') { return page; } +export function getPageId(this: IExecuteFunctions, i: number) { + const page = this.getNodeParameter('pageId', i, {}) as INodeParameterResourceLocator; + let pageId = ''; + + if (page.value && typeof page.value === 'string') { + if (page.mode === 'id') { + pageId = page.value; + } else if (page.value.includes('p=')) { + // e.g https://www.notion.so/xxxxx?v=xxxxx&p=xxxxx&pm=s + pageId = new URLSearchParams(page.value).get('p') || ''; + } else { + // e.g https://www.notion.so/page_name-xxxxx + pageId = page.value.match(databasePageUrlValidationRegexp)?.[1] || ''; + } + } + + if (!pageId) { + throw new NodeOperationError( + this.getNode(), + 'Could not extract page ID from URL: ' + page.value, + ); + } + + return pageId; +} + export function extractDatabaseId(database: string) { if (database.includes('?v=')) { const data = database.split('?v=')[0].split('/'); diff --git a/packages/nodes-base/nodes/Notion/test/GenericFunctions.test.ts b/packages/nodes-base/nodes/Notion/test/GenericFunctions.test.ts index 40b01ea18b9..1250a0231ae 100644 --- a/packages/nodes-base/nodes/Notion/test/GenericFunctions.test.ts +++ b/packages/nodes-base/nodes/Notion/test/GenericFunctions.test.ts @@ -1,5 +1,9 @@ +import type { IExecuteFunctions, INode, INodeParameterResourceLocator } from 'n8n-workflow'; +import { NodeOperationError } from 'n8n-workflow'; import { databasePageUrlExtractionRegexp } from '../shared/constants'; -import { extractPageId, formatBlocks } from '../shared/GenericFunctions'; +import { extractPageId, formatBlocks, getPageId } from '../shared/GenericFunctions'; +import type { MockProxy } from 'jest-mock-extended'; +import { mock } from 'jest-mock-extended'; describe('Test NotionV2, formatBlocks', () => { it('should format to_do block', () => { @@ -89,3 +93,113 @@ describe('Test Notion', () => { }); }); }); + +describe('Test Notion, getPageId', () => { + let mockExecuteFunctions: MockProxy; + const id = '3ab5bc794647496dac48feca926813fd'; + + beforeEach(() => { + mockExecuteFunctions = mock(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should return page ID directly when mode is id', () => { + const page = { + mode: 'id', + value: id, + } as INodeParameterResourceLocator; + + mockExecuteFunctions.getNodeParameter.mockReturnValue(page); + + const result = getPageId.call(mockExecuteFunctions, 0); + expect(result).toBe(id); + expect(mockExecuteFunctions.getNodeParameter).toHaveBeenCalledWith('pageId', 0, {}); + }); + + it('should extract page ID from URL with p parameter', () => { + const page = { + mode: 'url', + value: `https://www.notion.so/xxxxx?v=xxxxx&p=${id}&pm=s`, + } as INodeParameterResourceLocator; + + mockExecuteFunctions.getNodeParameter.mockReturnValue(page); + + const result = getPageId.call(mockExecuteFunctions, 0); + expect(result).toBe(id); + }); + + it('should extract page ID from URL using regex', () => { + const page = { + mode: 'url', + value: `https://www.notion.so/page-name-${id}`, + } as INodeParameterResourceLocator; + + mockExecuteFunctions.getNodeParameter.mockReturnValue(page); + + const result = getPageId.call(mockExecuteFunctions, 0); + expect(result).toBe(id); + }); + + it('should throw error when page ID cannot be extracted', () => { + const page = { + mode: 'url', + value: 'https://www.notion.so/invalid-url', + } as INodeParameterResourceLocator; + + mockExecuteFunctions.getNodeParameter.mockReturnValue(page); + mockExecuteFunctions.getNode.mockReturnValue(mock({ name: 'Notion', type: 'notion' })); + + expect(() => getPageId.call(mockExecuteFunctions, 0)).toThrow(NodeOperationError); + expect(() => getPageId.call(mockExecuteFunctions, 0)).toThrow( + 'Could not extract page ID from URL: https://www.notion.so/invalid-url', + ); + }); + + it('should throw error when page value is empty', () => { + const page = { + mode: 'url', + value: '', + } as INodeParameterResourceLocator; + + mockExecuteFunctions.getNodeParameter.mockReturnValue(page); + mockExecuteFunctions.getNode.mockReturnValue(mock({ name: 'Notion', type: 'notion' })); + + expect(() => getPageId.call(mockExecuteFunctions, 0)).toThrow(NodeOperationError); + expect(() => getPageId.call(mockExecuteFunctions, 0)).toThrow( + 'Could not extract page ID from URL: ', + ); + }); + + it('should throw error when page value is undefined', () => { + const page = { + mode: 'url', + value: undefined, + } as INodeParameterResourceLocator; + + mockExecuteFunctions.getNodeParameter.mockReturnValue(page); + mockExecuteFunctions.getNode.mockReturnValue(mock({ name: 'Notion', type: 'notion' })); + + expect(() => getPageId.call(mockExecuteFunctions, 0)).toThrow(NodeOperationError); + expect(() => getPageId.call(mockExecuteFunctions, 0)).toThrow( + 'Could not extract page ID from URL: undefined', + ); + }); + + it('should throw error when page value is not a string', () => { + const page = { + mode: 'url', + value: 123 as any, + } as INodeParameterResourceLocator; + + mockExecuteFunctions.getNodeParameter.mockReturnValue(page); + mockExecuteFunctions.getNode.mockReturnValue(mock({ name: 'Notion', type: 'notion' })); + + expect(() => getPageId.call(mockExecuteFunctions, 0)).toThrow(NodeOperationError); + expect(() => getPageId.call(mockExecuteFunctions, 0)).toThrow( + 'Could not extract page ID from URL: 123', + ); + }); +}); diff --git a/packages/nodes-base/nodes/Notion/v2/NotionV2.node.ts b/packages/nodes-base/nodes/Notion/v2/NotionV2.node.ts index 6c5205f9d04..198dd325cbe 100644 --- a/packages/nodes-base/nodes/Notion/v2/NotionV2.node.ts +++ b/packages/nodes-base/nodes/Notion/v2/NotionV2.node.ts @@ -14,7 +14,7 @@ import { extractBlockId, extractDatabaseId, extractDatabaseMentionRLC, - extractPageId, + getPageId, formatBlocks, formatTitle, mapFilters, @@ -401,9 +401,8 @@ export class NotionV2 implements INodeType { if (operation === 'get') { for (let i = 0; i < itemsLength; i++) { try { - const pageId = extractPageId( - this.getNodeParameter('pageId', i, '', { extractValue: true }) as string, - ); + const pageId = getPageId.call(this, i); + const simple = this.getNodeParameter('simple', i) as boolean; responseData = await notionApiRequest.call(this, 'GET', `/pages/${pageId}`); if (simple) { @@ -526,9 +525,7 @@ export class NotionV2 implements INodeType { if (operation === 'update') { for (let i = 0; i < itemsLength; i++) { try { - const pageId = extractPageId( - this.getNodeParameter('pageId', i, '', { extractValue: true }) as string, - ); + const pageId = getPageId.call(this, i); const simple = this.getNodeParameter('simple', i) as boolean; const properties = this.getNodeParameter( 'propertiesUi.propertyValues', @@ -635,9 +632,7 @@ export class NotionV2 implements INodeType { if (operation === 'archive') { for (let i = 0; i < itemsLength; i++) { try { - const pageId = extractPageId( - this.getNodeParameter('pageId', i, '', { extractValue: true }) as string, - ); + const pageId = getPageId.call(this, i); const simple = this.getNodeParameter('simple', i) as boolean; responseData = await notionApiRequest.call(this, 'PATCH', `/pages/${pageId}`, { archived: true, @@ -672,9 +667,7 @@ export class NotionV2 implements INodeType { parent: {}, properties: {}, }; - body.parent.page_id = extractPageId( - this.getNodeParameter('pageId', i, '', { extractValue: true }) as string, - ); + body.parent.page_id = getPageId.call(this, i); body.properties = formatTitle(this.getNodeParameter('title', i) as string); const blockValues = this.getNodeParameter( 'blockUi.blockValues', From 600479bf36ba8870d4aecacad19a2dc5f2d97959 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E0=A4=95=E0=A4=BE=E0=A4=B0=E0=A4=A4=E0=A5=8B=E0=A4=AB?= =?UTF-8?q?=E0=A5=8D=E0=A4=AB=E0=A5=87=E0=A4=B2=E0=A4=B8=E0=A5=8D=E0=A4=95?= =?UTF-8?q?=E0=A5=8D=E0=A4=B0=E0=A4=BF=E0=A4=AA=E0=A5=8D=E0=A4=9F=E2=84=A2?= Date: Mon, 11 Nov 2024 17:15:27 +0100 Subject: [PATCH 06/14] fix(core): Make push work for waiting webhooks (#11678) --- packages/cli/src/webhooks/waiting-webhooks.ts | 2 +- packages/cli/src/workflow-execute-additional-data.ts | 5 +++++ packages/workflow/src/Interfaces.ts | 1 + 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/cli/src/webhooks/waiting-webhooks.ts b/packages/cli/src/webhooks/waiting-webhooks.ts index 9529d04c04a..3176bbdf2d9 100644 --- a/packages/cli/src/webhooks/waiting-webhooks.ts +++ b/packages/cli/src/webhooks/waiting-webhooks.ts @@ -215,7 +215,7 @@ export class WaitingWebhooks implements IWebhookManager { workflowData as IWorkflowDb, workflowStartNode, executionMode, - undefined, + runExecutionData.pushRef, runExecutionData, execution.id, req, diff --git a/packages/cli/src/workflow-execute-additional-data.ts b/packages/cli/src/workflow-execute-additional-data.ts index 351cd6f0599..476ebf492dc 100644 --- a/packages/cli/src/workflow-execute-additional-data.ts +++ b/packages/cli/src/workflow-execute-additional-data.ts @@ -495,6 +495,11 @@ function hookFunctionsSave(): IWorkflowExecuteHooks { retryOf: this.retryOf, }); + // When going into the waiting state, store the pushRef in the execution-data + if (fullRunData.waitTill && isManualMode) { + fullExecutionData.data.pushRef = this.pushRef; + } + await updateExistingExecution({ executionId: this.executionId, workflowId: this.workflowData.id, diff --git a/packages/workflow/src/Interfaces.ts b/packages/workflow/src/Interfaces.ts index 57ec23b5449..c993a4826c2 100644 --- a/packages/workflow/src/Interfaces.ts +++ b/packages/workflow/src/Interfaces.ts @@ -2114,6 +2114,7 @@ export interface IRunExecutionData { waitingExecutionSource: IWaitingForExecutionSource | null; }; waitTill?: Date; + pushRef?: string; } export interface IRunData { From d9d01c42dba491c81819e1a399a4896d4efed6c6 Mon Sep 17 00:00:00 2001 From: Elias Meire Date: Mon, 11 Nov 2024 17:19:50 +0100 Subject: [PATCH 07/14] test(editor): Add unit test for inputpanel component (#11644) Co-authored-by: Shireen Missi <94372015+ShireenMissi@users.noreply.github.com> --- .../src/components/InputPanel.test.ts | 86 ++++++ .../editor-ui/src/components/InputPanel.vue | 2 +- packages/editor-ui/src/components/RunData.vue | 6 +- .../__snapshots__/InputPanel.test.ts.snap | 281 ++++++++++++++++++ 4 files changed, 373 insertions(+), 2 deletions(-) create mode 100644 packages/editor-ui/src/components/InputPanel.test.ts create mode 100644 packages/editor-ui/src/components/__snapshots__/InputPanel.test.ts.snap diff --git a/packages/editor-ui/src/components/InputPanel.test.ts b/packages/editor-ui/src/components/InputPanel.test.ts new file mode 100644 index 00000000000..0b4734068af --- /dev/null +++ b/packages/editor-ui/src/components/InputPanel.test.ts @@ -0,0 +1,86 @@ +import { createTestNode, createTestWorkflow, createTestWorkflowObject } from '@/__tests__/mocks'; +import { createComponentRenderer } from '@/__tests__/render'; +import InputPanel, { type Props } from '@/components/InputPanel.vue'; +import { STORES } from '@/constants'; +import { useWorkflowsStore } from '@/stores/workflows.store'; +import { createTestingPinia } from '@pinia/testing'; +import { NodeConnectionType, type IConnections, type INodeExecutionData } from 'n8n-workflow'; +import { setActivePinia } from 'pinia'; +import { mockedStore } from '../__tests__/utils'; +import { waitFor } from '@testing-library/vue'; + +vi.mock('vue-router', () => { + return { + useRouter: () => ({}), + useRoute: () => ({ meta: {} }), + RouterLink: vi.fn(), + }; +}); + +const nodes = [ + createTestNode({ id: 'node1', name: 'Node 1' }), + createTestNode({ id: 'node2', name: 'Node 2' }), + createTestNode({ name: 'Agent' }), + createTestNode({ name: 'Tool' }), +]; + +const render = (props: Partial = {}, pinData?: INodeExecutionData[]) => { + const connections: IConnections = { + [nodes[0].name]: { + [NodeConnectionType.Main]: [ + [{ node: nodes[1].name, type: NodeConnectionType.Main, index: 0 }], + ], + }, + [nodes[1].name]: { + [NodeConnectionType.Main]: [ + [{ node: nodes[2].name, type: NodeConnectionType.Main, index: 0 }], + ], + }, + [nodes[3].name]: { + [NodeConnectionType.AiMemory]: [ + [{ node: nodes[2].name, type: NodeConnectionType.AiMemory, index: 0 }], + ], + }, + }; + + const pinia = createTestingPinia({ + stubActions: false, + initialState: { [STORES.NDV]: { activeNodeName: props.currentNodeName ?? nodes[1].name } }, + }); + setActivePinia(pinia); + + const workflow = createTestWorkflow({ nodes, connections }); + useWorkflowsStore().setWorkflow(workflow); + + if (pinData) { + mockedStore(useWorkflowsStore).pinDataByNodeName.mockReturnValue(pinData); + } + + const workflowObject = createTestWorkflowObject({ + nodes, + connections, + }); + + return createComponentRenderer(InputPanel, { + props: { + pushRef: 'pushRef', + runIndex: 0, + currentNodeName: nodes[1].name, + workflow: workflowObject, + }, + global: { + stubs: { + InputPanelPinButton: { template: '' }, + }, + }, + })({ props }); +}; + +describe('InputPanel', () => { + it('should render', async () => { + const { container, queryByTestId } = render({}, [{ json: { name: 'Test' } }]); + + await waitFor(() => expect(queryByTestId('ndv-data-size-warning')).not.toBeInTheDocument()); + expect(container).toMatchSnapshot(); + }); +}); diff --git a/packages/editor-ui/src/components/InputPanel.vue b/packages/editor-ui/src/components/InputPanel.vue index 7f42af92f6c..e695fe7bb37 100644 --- a/packages/editor-ui/src/components/InputPanel.vue +++ b/packages/editor-ui/src/components/InputPanel.vue @@ -25,7 +25,7 @@ import { storeToRefs } from 'pinia'; type MappingMode = 'debugging' | 'mapping'; -type Props = { +export type Props = { runIndex: number; workflow: Workflow; pushRef: string; diff --git a/packages/editor-ui/src/components/RunData.vue b/packages/editor-ui/src/components/RunData.vue index e1f74f215bf..cd146e7a65f 100644 --- a/packages/editor-ui/src/components/RunData.vue +++ b/packages/editor-ui/src/components/RunData.vue @@ -1522,7 +1522,11 @@ defineExpose({ enterEditMode }); xxx
-
+
{{ tooMuchDataTitle }} should render 1`] = ` +
+
+ + +
+ +
+ + Input + +
+ + + + +
+
+ +
+ +
+ + + + + +
+ + + +
+
+ + + + + + +
+ +
+ + + +
+`; From 8f695f3417820e7b9bb04b78972f6abbd61abbe8 Mon Sep 17 00:00:00 2001 From: Elias Meire Date: Mon, 11 Nov 2024 17:45:18 +0100 Subject: [PATCH 08/14] fix(editor): Fix scrolling in code edit modal (#11647) --- .../components/CodeNodeEditor/CodeNodeEditor.vue | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/editor-ui/src/components/CodeNodeEditor/CodeNodeEditor.vue b/packages/editor-ui/src/components/CodeNodeEditor/CodeNodeEditor.vue index f46e8f32907..9054604f055 100644 --- a/packages/editor-ui/src/components/CodeNodeEditor/CodeNodeEditor.vue +++ b/packages/editor-ui/src/components/CodeNodeEditor/CodeNodeEditor.vue @@ -404,7 +404,12 @@ async function onDrop(value: string, event: MouseEvent) { data-test-id="code-node-tab-code" :class="$style.fillHeight" > - +