Skip to content

Commit

Permalink
fix(editor): Add pinned data for freshly added nodes (#8323)
Browse files Browse the repository at this point in the history
  • Loading branch information
cstuncsik authored and ivov committed Jan 22, 2024
1 parent f898982 commit 26c6fe3
Show file tree
Hide file tree
Showing 4 changed files with 231 additions and 123 deletions.
79 changes: 79 additions & 0 deletions cypress/e2e/19-execution.cy.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { v4 as uuid } from 'uuid';
import { NDV, WorkflowExecutionsTab, WorkflowPage as WorkflowPageClass } from '../pages';
import { SCHEDULE_TRIGGER_NODE_NAME, EDIT_FIELDS_SET_NODE_NAME } from '../constants';

const workflowPage = new WorkflowPageClass();
const executionsTab = new WorkflowExecutionsTab();
Expand Down Expand Up @@ -409,5 +410,83 @@ describe('Execution', () => {
.should('have.class', 'pinned')
.should('have.class', 'has-run');
});

it('when connecting pinned node by output drag and drop', () => {
cy.drag(
workflowPage.getters.getEndpointSelector('output', SCHEDULE_TRIGGER_NODE_NAME),
[-200, -300],
);
workflowPage.getters.nodeCreatorSearchBar().should('be.visible');
workflowPage.actions.addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME, false);
cy.drag('[data-test-id="canvas-node"].jtk-drag-selected', [150, 200], {
clickToFinish: true,
});

workflowPage.getters
.getConnectionBetweenNodes('Schedule Trigger', 'Edit Fields8')
.should('have.class', 'success')
.should('have.class', 'pinned')
.should('not.have.class', 'has-run');

workflowPage.actions.executeWorkflow();

workflowPage.getters
.getConnectionBetweenNodes('Schedule Trigger', 'Edit Fields8')
.should('have.class', 'success')
.should('have.class', 'pinned')
.should('have.class', 'has-run');

cy.drag(workflowPage.getters.getEndpointSelector('output', 'Edit Fields2'), [-200, -300]);
workflowPage.getters.nodeCreatorSearchBar().should('be.visible');
workflowPage.actions.addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME, false);
cy.drag('[data-test-id="canvas-node"].jtk-drag-selected', [150, 200], {
clickToFinish: true,
});

workflowPage.getters
.getConnectionBetweenNodes('Edit Fields2', 'Edit Fields11')
.should('have.class', 'success')
.should('have.class', 'pinned')
.should('have.class', 'has-run');
});

it('when connecting pinned node after adding an unconnected node', () => {
workflowPage.actions.addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME);

cy.draganddrop(
workflowPage.getters.getEndpointSelector('output', SCHEDULE_TRIGGER_NODE_NAME),
workflowPage.getters.getEndpointSelector('input', 'Edit Fields8'),
);
workflowPage.getters.zoomToFitButton().click();

workflowPage.getters
.getConnectionBetweenNodes('Schedule Trigger', 'Edit Fields8')
.should('have.class', 'success')
.should('have.class', 'pinned')
.should('not.have.class', 'has-run');

workflowPage.actions.executeWorkflow();

workflowPage.getters
.getConnectionBetweenNodes('Schedule Trigger', 'Edit Fields8')
.should('have.class', 'success')
.should('have.class', 'pinned')
.should('have.class', 'has-run');

workflowPage.actions.deselectAll();
workflowPage.actions.addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME);
workflowPage.getters.zoomToFitButton().click();

cy.draganddrop(
workflowPage.getters.getEndpointSelector('output', 'Edit Fields7'),
workflowPage.getters.getEndpointSelector('input', 'Edit Fields11'),
);

workflowPage.getters
.getConnectionBetweenNodes('Edit Fields7', 'Edit Fields11')
.should('have.class', 'success')
.should('have.class', 'pinned')
.should('have.class', 'has-run');
});
});
});
79 changes: 78 additions & 1 deletion packages/editor-ui/src/composables/useNodeHelpers.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { useHistoryStore } from '@/stores/history.store';
import { CUSTOM_API_CALL_KEY, PLACEHOLDER_FILLED_AT_EXECUTION_TIME } from '@/constants';
import {
CUSTOM_API_CALL_KEY,
NODE_OUTPUT_DEFAULT_KEY,
PLACEHOLDER_FILLED_AT_EXECUTION_TIME,
} from '@/constants';

import { NodeHelpers, NodeConnectionType, ExpressionEvaluatorProxy } from 'n8n-workflow';
import type {
Expand All @@ -21,6 +25,7 @@ import type {
INodePropertyOptions,
INodeCredentialsDetails,
INodeParameters,
ITaskData,
} from 'n8n-workflow';

import type {
Expand All @@ -43,6 +48,9 @@ import { EnableNodeToggleCommand } from '@/models/history';
import { useTelemetry } from './useTelemetry';
import { getCredentialPermissions } from '@/permissions';
import { hasPermission } from '@/rbac/permissions';
import type { N8nPlusEndpoint } from '@/plugins/jsplumb/N8nPlusEndpointType';
import * as NodeViewUtils from '@/utils/nodeViewUtils';
import { useCanvasStore } from '@/stores/canvas.store';

declare namespace HttpRequestNode {
namespace V2 {
Expand All @@ -60,6 +68,7 @@ export function useNodeHelpers() {
const nodeTypesStore = useNodeTypesStore();
const workflowsStore = useWorkflowsStore();
const i18n = useI18n();
const canvasStore = useCanvasStore();

function hasProxyAuth(node: INodeUi): boolean {
return Object.keys(node.parameters).includes('nodeCredentialType');
Expand Down Expand Up @@ -700,6 +709,73 @@ export function useNodeHelpers() {
return undefined;
}

function setSuccessOutput(data: ITaskData[], sourceNode: INodeUi | null) {
if (!sourceNode) {
throw new Error('Source node is null or not defined');
}

const allNodeConnections = workflowsStore.outgoingConnectionsByNodeName(sourceNode.name);

const connectionType = Object.keys(allNodeConnections)[0];
const nodeConnections = allNodeConnections[connectionType];
const outputMap = NodeViewUtils.getOutputSummary(
data,
nodeConnections || [],
(connectionType as ConnectionTypes) ?? NodeConnectionType.Main,
);
const sourceNodeType = nodeTypesStore.getNodeType(sourceNode.type, sourceNode.typeVersion);

Object.keys(outputMap).forEach((sourceOutputIndex: string) => {
Object.keys(outputMap[sourceOutputIndex]).forEach((targetNodeName: string) => {
Object.keys(outputMap[sourceOutputIndex][targetNodeName]).forEach(
(targetInputIndex: string) => {
if (targetNodeName) {
const targetNode = workflowsStore.getNodeByName(targetNodeName);
const connection = NodeViewUtils.getJSPlumbConnection(
sourceNode,
parseInt(sourceOutputIndex, 10),
targetNode,
parseInt(targetInputIndex, 10),
connectionType as ConnectionTypes,
sourceNodeType,
canvasStore.jsPlumbInstance,
);

if (connection) {
const output = outputMap[sourceOutputIndex][targetNodeName][targetInputIndex];

if (output.isArtificialRecoveredEventItem) {
NodeViewUtils.recoveredConnection(connection);
} else if (!output?.total && !output.isArtificialRecoveredEventItem) {
NodeViewUtils.resetConnection(connection);
} else {
NodeViewUtils.addConnectionOutputSuccess(connection, output);
}
}
}

const endpoint = NodeViewUtils.getPlusEndpoint(
sourceNode,
parseInt(sourceOutputIndex, 10),
canvasStore.jsPlumbInstance,
);
if (endpoint?.endpoint) {
const output = outputMap[sourceOutputIndex][NODE_OUTPUT_DEFAULT_KEY][0];

if (output && output.total > 0) {
(endpoint.endpoint as N8nPlusEndpoint).setSuccessOutput(
NodeViewUtils.getRunItemsLabel(output),
);
} else {
(endpoint.endpoint as N8nPlusEndpoint).clearSuccessOutput();
}
}
},
);
});
});
}

return {
hasProxyAuth,
isCustomApiCallSelected,
Expand All @@ -718,5 +794,6 @@ export function useNodeHelpers() {
getNodeSubtitle,
updateNodesCredentialsIssues,
getNodeInputData,
setSuccessOutput,
};
}
61 changes: 61 additions & 0 deletions packages/editor-ui/src/utils/nodeViewUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import type {
INodeTypeDescription,
} from 'n8n-workflow';
import { NodeConnectionType } from 'n8n-workflow';
import type { BrowserJsPlumbInstance } from '@jsplumb/browser-ui';
import { EVENT_CONNECTION_MOUSEOUT, EVENT_CONNECTION_MOUSEOVER } from '@jsplumb/browser-ui';
import { useUIStore } from '@/stores/ui.store';

Expand Down Expand Up @@ -1083,3 +1084,63 @@ export function isElementIntersection(

return isWithinVerticalBounds && isWithinHorizontalBounds;
}

export const getJSPlumbEndpoints = (
node: INodeUi | null,
instance: BrowserJsPlumbInstance,
): Endpoint[] => {
const nodeEl = instance.getManagedElement(node?.id);

const endpoints = instance?.getEndpoints(nodeEl);
return endpoints;
};

export const getPlusEndpoint = (
node: INodeUi | null,
outputIndex: number,
instance: BrowserJsPlumbInstance,
): Endpoint | undefined => {
const endpoints = getJSPlumbEndpoints(node, instance);
return endpoints.find(
(endpoint: Endpoint) =>
// @ts-ignore
endpoint.endpoint.type === 'N8nPlus' && endpoint?.__meta?.index === outputIndex,
);
};

export const getJSPlumbConnection = (
sourceNode: INodeUi | null,
sourceOutputIndex: number,
targetNode: INodeUi | null,
targetInputIndex: number,
connectionType: ConnectionTypes,
sourceNodeType: INodeTypeDescription | null,
instance: BrowserJsPlumbInstance,
): Connection | undefined => {
if (!sourceNode || !targetNode) {
return;
}

const sourceId = sourceNode.id;
const targetId = targetNode.id;

const sourceEndpoint = getOutputEndpointUUID(sourceId, connectionType, sourceOutputIndex);
const targetEndpoint = getInputEndpointUUID(targetId, connectionType, targetInputIndex);

const sourceNodeOutput = sourceNodeType?.outputs?.[sourceOutputIndex] || NodeConnectionType.Main;
const sourceNodeOutputName =
typeof sourceNodeOutput === 'string' ? sourceNodeOutput : sourceNodeOutput.name;
const scope = getEndpointScope(sourceNodeOutputName);

// @ts-ignore
const connections = instance?.getConnections({
scope,
source: sourceId,
target: targetId,
}) as Connection[];

return connections.find((connection: Connection) => {
const uuids = connection.getUuids();
return uuids[0] === sourceEndpoint && uuids[1] === targetEndpoint;
});
};
Loading

0 comments on commit 26c6fe3

Please sign in to comment.