diff --git a/cypress/e2e/20-workflow-executions.cy.ts b/cypress/e2e/20-workflow-executions.cy.ts index 3bcea60d6783b..bf5ecb17b3e53 100644 --- a/cypress/e2e/20-workflow-executions.cy.ts +++ b/cypress/e2e/20-workflow-executions.cy.ts @@ -84,7 +84,8 @@ describe('Current Workflow Executions', () => { executionsTab.actions.switchToExecutionsTab(); cy.wait(['@getExecution']); - cy.getByTestId('workflow-preview-iframe') + executionsTab.getters + .workflowExecutionPreviewIframe() .should('be.visible') .its('0.contentDocument.body') // Access the body of the iframe document .should('not.be.empty') // Ensure the body is not empty diff --git a/cypress/e2e/2106-ADO-pinned-data-execution-preview.cy.ts b/cypress/e2e/2106-ADO-pinned-data-execution-preview.cy.ts new file mode 100644 index 0000000000000..386d8762aa760 --- /dev/null +++ b/cypress/e2e/2106-ADO-pinned-data-execution-preview.cy.ts @@ -0,0 +1,65 @@ +import { v4 as uuid } from 'uuid'; +import { WorkflowExecutionsTab, WorkflowPage as WorkflowPageClass } from '../pages'; +import { BACKEND_BASE_URL } from '../constants'; + +const workflowPage = new WorkflowPageClass(); +const executionsTab = new WorkflowExecutionsTab(); + +describe('ADO-2106 connections should be colored correctly for pinned data in executions preview', () => { + beforeEach(() => { + workflowPage.actions.visit(); + }); + + beforeEach(() => { + cy.createFixtureWorkflow('Webhook_set_pinned.json', `Webhook set pinned ${uuid()}`); + workflowPage.actions.deselectAll(); + workflowPage.getters.zoomToFitButton().click(); + + workflowPage.getters.getConnectionBetweenNodes('Webhook', 'Set').should('have.class', 'pinned'); + }); + + it('should not color connections for pinned data nodes for production executions', () => { + workflowPage.actions.activateWorkflow(); + + // Execute the workflow + cy.request('POST', `${BACKEND_BASE_URL}/webhook/23fc3930-b8f9-41d9-89db-b647291a2201`, { + here: 'is some data', + }).then((response) => { + expect(response.status).to.eq(200); + }); + + executionsTab.actions.switchToExecutionsTab(); + + executionsTab.getters.successfulExecutionListItems().should('have.length', 1); + + executionsTab.getters + .workflowExecutionPreviewIframe() + .should('be.visible') + .its('0.contentDocument.body') + .should('not.be.empty') + .then(cy.wrap) + .find(`.jtk-connector[data-source-node="Webhook"][data-target-node="Set"]`) + .should('have.class', 'success') + .should('have.class', 'has-run') + .should('not.have.class', 'pinned'); + }); + + it('should color connections for pinned data nodes for manual executions', () => { + workflowPage.actions.executeWorkflow(); + + executionsTab.actions.switchToExecutionsTab(); + + executionsTab.getters.successfulExecutionListItems().should('have.length', 1); + + executionsTab.getters + .workflowExecutionPreviewIframe() + .should('be.visible') + .its('0.contentDocument.body') + .should('not.be.empty') + .then(cy.wrap) + .find(`.jtk-connector[data-source-node="Webhook"][data-target-node="Set"]`) + .should('have.class', 'success') + .should('have.class', 'has-run') + .should('have.class', 'pinned'); + }); +}); diff --git a/cypress/fixtures/Webhook_set_pinned.json b/cypress/fixtures/Webhook_set_pinned.json new file mode 100644 index 0000000000000..12401db243f3f --- /dev/null +++ b/cypress/fixtures/Webhook_set_pinned.json @@ -0,0 +1,67 @@ +{ + "nodes": [ + { + "parameters": { + "options": {} + }, + "id": "bd816131-d8ad-4b4c-90d6-59fdab2e6307", + "name": "Set", + "type": "n8n-nodes-base.set", + "typeVersion": 1, + "position": [ + 720, + 460 + ] + }, + { + "parameters": { + "httpMethod": "POST", + "path": "23fc3930-b8f9-41d9-89db-b647291a2201", + "options": {} + }, + "id": "82fe0f6c-854a-4eb9-b311-d7b43025c047", + "name": "Webhook", + "type": "n8n-nodes-base.webhook", + "typeVersion": 1, + "position": [ + 460, + 460 + ], + "webhookId": "23fc3930-b8f9-41d9-89db-b647291a2201" + } + ], + "connections": { + "Webhook": { + "main": [ + [ + { + "node": "Set", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "pinData": { + "Webhook": [ + { + "headers": { + "host": "localhost:5678", + "content-length": "37", + "accept": "*/*", + "content-type": "application/json", + "accept-encoding": "gzip" + }, + "params": {}, + "query": {}, + "body": { + "here": "be", + "dragons": true + }, + "webhookUrl": "http://localhost:5678/webhook-test/23fc3930-b8f9-41d9-89db-b647291a2201", + "executionMode": "test" + } + ] + } +} diff --git a/cypress/pages/workflow-executions-tab.ts b/cypress/pages/workflow-executions-tab.ts index cf9665a8b8e33..474a00745e017 100644 --- a/cypress/pages/workflow-executions-tab.ts +++ b/cypress/pages/workflow-executions-tab.ts @@ -24,6 +24,7 @@ export class WorkflowExecutionsTab extends BasePage { executionPreviewId: () => this.getters.executionPreviewDetails().find('[data-test-id="execution-preview-id"]'), executionDebugButton: () => cy.getByTestId('execution-debug-button'), + workflowExecutionPreviewIframe: () => cy.getByTestId('workflow-preview-iframe'), }; actions = { toggleNodeEnabled: (nodeName: string) => { diff --git a/packages/editor-ui/src/components/Node.vue b/packages/editor-ui/src/components/Node.vue index 74aa86a208d3a..9dd4af5130d11 100644 --- a/packages/editor-ui/src/components/Node.vue +++ b/packages/editor-ui/src/components/Node.vue @@ -108,9 +108,9 @@
@@ -187,7 +187,7 @@ import { LOCAL_STORAGE_PIN_DATA_DISCOVERY_CANVAS_FLAG, MANUAL_TRIGGER_NODE_TYPE, NODE_INSERT_SPACER_BETWEEN_INPUT_GROUPS, - NOT_DUPLICATABE_NODE_TYPES, + NOT_DUPLICATABLE_NODE_TYPES, SIMULATE_NODE_TYPE, SIMULATE_TRIGGER_NODE_TYPE, WAIT_TIME_UNLIMITED, @@ -287,7 +287,7 @@ export default defineComponent({ }, isDuplicatable(): boolean { if (!this.nodeType) return true; - if (NOT_DUPLICATABE_NODE_TYPES.includes(this.nodeType.name)) return false; + if (NOT_DUPLICATABLE_NODE_TYPES.includes(this.nodeType.name)) return false; return ( this.nodeType.maxNodes === undefined || this.sameTypeNodes.length < this.nodeType.maxNodes ); @@ -493,7 +493,7 @@ export default defineComponent({ position(): XYPosition { return this.node ? this.node.position : [0, 0]; }, - showDisabledLinethrough(): boolean { + showDisabledLineThrough(): boolean { return ( !this.isConfigurableNode && !!(this.data?.disabled && this.inputs.length === 1 && this.outputs.length === 1) @@ -1118,7 +1118,7 @@ export default defineComponent({ } } -.disabled-linethrough { +.disabled-line-through { border: 1px solid var(--color-foreground-dark); position: absolute; top: 49px; @@ -1189,7 +1189,7 @@ export default defineComponent({ overflow: auto; } -.disabled-linethrough { +.disabled-line-through { z-index: 8; } diff --git a/packages/editor-ui/src/components/WorkflowPreview.vue b/packages/editor-ui/src/components/WorkflowPreview.vue index 84d290a9122af..8abeef22d0486 100644 --- a/packages/editor-ui/src/components/WorkflowPreview.vue +++ b/packages/editor-ui/src/components/WorkflowPreview.vue @@ -27,7 +27,6 @@ import { onMounted, onBeforeUnmount, ref, computed, watch } from 'vue'; import { useI18n } from '@/composables/useI18n'; import { useToast } from '@/composables/useToast'; import type { IWorkflowDb, IWorkflowTemplate } from '@/Interface'; -import { useRootStore } from '@/stores/n8nRoot.store'; import { useExecutionsStore } from '@/stores/executions.store'; const props = withDefaults( @@ -44,6 +43,9 @@ const props = withDefaults( { loading: false, mode: 'workflow', + workflow: undefined, + executionId: undefined, + executionMode: undefined, loaderType: 'image', canOpenNDV: true, hideNodeIssues: false, @@ -56,7 +58,6 @@ const emit = defineEmits<{ const i18n = useI18n(); const toast = useToast(); -const rootStore = useRootStore(); const executionsStore = useExecutionsStore(); const iframeRef = ref(null); @@ -73,8 +74,8 @@ const iframeSrc = computed(() => { const showPreview = computed(() => { return ( !props.loading && - ((props.mode === 'workflow' && props.workflow) || - (props.mode === 'execution' && props.executionId)) && + ((props.mode === 'workflow' && !!props.workflow) || + (props.mode === 'execution' && !!props.executionId)) && ready.value ); }); @@ -114,7 +115,7 @@ const loadExecution = () => { JSON.stringify({ command: 'openExecution', executionId: props.executionId, - executionMode: props.executionMode || '', + executionMode: props.executionMode ?? '', canOpenNDV: props.canOpenNDV, }), '*', diff --git a/packages/editor-ui/src/composables/useContextMenu.ts b/packages/editor-ui/src/composables/useContextMenu.ts index 8517bb89c4e73..54448e64330a0 100644 --- a/packages/editor-ui/src/composables/useContextMenu.ts +++ b/packages/editor-ui/src/composables/useContextMenu.ts @@ -1,5 +1,5 @@ import type { ActionDropdownItem, XYPosition } from '@/Interface'; -import { NOT_DUPLICATABE_NODE_TYPES, STICKY_NODE_TYPE } from '@/constants'; +import { NOT_DUPLICATABLE_NODE_TYPES, STICKY_NODE_TYPE } from '@/constants'; import { useNodeTypesStore } from '@/stores/nodeTypes.store'; import { useSourceControlStore } from '@/stores/sourceControl.store'; import { useUIStore } from '@/stores/ui.store'; @@ -72,7 +72,7 @@ export const useContextMenu = (onAction: ContextMenuActionCallback = () => {}) = const canDuplicateNode = (node: INode): boolean => { const nodeType = nodeTypesStore.getNodeType(node.type, node.typeVersion); if (!nodeType) return false; - if (NOT_DUPLICATABE_NODE_TYPES.includes(nodeType.name)) return false; + if (NOT_DUPLICATABLE_NODE_TYPES.includes(nodeType.name)) return false; return canAddNodeOfType(nodeType); }; diff --git a/packages/editor-ui/src/constants.ts b/packages/editor-ui/src/constants.ts index 4f19cd5f44c96..67cb3e7b94ef6 100644 --- a/packages/editor-ui/src/constants.ts +++ b/packages/editor-ui/src/constants.ts @@ -752,7 +752,7 @@ export const APPEND_ATTRIBUTION_DEFAULT_PATH = 'parameters.options.appendAttribu export const DRAG_EVENT_DATA_KEY = 'nodesAndConnections'; -export const NOT_DUPLICATABE_NODE_TYPES = [FORM_TRIGGER_NODE_TYPE]; +export const NOT_DUPLICATABLE_NODE_TYPES = [FORM_TRIGGER_NODE_TYPE]; export const UPDATE_WEBHOOK_ID_NODE_TYPES = [FORM_TRIGGER_NODE_TYPE]; export const CREATOR_HUB_URL = 'https://creators.n8n.io/hub'; diff --git a/packages/editor-ui/src/views/NodeView.vue b/packages/editor-ui/src/views/NodeView.vue index b8f068abdfe5a..63fcd797c15a3 100644 --- a/packages/editor-ui/src/views/NodeView.vue +++ b/packages/editor-ui/src/views/NodeView.vue @@ -915,7 +915,7 @@ export default defineComponent({ setTimeout(() => { void this.usersStore.showPersonalizationSurvey(); - this.addPinDataConnections(this.workflowsStore.pinnedWorkflowData || ({} as IPinData)); + this.addPinDataConnections(this.workflowsStore.pinnedWorkflowData); }, 0); }); @@ -2332,7 +2332,7 @@ export default defineComponent({ this.workflowsStore.addWorkflowTagIds(tagIds); setTimeout(() => { - this.addPinDataConnections(this.workflowsStore.pinnedWorkflowData || ({} as IPinData)); + this.addPinDataConnections(this.workflowsStore.pinnedWorkflowData); }); } } catch (error) { @@ -3892,7 +3892,7 @@ export default defineComponent({ }); setTimeout(() => { - this.addPinDataConnections(this.workflowsStore.pinnedWorkflowData ?? ({} as IPinData)); + this.addPinDataConnections(this.workflowsStore.pinnedWorkflowData); }); }, __removeConnection(connection: [IConnection, IConnection], removeVisualConnection = false) { @@ -4904,23 +4904,31 @@ export default defineComponent({ await this.importWorkflowData(workflowData, 'url'); } }, - addPinDataConnections(pinData: IPinData) { + addPinDataConnections(pinData?: IPinData) { + if (!pinData) { + return; + } + Object.keys(pinData).forEach((nodeName) => { const node = this.workflowsStore.getNodeByName(nodeName); if (!node) { return; } + const nodeElement = document.getElementById(node.id); + if (!nodeElement) { + return; + } + const hasRun = this.workflowsStore.getWorkflowResultDataByNodeName(nodeName) !== null; - const classNames = ['pinned']; + // In case we are showing a production execution preview we want + // to show pinned data connections as they wouldn't have been pinned + const classNames = this.isProductionExecutionPreview ? [] : ['pinned']; if (hasRun) { classNames.push('has-run'); } - const nodeElement = document.getElementById(node.id); - if (!nodeElement) { - return; - } + const connections = this.instance?.getConnections({ source: nodeElement, }); @@ -5055,7 +5063,7 @@ export default defineComponent({ }); } - this.addPinDataConnections(this.workflowsStore.pinnedWorkflowData || ({} as IPinData)); + this.addPinDataConnections(this.workflowsStore.pinnedWorkflowData); }, async saveCurrentWorkflowExternal(callback: () => void) {