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:
parent
ff92fc7fef
commit
83228e26fb
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
@ -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;
|
||||
});
|
||||
};
|
||||
|
@ -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) {
|
||||
|
Loading…
Reference in New Issue
Block a user