Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(editor): Add pinned data for freshly added nodes #8323

Merged
merged 10 commits into from
Jan 19, 2024
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
Loading