1
1
mirror of https://github.com/n8n-io/n8n.git synced 2024-09-11 13:15:28 +03:00

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

This commit is contained in:
Csaba Tuncsik 2024-01-19 14:44:54 +01:00 committed by GitHub
parent ff92fc7fef
commit 83228e26fb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 231 additions and 123 deletions

View File

@ -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();
@ -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');
});
});
});

View File

@ -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 {
@ -21,6 +25,7 @@ import type {
INodePropertyOptions,
INodeCredentialsDetails,
INodeParameters,
ITaskData,
} from 'n8n-workflow';
import type {
@ -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 {
@ -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');
@ -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,
@ -718,5 +794,6 @@ export function useNodeHelpers() {
getNodeSubtitle,
updateNodesCredentialsIssues,
getNodeInputData,
setSuccessOutput,
};
}

View File

@ -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';
@ -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;
});
};

View File

@ -223,7 +223,6 @@ import {
MODAL_CANCEL,
MODAL_CLOSE,
MODAL_CONFIRM,
NODE_OUTPUT_DEFAULT_KEY,
ONBOARDING_CALL_SIGNUP_MODAL_KEY,
ONBOARDING_PROMPT_TIMEBOX,
PLACEHOLDER_EMPTY_WORKFLOW_ID,
@ -3629,7 +3628,7 @@ export default defineComponent({
});
setTimeout(() => {
this.addPinDataConnections(this.workflowsStore.pinData);
this.addPinDataConnections(this.workflowsStore.pinnedWorkflowData || ({} as IPinData));
});
},
__removeConnection(connection: [IConnection, IConnection], removeVisualConnection = false) {
@ -3718,70 +3717,6 @@ export default defineComponent({
const workflowData = deepCopy(await this.getNodesToSave(nodes));
await this.importWorkflowData(workflowData, 'duplicate', false);
},
getJSPlumbConnection(
sourceNodeName: string,
sourceOutputIndex: number,
targetNodeName: string,
targetInputIndex: number,
connectionType: ConnectionTypes,
): Connection | undefined {
const sourceNode = this.workflowsStore.getNodeByName(sourceNodeName);
const targetNode = this.workflowsStore.getNodeByName(targetNodeName);
if (!sourceNode || !targetNode) {
return;
}
const sourceId = sourceNode.id;
const targetId = targetNode.id;
const sourceEndpoint = NodeViewUtils.getOutputEndpointUUID(
sourceId,
connectionType,
sourceOutputIndex,
);
const targetEndpoint = NodeViewUtils.getInputEndpointUUID(
targetId,
connectionType,
targetInputIndex,
);
const sourceNodeType = this.nodeTypesStore.getNodeType(
sourceNode.type,
sourceNode.typeVersion,
);
const sourceNodeOutput =
sourceNodeType?.outputs?.[sourceOutputIndex] || NodeConnectionType.Main;
const sourceNodeOutputName =
typeof sourceNodeOutput === 'string' ? sourceNodeOutput : sourceNodeOutput.name;
const scope = NodeViewUtils.getEndpointScope(sourceNodeOutputName);
// @ts-ignore
const connections = this.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;
});
},
getJSPlumbEndpoints(nodeName: string): Endpoint[] {
const node = this.workflowsStore.getNodeByName(nodeName);
const nodeEl = this.instance.getManagedElement(node?.id);
const endpoints = this.instance?.getEndpoints(nodeEl);
return endpoints;
},
getPlusEndpoint(nodeName: string, outputIndex: number): Endpoint | undefined {
const endpoints = this.getJSPlumbEndpoints(nodeName);
return endpoints.find(
(endpoint: Endpoint) =>
// @ts-ignore
endpoint.endpoint.type === 'N8nPlus' && endpoint?.__meta?.index === outputIndex,
);
},
getIncomingOutgoingConnections(nodeName: string): {
incoming: Connection[];
outgoing: Connection[];
@ -3854,7 +3789,7 @@ export default defineComponent({
outgoing.forEach((connection: Connection) => {
NodeViewUtils.resetConnection(connection);
});
const endpoints = this.getJSPlumbEndpoints(sourceNodeName);
const endpoints = NodeViewUtils.getJSPlumbEndpoints(sourceNode, this.instance);
endpoints.forEach((endpoint: Endpoint) => {
if (endpoint.endpoint.type === 'N8nPlus') {
(endpoint.endpoint as N8nPlusEndpoint).clearSuccessOutput();
@ -3864,60 +3799,7 @@ export default defineComponent({
return;
}
const allNodeConnections = this.workflowsStore.outgoingConnectionsByNodeName(sourceNodeName);
const connectionType = Object.keys(allNodeConnections)[0];
const nodeConnections = allNodeConnections[connectionType];
const outputMap = NodeViewUtils.getOutputSummary(
data,
nodeConnections || [],
(connectionType as ConnectionTypes) ?? NodeConnectionType.Main,
);
Object.keys(outputMap).forEach((sourceOutputIndex: string) => {
Object.keys(outputMap[sourceOutputIndex]).forEach((targetNodeName: string) => {
Object.keys(outputMap[sourceOutputIndex][targetNodeName]).forEach(
(targetInputIndex: string) => {
if (targetNodeName) {
const connection = this.getJSPlumbConnection(
sourceNodeName,
parseInt(sourceOutputIndex, 10),
targetNodeName,
parseInt(targetInputIndex, 10),
connectionType as ConnectionTypes,
);
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 = this.getPlusEndpoint(
sourceNodeName,
parseInt(sourceOutputIndex, 10),
);
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();
}
}
},
);
});
});
this.nodeHelpers.setSuccessOutput(data, sourceNode);
},
removeNode(nodeName: string, trackHistory = false, trackBulk = true) {
if (!this.editAllowedCheck()) {
@ -4718,6 +4600,13 @@ export default defineComponent({
return;
}
const hasRun = this.workflowsStore.getWorkflowResultDataByNodeName(nodeName) !== null;
const classNames = ['pinned'];
if (hasRun) {
classNames.push('has-run');
}
// @ts-ignore
const connections = this.instance?.getConnections({
source: node.id,
@ -4727,7 +4616,7 @@ export default defineComponent({
NodeViewUtils.addConnectionOutputSuccess(connection, {
total: pinData[nodeName].length,
iterations: 0,
classNames: ['pinned'],
classNames,
});
});
});
@ -4840,6 +4729,8 @@ export default defineComponent({
});
});
}
this.addPinDataConnections(this.workflowsStore.pinnedWorkflowData || ({} as IPinData));
},
async saveCurrentWorkflowExternal(callback: () => void) {