mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
feat(editor): Add ability to add a node between two nodes in new canvas (no-changelog) (#10006)
This commit is contained in:
parent
d651be4e01
commit
1aae65dfdc
|
@ -24,6 +24,7 @@ const emit = defineEmits<{
|
||||||
'create:connection': [connection: Connection];
|
'create:connection': [connection: Connection];
|
||||||
'create:connection:end': [connection: Connection];
|
'create:connection:end': [connection: Connection];
|
||||||
'create:connection:cancelled': [handle: ConnectStartEvent];
|
'create:connection:cancelled': [handle: ConnectStartEvent];
|
||||||
|
'click:connection:add': [connection: Connection];
|
||||||
'click:pane': [position: XYPosition];
|
'click:pane': [position: XYPosition];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
@ -123,6 +124,10 @@ function onDeleteConnection(connection: Connection) {
|
||||||
emit('delete:connection', connection);
|
emit('delete:connection', connection);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onClickConnectionAdd(connection: Connection) {
|
||||||
|
emit('click:connection:add', connection);
|
||||||
|
}
|
||||||
|
|
||||||
function onRunNode(id: string) {
|
function onRunNode(id: string) {
|
||||||
emit('run:node', id);
|
emit('run:node', id);
|
||||||
}
|
}
|
||||||
|
@ -190,6 +195,7 @@ function onClickPane(event: MouseEvent) {
|
||||||
<Edge
|
<Edge
|
||||||
v-bind="canvasEdgeProps"
|
v-bind="canvasEdgeProps"
|
||||||
:hovered="hoveredEdges[canvasEdgeProps.id]"
|
:hovered="hoveredEdges[canvasEdgeProps.id]"
|
||||||
|
@add="onClickConnectionAdd"
|
||||||
@delete="onDeleteConnection"
|
@delete="onDeleteConnection"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -5,8 +5,11 @@ import { BaseEdge, EdgeLabelRenderer, getBezierPath } from '@vue-flow/core';
|
||||||
import CanvasEdgeToolbar from './CanvasEdgeToolbar.vue';
|
import CanvasEdgeToolbar from './CanvasEdgeToolbar.vue';
|
||||||
import { computed, useCssModule } from 'vue';
|
import { computed, useCssModule } from 'vue';
|
||||||
import type { CanvasConnectionData } from '@/types';
|
import type { CanvasConnectionData } from '@/types';
|
||||||
|
import { NodeConnectionType } from 'n8n-workflow';
|
||||||
|
import { isValidNodeConnectionType } from '@/utils/typeGuards';
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
add: [connection: Connection];
|
||||||
delete: [connection: Connection];
|
delete: [connection: Connection];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
@ -18,6 +21,12 @@ const props = defineProps<CanvasEdgeProps>();
|
||||||
|
|
||||||
const $style = useCssModule();
|
const $style = useCssModule();
|
||||||
|
|
||||||
|
const connectionType = computed(() =>
|
||||||
|
isValidNodeConnectionType(props.data.source.type)
|
||||||
|
? props.data.source.type
|
||||||
|
: NodeConnectionType.Main,
|
||||||
|
);
|
||||||
|
|
||||||
const isFocused = computed(() => props.selected || props.hovered);
|
const isFocused = computed(() => props.selected || props.hovered);
|
||||||
|
|
||||||
const status = computed(() => props.data.status);
|
const status = computed(() => props.data.status);
|
||||||
|
@ -86,6 +95,10 @@ const connection = computed<Connection>(() => ({
|
||||||
targetHandle: props.targetHandleId,
|
targetHandle: props.targetHandleId,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
function onAdd() {
|
||||||
|
emit('add', connection.value);
|
||||||
|
}
|
||||||
|
|
||||||
function onDelete() {
|
function onDelete() {
|
||||||
emit('delete', connection.value);
|
emit('delete', connection.value);
|
||||||
}
|
}
|
||||||
|
@ -105,7 +118,13 @@ function onDelete() {
|
||||||
:label-show-bg="false"
|
:label-show-bg="false"
|
||||||
/>
|
/>
|
||||||
<EdgeLabelRenderer>
|
<EdgeLabelRenderer>
|
||||||
<CanvasEdgeToolbar :class="edgeToolbarClasses" :style="edgeToolbarStyle" @delete="onDelete" />
|
<CanvasEdgeToolbar
|
||||||
|
:type="connectionType"
|
||||||
|
:class="edgeToolbarClasses"
|
||||||
|
:style="edgeToolbarStyle"
|
||||||
|
@add="onAdd"
|
||||||
|
@delete="onDelete"
|
||||||
|
/>
|
||||||
</EdgeLabelRenderer>
|
</EdgeLabelRenderer>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,17 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { useI18n } from '@/composables/useI18n';
|
import { useI18n } from '@/composables/useI18n';
|
||||||
import { computed, useCssModule } from 'vue';
|
import { computed, useCssModule } from 'vue';
|
||||||
|
import { NodeConnectionType } from 'n8n-workflow';
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
add: [];
|
||||||
delete: [];
|
delete: [];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
type: NodeConnectionType;
|
||||||
|
}>();
|
||||||
|
|
||||||
const $style = useCssModule();
|
const $style = useCssModule();
|
||||||
|
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
|
@ -14,6 +20,12 @@ const classes = computed(() => ({
|
||||||
[$style.canvasEdgeToolbar]: true,
|
[$style.canvasEdgeToolbar]: true,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const isAddButtonVisible = computed(() => props.type === NodeConnectionType.Main);
|
||||||
|
|
||||||
|
function onAdd() {
|
||||||
|
emit('add');
|
||||||
|
}
|
||||||
|
|
||||||
function onDelete() {
|
function onDelete() {
|
||||||
emit('delete');
|
emit('delete');
|
||||||
}
|
}
|
||||||
|
@ -21,6 +33,15 @@ function onDelete() {
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div :class="classes" data-test-id="canvas-edge-toolbar">
|
<div :class="classes" data-test-id="canvas-edge-toolbar">
|
||||||
|
<N8nIconButton
|
||||||
|
v-if="isAddButtonVisible"
|
||||||
|
data-test-id="add-connection-button"
|
||||||
|
type="tertiary"
|
||||||
|
size="small"
|
||||||
|
icon="plus"
|
||||||
|
:title="i18n.baseText('node.add')"
|
||||||
|
@click="onAdd"
|
||||||
|
/>
|
||||||
<N8nIconButton
|
<N8nIconButton
|
||||||
data-test-id="delete-connection-button"
|
data-test-id="delete-connection-button"
|
||||||
type="tertiary"
|
type="tertiary"
|
||||||
|
@ -37,6 +58,7 @@ function onDelete() {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
gap: var(--spacing-2xs);
|
||||||
pointer-events: all;
|
pointer-events: all;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -15,6 +15,11 @@ import {
|
||||||
import { MANUAL_TRIGGER_NODE_TYPE, SET_NODE_TYPE } from '@/constants';
|
import { MANUAL_TRIGGER_NODE_TYPE, SET_NODE_TYPE } from '@/constants';
|
||||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
|
import {
|
||||||
|
createCanvasConnectionHandleString,
|
||||||
|
createCanvasConnectionId,
|
||||||
|
} from '@/utils/canvasUtilsV2';
|
||||||
|
import { CanvasConnectionMode } from '@/types';
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
const pinia = createPinia();
|
const pinia = createPinia();
|
||||||
|
@ -243,6 +248,25 @@ describe('useCanvasMapping', () => {
|
||||||
workflowObject: ref(workflowObject) as Ref<Workflow>,
|
workflowObject: ref(workflowObject) as Ref<Workflow>,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const source = manualTriggerNode.id;
|
||||||
|
const sourceHandle = createCanvasConnectionHandleString({
|
||||||
|
type: NodeConnectionType.Main,
|
||||||
|
index: 0,
|
||||||
|
mode: CanvasConnectionMode.Output,
|
||||||
|
});
|
||||||
|
const target = setNode.id;
|
||||||
|
const targetHandle = createCanvasConnectionHandleString({
|
||||||
|
type: NodeConnectionType.Main,
|
||||||
|
index: 0,
|
||||||
|
mode: CanvasConnectionMode.Input,
|
||||||
|
});
|
||||||
|
const connectionId = createCanvasConnectionId({
|
||||||
|
source,
|
||||||
|
sourceHandle,
|
||||||
|
target,
|
||||||
|
targetHandle,
|
||||||
|
});
|
||||||
|
|
||||||
expect(mappedConnections.value).toEqual([
|
expect(mappedConnections.value).toEqual([
|
||||||
{
|
{
|
||||||
data: {
|
data: {
|
||||||
|
@ -257,12 +281,12 @@ describe('useCanvasMapping', () => {
|
||||||
type: NodeConnectionType.Main,
|
type: NodeConnectionType.Main,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
id: `[${manualTriggerNode.id}/${NodeConnectionType.Main}/0][${setNode.id}/${NodeConnectionType.Main}/0]`,
|
id: connectionId,
|
||||||
label: '',
|
label: '',
|
||||||
source: manualTriggerNode.id,
|
source,
|
||||||
sourceHandle: `outputs/${NodeConnectionType.Main}/0`,
|
sourceHandle,
|
||||||
target: setNode.id,
|
target,
|
||||||
targetHandle: `inputs/${NodeConnectionType.Main}/0`,
|
targetHandle,
|
||||||
type: 'canvas-edge',
|
type: 'canvas-edge',
|
||||||
animated: false,
|
animated: false,
|
||||||
},
|
},
|
||||||
|
@ -293,6 +317,44 @@ describe('useCanvasMapping', () => {
|
||||||
workflowObject: ref(workflowObject) as Ref<Workflow>,
|
workflowObject: ref(workflowObject) as Ref<Workflow>,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const sourceA = manualTriggerNode.id;
|
||||||
|
const sourceHandleA = createCanvasConnectionHandleString({
|
||||||
|
type: NodeConnectionType.AiTool,
|
||||||
|
index: 0,
|
||||||
|
mode: CanvasConnectionMode.Output,
|
||||||
|
});
|
||||||
|
const targetA = setNode.id;
|
||||||
|
const targetHandleA = createCanvasConnectionHandleString({
|
||||||
|
type: NodeConnectionType.AiTool,
|
||||||
|
index: 0,
|
||||||
|
mode: CanvasConnectionMode.Input,
|
||||||
|
});
|
||||||
|
const connectionIdA = createCanvasConnectionId({
|
||||||
|
source: sourceA,
|
||||||
|
sourceHandle: sourceHandleA,
|
||||||
|
target: targetA,
|
||||||
|
targetHandle: targetHandleA,
|
||||||
|
});
|
||||||
|
|
||||||
|
const sourceB = manualTriggerNode.id;
|
||||||
|
const sourceHandleB = createCanvasConnectionHandleString({
|
||||||
|
type: NodeConnectionType.AiDocument,
|
||||||
|
index: 0,
|
||||||
|
mode: CanvasConnectionMode.Output,
|
||||||
|
});
|
||||||
|
const targetB = setNode.id;
|
||||||
|
const targetHandleB = createCanvasConnectionHandleString({
|
||||||
|
type: NodeConnectionType.AiDocument,
|
||||||
|
index: 1,
|
||||||
|
mode: CanvasConnectionMode.Input,
|
||||||
|
});
|
||||||
|
const connectionIdB = createCanvasConnectionId({
|
||||||
|
source: sourceB,
|
||||||
|
sourceHandle: sourceHandleB,
|
||||||
|
target: targetB,
|
||||||
|
targetHandle: targetHandleB,
|
||||||
|
});
|
||||||
|
|
||||||
expect(mappedConnections.value).toEqual([
|
expect(mappedConnections.value).toEqual([
|
||||||
{
|
{
|
||||||
data: {
|
data: {
|
||||||
|
@ -307,12 +369,12 @@ describe('useCanvasMapping', () => {
|
||||||
type: NodeConnectionType.AiTool,
|
type: NodeConnectionType.AiTool,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
id: `[${manualTriggerNode.id}/${NodeConnectionType.AiTool}/0][${setNode.id}/${NodeConnectionType.AiTool}/0]`,
|
id: connectionIdA,
|
||||||
label: '',
|
label: '',
|
||||||
source: manualTriggerNode.id,
|
source: sourceA,
|
||||||
sourceHandle: `outputs/${NodeConnectionType.AiTool}/0`,
|
sourceHandle: sourceHandleA,
|
||||||
target: setNode.id,
|
target: targetA,
|
||||||
targetHandle: `inputs/${NodeConnectionType.AiTool}/0`,
|
targetHandle: targetHandleA,
|
||||||
type: 'canvas-edge',
|
type: 'canvas-edge',
|
||||||
animated: false,
|
animated: false,
|
||||||
},
|
},
|
||||||
|
@ -329,12 +391,12 @@ describe('useCanvasMapping', () => {
|
||||||
type: NodeConnectionType.AiDocument,
|
type: NodeConnectionType.AiDocument,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
id: `[${manualTriggerNode.id}/${NodeConnectionType.AiDocument}/0][${setNode.id}/${NodeConnectionType.AiDocument}/1]`,
|
id: connectionIdB,
|
||||||
label: '',
|
label: '',
|
||||||
source: manualTriggerNode.id,
|
source: sourceB,
|
||||||
sourceHandle: `outputs/${NodeConnectionType.AiDocument}/0`,
|
sourceHandle: sourceHandleB,
|
||||||
target: setNode.id,
|
target: targetB,
|
||||||
targetHandle: `inputs/${NodeConnectionType.AiDocument}/1`,
|
targetHandle: targetHandleB,
|
||||||
type: 'canvas-edge',
|
type: 'canvas-edge',
|
||||||
animated: false,
|
animated: false,
|
||||||
},
|
},
|
||||||
|
|
|
@ -5,12 +5,7 @@
|
||||||
|
|
||||||
import type { CanvasNode } from '@/types';
|
import type { CanvasNode } from '@/types';
|
||||||
import { CanvasConnectionMode } from '@/types';
|
import { CanvasConnectionMode } from '@/types';
|
||||||
import type {
|
import type { AddedNodesAndConnections, INodeUi, XYPosition } from '@/Interface';
|
||||||
AddedNodesAndConnections,
|
|
||||||
INodeUi,
|
|
||||||
INodeUpdatePropertiesInformation,
|
|
||||||
XYPosition,
|
|
||||||
} from '@/Interface';
|
|
||||||
import {
|
import {
|
||||||
FORM_TRIGGER_NODE_TYPE,
|
FORM_TRIGGER_NODE_TYPE,
|
||||||
QUICKSTART_NOTE_NAME,
|
QUICKSTART_NOTE_NAME,
|
||||||
|
@ -31,7 +26,9 @@ import {
|
||||||
} from '@/models/history';
|
} from '@/models/history';
|
||||||
import type { Connection } from '@vue-flow/core';
|
import type { Connection } from '@vue-flow/core';
|
||||||
import {
|
import {
|
||||||
|
createCanvasConnectionHandleString,
|
||||||
getUniqueNodeName,
|
getUniqueNodeName,
|
||||||
|
getVueFlowConnectorLengths,
|
||||||
mapCanvasConnectionToLegacyConnection,
|
mapCanvasConnectionToLegacyConnection,
|
||||||
parseCanvasConnectionHandleString,
|
parseCanvasConnectionHandleString,
|
||||||
} from '@/utils/canvasUtilsV2';
|
} from '@/utils/canvasUtilsV2';
|
||||||
|
@ -334,6 +331,7 @@ export function useCanvasOperations({
|
||||||
nodeHelpers.matchCredentials(newNodeData);
|
nodeHelpers.matchCredentials(newNodeData);
|
||||||
|
|
||||||
const lastSelectedNode = uiStore.getLastSelectedNode;
|
const lastSelectedNode = uiStore.getLastSelectedNode;
|
||||||
|
const lastSelectedNodeConnection = uiStore.lastSelectedNodeConnection;
|
||||||
const lastSelectedNodeOutputIndex = uiStore.lastSelectedNodeOutputIndex;
|
const lastSelectedNodeOutputIndex = uiStore.lastSelectedNodeOutputIndex;
|
||||||
const lastSelectedNodeEndpointUuid = uiStore.lastSelectedNodeEndpointUuid;
|
const lastSelectedNodeEndpointUuid = uiStore.lastSelectedNodeEndpointUuid;
|
||||||
|
|
||||||
|
@ -378,11 +376,37 @@ export function useCanvasOperations({
|
||||||
// Connect active node to the newly created one
|
// Connect active node to the newly created one
|
||||||
createConnection({
|
createConnection({
|
||||||
source: lastSelectedNode.id,
|
source: lastSelectedNode.id,
|
||||||
sourceHandle: `outputs/${NodeConnectionType.Main}/${outputIndex}`,
|
sourceHandle: createCanvasConnectionHandleString({
|
||||||
|
mode: CanvasConnectionMode.Output,
|
||||||
|
type: NodeConnectionType.Main,
|
||||||
|
index: outputIndex,
|
||||||
|
}),
|
||||||
target: newNodeData.id,
|
target: newNodeData.id,
|
||||||
targetHandle: `inputs/${NodeConnectionType.Main}/0`,
|
targetHandle: createCanvasConnectionHandleString({
|
||||||
|
mode: CanvasConnectionMode.Input,
|
||||||
|
type: NodeConnectionType.Main,
|
||||||
|
index: 0,
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (lastSelectedNodeConnection) {
|
||||||
|
deleteConnection(lastSelectedNodeConnection, { trackHistory: options.trackHistory });
|
||||||
|
|
||||||
|
const targetNode = workflowsStore.getNodeById(lastSelectedNodeConnection.target);
|
||||||
|
if (targetNode) {
|
||||||
|
createConnection({
|
||||||
|
source: newNodeData.id,
|
||||||
|
sourceHandle: createCanvasConnectionHandleString({
|
||||||
|
mode: CanvasConnectionMode.Input,
|
||||||
|
type: NodeConnectionType.Main,
|
||||||
|
index: 0,
|
||||||
|
}),
|
||||||
|
target: lastSelectedNodeConnection.target,
|
||||||
|
targetHandle: lastSelectedNodeConnection.targetHandle,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
historyStore.stopRecordingUndo();
|
historyStore.stopRecordingUndo();
|
||||||
|
@ -516,11 +540,10 @@ export function useCanvasOperations({
|
||||||
node.position,
|
node.position,
|
||||||
);
|
);
|
||||||
} else if (lastSelectedNode) {
|
} else if (lastSelectedNode) {
|
||||||
// @TODO Implement settings lastSelectedConnection for new canvas
|
if (uiStore.lastSelectedNodeConnection) {
|
||||||
const lastSelectedConnection = canvasStore.lastSelectedConnection;
|
|
||||||
if (lastSelectedConnection) {
|
|
||||||
// set when injecting into a connection
|
// set when injecting into a connection
|
||||||
const [diffX] = NodeViewUtils.getConnectorLengths(lastSelectedConnection);
|
const [diffX] = getVueFlowConnectorLengths(uiStore.lastSelectedNodeConnection);
|
||||||
|
|
||||||
if (diffX <= NodeViewUtils.MAX_X_TO_PUSH_DOWNSTREAM_NODES) {
|
if (diffX <= NodeViewUtils.MAX_X_TO_PUSH_DOWNSTREAM_NODES) {
|
||||||
pushDownstreamNodes(lastSelectedNode.name, NodeViewUtils.PUSH_NODES_OFFSET, {
|
pushDownstreamNodes(lastSelectedNode.name, NodeViewUtils.PUSH_NODES_OFFSET, {
|
||||||
trackHistory: options.trackHistory,
|
trackHistory: options.trackHistory,
|
||||||
|
@ -537,7 +560,7 @@ export function useCanvasOperations({
|
||||||
canvasStore.newNodeInsertPosition = null;
|
canvasStore.newNodeInsertPosition = null;
|
||||||
} else {
|
} else {
|
||||||
let yOffset = 0;
|
let yOffset = 0;
|
||||||
if (lastSelectedConnection) {
|
if (uiStore.lastSelectedNodeConnection) {
|
||||||
const sourceNodeType = nodeTypesStore.getNodeType(
|
const sourceNodeType = nodeTypesStore.getNodeType(
|
||||||
lastSelectedNode.type,
|
lastSelectedNode.type,
|
||||||
lastSelectedNode.typeVersion,
|
lastSelectedNode.typeVersion,
|
||||||
|
@ -556,16 +579,15 @@ export function useCanvasOperations({
|
||||||
sourceNodeType,
|
sourceNodeType,
|
||||||
);
|
);
|
||||||
const sourceNodeOutputTypes = NodeHelpers.getConnectionTypes(sourceNodeOutputs);
|
const sourceNodeOutputTypes = NodeHelpers.getConnectionTypes(sourceNodeOutputs);
|
||||||
|
|
||||||
const sourceNodeOutputMainOutputs = sourceNodeOutputTypes.filter(
|
const sourceNodeOutputMainOutputs = sourceNodeOutputTypes.filter(
|
||||||
(output) => output === NodeConnectionType.Main,
|
(output) => output === NodeConnectionType.Main,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (sourceNodeOutputMainOutputs.length > 1) {
|
if (sourceNodeOutputMainOutputs.length > 1) {
|
||||||
|
const { index: sourceOutputIndex } = parseCanvasConnectionHandleString(
|
||||||
|
uiStore.lastSelectedNodeConnection.sourceHandle,
|
||||||
|
);
|
||||||
const offset = offsets[sourceNodeOutputMainOutputs.length - 2];
|
const offset = offsets[sourceNodeOutputMainOutputs.length - 2];
|
||||||
const sourceOutputIndex = lastSelectedConnection.__meta
|
|
||||||
? lastSelectedConnection.__meta.sourceOutputIndex
|
|
||||||
: 0;
|
|
||||||
yOffset = offset[sourceOutputIndex];
|
yOffset = offset[sourceOutputIndex];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -729,31 +751,18 @@ export function useCanvasOperations({
|
||||||
);
|
);
|
||||||
for (const nodeName of checkNodes) {
|
for (const nodeName of checkNodes) {
|
||||||
const node = workflowsStore.nodesByName[nodeName];
|
const node = workflowsStore.nodesByName[nodeName];
|
||||||
const oldPosition = node.position;
|
|
||||||
|
|
||||||
if (node.position[0] < sourceNode.position[0]) {
|
if (node.position[0] < sourceNode.position[0]) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateInformation: INodeUpdatePropertiesInformation = {
|
updateNodePosition(
|
||||||
name: nodeName,
|
node.id,
|
||||||
properties: {
|
{
|
||||||
position: [node.position[0] + margin, node.position[1]],
|
x: node.position[0] + margin,
|
||||||
|
y: node.position[1],
|
||||||
},
|
},
|
||||||
};
|
{ trackHistory },
|
||||||
|
);
|
||||||
workflowsStore.updateNodeProperties(updateInformation);
|
|
||||||
updateNodePosition(node.id, { x: node.position[0], y: node.position[1] });
|
|
||||||
|
|
||||||
if (
|
|
||||||
(trackHistory && oldPosition[0] !== updateInformation.properties.position[0]) ||
|
|
||||||
oldPosition[1] !== updateInformation.properties.position[1]
|
|
||||||
) {
|
|
||||||
historyStore.pushCommandToUndo(
|
|
||||||
new MoveNodeCommand(nodeName, oldPosition, updateInformation.properties.position),
|
|
||||||
trackHistory,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -960,6 +960,7 @@
|
||||||
"node.disable": "Deactivate",
|
"node.disable": "Deactivate",
|
||||||
"node.enable": "Activate",
|
"node.enable": "Activate",
|
||||||
"node.delete": "Delete",
|
"node.delete": "Delete",
|
||||||
|
"node.add": "Add",
|
||||||
"node.issues": "Issues",
|
"node.issues": "Issues",
|
||||||
"node.nodeIsExecuting": "Node is executing",
|
"node.nodeIsExecuting": "Node is executing",
|
||||||
"node.nodeIsWaitingTill": "Node is waiting until {date} {time}",
|
"node.nodeIsWaitingTill": "Node is waiting until {date} {time}",
|
||||||
|
|
|
@ -12,7 +12,6 @@ import type {
|
||||||
SimplifiedNodeType,
|
SimplifiedNodeType,
|
||||||
ActionsRecord,
|
ActionsRecord,
|
||||||
ToggleNodeCreatorOptions,
|
ToggleNodeCreatorOptions,
|
||||||
NewConnectionInfo,
|
|
||||||
} from '@/Interface';
|
} from '@/Interface';
|
||||||
|
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
|
@ -26,7 +25,14 @@ import { useExternalHooks } from '@/composables/useExternalHooks';
|
||||||
import { useTelemetry } from '@/composables/useTelemetry';
|
import { useTelemetry } from '@/composables/useTelemetry';
|
||||||
import { useViewStacks } from '@/components/Node/NodeCreator/composables/useViewStacks';
|
import { useViewStacks } from '@/components/Node/NodeCreator/composables/useViewStacks';
|
||||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||||
import { createCanvasConnectionHandleString } from '@/utils/canvasUtilsV2';
|
import {
|
||||||
|
createCanvasConnectionHandleString,
|
||||||
|
parseCanvasConnectionHandleString,
|
||||||
|
} from '@/utils/canvasUtilsV2';
|
||||||
|
import type { Connection } from '@vue-flow/core';
|
||||||
|
import { CanvasConnectionMode } from '@/types';
|
||||||
|
import { isVueFlowConnection } from '@/utils/typeGuards';
|
||||||
|
import type { PartialBy } from '@/utils/typeHelpers';
|
||||||
|
|
||||||
export const useNodeCreatorStore = defineStore(STORES.NODE_CREATOR, () => {
|
export const useNodeCreatorStore = defineStore(STORES.NODE_CREATOR, () => {
|
||||||
const workflowsStore = useWorkflowsStore();
|
const workflowsStore = useWorkflowsStore();
|
||||||
|
@ -90,15 +96,15 @@ export const useNodeCreatorStore = defineStore(STORES.NODE_CREATOR, () => {
|
||||||
});
|
});
|
||||||
} else if (connectionType && nodeData) {
|
} else if (connectionType && nodeData) {
|
||||||
openNodeCreatorForConnectingNode({
|
openNodeCreatorForConnectingNode({
|
||||||
index: 0,
|
connection: {
|
||||||
endpointUuid: createCanvasConnectionHandleString({
|
source: nodeData.id,
|
||||||
mode: 'inputs',
|
sourceHandle: createCanvasConnectionHandleString({
|
||||||
type: connectionType,
|
mode: 'inputs',
|
||||||
index: 0,
|
type: connectionType,
|
||||||
}),
|
index: 0,
|
||||||
|
}),
|
||||||
|
},
|
||||||
eventSource: NODE_CREATOR_OPEN_SOURCES.NOTICE_ERROR_MESSAGE,
|
eventSource: NODE_CREATOR_OPEN_SOURCES.NOTICE_ERROR_MESSAGE,
|
||||||
outputType: connectionType,
|
|
||||||
sourceId: nodeData.id,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -172,35 +178,43 @@ export const useNodeCreatorStore = defineStore(STORES.NODE_CREATOR, () => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function openNodeCreatorForConnectingNode(info: NewConnectionInfo) {
|
function openNodeCreatorForConnectingNode({
|
||||||
const type = info.outputType ?? NodeConnectionType.Main;
|
connection,
|
||||||
|
eventSource,
|
||||||
|
nodeCreatorView,
|
||||||
|
}: {
|
||||||
|
connection: PartialBy<Connection, 'target' | 'targetHandle'>;
|
||||||
|
eventSource?: NodeCreatorOpenSource;
|
||||||
|
nodeCreatorView?: NodeFilterType;
|
||||||
|
}) {
|
||||||
// Get the node and set it as active that new nodes
|
// Get the node and set it as active that new nodes
|
||||||
// which get created get automatically connected
|
// which get created get automatically connected
|
||||||
// to it.
|
// to it.
|
||||||
const sourceNode = workflowsStore.getNodeById(info.sourceId);
|
const sourceNode = workflowsStore.getNodeById(connection.source);
|
||||||
if (!sourceNode) {
|
if (!sourceNode) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { type, index, mode } = parseCanvasConnectionHandleString(connection.sourceHandle);
|
||||||
|
|
||||||
uiStore.lastSelectedNode = sourceNode.name;
|
uiStore.lastSelectedNode = sourceNode.name;
|
||||||
uiStore.lastSelectedNodeEndpointUuid = info.endpointUuid ?? null;
|
uiStore.lastSelectedNodeEndpointUuid = connection.sourceHandle ?? null;
|
||||||
uiStore.lastSelectedNodeOutputIndex = info.index;
|
uiStore.lastSelectedNodeOutputIndex = index;
|
||||||
// canvasStore.newNodeInsertPosition = null;
|
// canvasStore.newNodeInsertPosition = null;
|
||||||
|
|
||||||
// @TODO Add connection to store
|
if (isVueFlowConnection(connection)) {
|
||||||
// if (info.connection) {
|
uiStore.lastSelectedNodeConnection = connection;
|
||||||
// canvasStore.setLastSelectedConnection(info.connection);
|
}
|
||||||
// }
|
|
||||||
|
|
||||||
openNodeCreator({
|
openNodeCreator({
|
||||||
source: info.eventSource,
|
source: eventSource,
|
||||||
createNodeActive: true,
|
createNodeActive: true,
|
||||||
nodeCreatorView: info.nodeCreatorView,
|
nodeCreatorView,
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: The animation is a bit glitchy because we're updating view stack immediately
|
// TODO: The animation is a bit glitchy because we're updating view stack immediately
|
||||||
// after the node creator is opened
|
// after the node creator is opened
|
||||||
const isOutput = info.connection?.endpoints[0].parameters.connection === 'source';
|
const isOutput = mode === CanvasConnectionMode.Output;
|
||||||
const isScopedConnection =
|
const isScopedConnection =
|
||||||
type !== NodeConnectionType.Main && nodeConnectionTypes.includes(type);
|
type !== NodeConnectionType.Main && nodeConnectionTypes.includes(type);
|
||||||
|
|
||||||
|
|
|
@ -69,6 +69,7 @@ import {
|
||||||
updateTheme,
|
updateTheme,
|
||||||
} from './ui.utils';
|
} from './ui.utils';
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
|
import type { Connection } from '@vue-flow/core';
|
||||||
|
|
||||||
let savedTheme: ThemeOption = 'system';
|
let savedTheme: ThemeOption = 'system';
|
||||||
try {
|
try {
|
||||||
|
@ -177,6 +178,7 @@ export const useUIStore = defineStore(STORES.UI, () => {
|
||||||
const lastSelectedNode = ref<string | null>(null);
|
const lastSelectedNode = ref<string | null>(null);
|
||||||
const lastSelectedNodeOutputIndex = ref<number | null>(null);
|
const lastSelectedNodeOutputIndex = ref<number | null>(null);
|
||||||
const lastSelectedNodeEndpointUuid = ref<string | null>(null);
|
const lastSelectedNodeEndpointUuid = ref<string | null>(null);
|
||||||
|
const lastSelectedNodeConnection = ref<Connection | null>(null);
|
||||||
const nodeViewOffsetPosition = ref<[number, number]>([0, 0]);
|
const nodeViewOffsetPosition = ref<[number, number]>([0, 0]);
|
||||||
const nodeViewMoveInProgress = ref<boolean>(false);
|
const nodeViewMoveInProgress = ref<boolean>(false);
|
||||||
const selectedNodes = ref<INodeUi[]>([]);
|
const selectedNodes = ref<INodeUi[]>([]);
|
||||||
|
@ -619,6 +621,7 @@ export const useUIStore = defineStore(STORES.UI, () => {
|
||||||
selectedNodes,
|
selectedNodes,
|
||||||
bannersHeight,
|
bannersHeight,
|
||||||
lastSelectedNodeEndpointUuid,
|
lastSelectedNodeEndpointUuid,
|
||||||
|
lastSelectedNodeConnection,
|
||||||
nodeViewOffsetPosition,
|
nodeViewOffsetPosition,
|
||||||
nodeViewMoveInProgress,
|
nodeViewMoveInProgress,
|
||||||
nodeViewInitialized,
|
nodeViewInitialized,
|
||||||
|
|
|
@ -5,12 +5,14 @@ import {
|
||||||
mapCanvasConnectionToLegacyConnection,
|
mapCanvasConnectionToLegacyConnection,
|
||||||
parseCanvasConnectionHandleString,
|
parseCanvasConnectionHandleString,
|
||||||
createCanvasConnectionHandleString,
|
createCanvasConnectionHandleString,
|
||||||
|
createCanvasConnectionId,
|
||||||
} from '@/utils/canvasUtilsV2';
|
} from '@/utils/canvasUtilsV2';
|
||||||
import { NodeConnectionType, type IConnections, type INodeTypeDescription } from 'n8n-workflow';
|
import { NodeConnectionType, type IConnections, type INodeTypeDescription } from 'n8n-workflow';
|
||||||
import type { CanvasConnection } from '@/types';
|
import type { CanvasConnection } from '@/types';
|
||||||
import type { INodeUi } from '@/Interface';
|
import type { INodeUi } from '@/Interface';
|
||||||
import type { Connection } from '@vue-flow/core';
|
import type { Connection } from '@vue-flow/core';
|
||||||
import { createTestNode } from '@/__tests__/mocks';
|
import { createTestNode } from '@/__tests__/mocks';
|
||||||
|
import { CanvasConnectionMode } from '@/types';
|
||||||
|
|
||||||
vi.mock('uuid', () => ({
|
vi.mock('uuid', () => ({
|
||||||
v4: vi.fn(() => 'mock-uuid'),
|
v4: vi.fn(() => 'mock-uuid'),
|
||||||
|
@ -47,15 +49,34 @@ describe('mapLegacyConnectionsToCanvasConnections', () => {
|
||||||
nodes,
|
nodes,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const source = nodes[0].id;
|
||||||
|
const sourceHandle = createCanvasConnectionHandleString({
|
||||||
|
mode: CanvasConnectionMode.Output,
|
||||||
|
type: NodeConnectionType.Main,
|
||||||
|
index: 0,
|
||||||
|
});
|
||||||
|
const target = nodes[1].id;
|
||||||
|
const targetHandle = createCanvasConnectionHandleString({
|
||||||
|
mode: CanvasConnectionMode.Input,
|
||||||
|
type: NodeConnectionType.Main,
|
||||||
|
index: 0,
|
||||||
|
});
|
||||||
|
const id = createCanvasConnectionId({
|
||||||
|
source,
|
||||||
|
target,
|
||||||
|
sourceHandle,
|
||||||
|
targetHandle,
|
||||||
|
});
|
||||||
|
|
||||||
expect(result).toEqual([
|
expect(result).toEqual([
|
||||||
{
|
{
|
||||||
id: '[1/main/0][2/main/0]',
|
id,
|
||||||
source: '1',
|
source,
|
||||||
target: '2',
|
target,
|
||||||
sourceHandle: 'outputs/main/0',
|
sourceHandle,
|
||||||
targetHandle: 'inputs/main/0',
|
targetHandle,
|
||||||
data: {
|
data: {
|
||||||
fromNodeName: 'Node A',
|
fromNodeName: nodes[0].name,
|
||||||
source: {
|
source: {
|
||||||
index: 0,
|
index: 0,
|
||||||
type: NodeConnectionType.Main,
|
type: NodeConnectionType.Main,
|
||||||
|
@ -147,15 +168,53 @@ describe('mapLegacyConnectionsToCanvasConnections', () => {
|
||||||
nodes,
|
nodes,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const sourceA = nodes[0].id;
|
||||||
|
const sourceHandleA = createCanvasConnectionHandleString({
|
||||||
|
mode: CanvasConnectionMode.Output,
|
||||||
|
type: NodeConnectionType.Main,
|
||||||
|
index: 0,
|
||||||
|
});
|
||||||
|
const targetA = nodes[1].id;
|
||||||
|
const targetHandleA = createCanvasConnectionHandleString({
|
||||||
|
mode: CanvasConnectionMode.Input,
|
||||||
|
type: NodeConnectionType.Main,
|
||||||
|
index: 0,
|
||||||
|
});
|
||||||
|
const connectionIdA = createCanvasConnectionId({
|
||||||
|
source: sourceA,
|
||||||
|
target: targetA,
|
||||||
|
sourceHandle: sourceHandleA,
|
||||||
|
targetHandle: targetHandleA,
|
||||||
|
});
|
||||||
|
|
||||||
|
const sourceB = nodes[0].id;
|
||||||
|
const sourceHandleB = createCanvasConnectionHandleString({
|
||||||
|
mode: CanvasConnectionMode.Output,
|
||||||
|
type: NodeConnectionType.Main,
|
||||||
|
index: 1,
|
||||||
|
});
|
||||||
|
const targetB = nodes[1].id;
|
||||||
|
const targetHandleB = createCanvasConnectionHandleString({
|
||||||
|
mode: CanvasConnectionMode.Input,
|
||||||
|
type: NodeConnectionType.Main,
|
||||||
|
index: 1,
|
||||||
|
});
|
||||||
|
const connectionIdB = createCanvasConnectionId({
|
||||||
|
source: sourceB,
|
||||||
|
target: targetB,
|
||||||
|
sourceHandle: sourceHandleB,
|
||||||
|
targetHandle: targetHandleB,
|
||||||
|
});
|
||||||
|
|
||||||
expect(result).toEqual([
|
expect(result).toEqual([
|
||||||
{
|
{
|
||||||
id: '[1/main/0][2/main/0]',
|
id: connectionIdA,
|
||||||
source: '1',
|
source: sourceA,
|
||||||
target: '2',
|
target: targetA,
|
||||||
sourceHandle: 'outputs/main/0',
|
sourceHandle: sourceHandleA,
|
||||||
targetHandle: 'inputs/main/0',
|
targetHandle: targetHandleA,
|
||||||
data: {
|
data: {
|
||||||
fromNodeName: 'Node A',
|
fromNodeName: nodes[0].name,
|
||||||
source: {
|
source: {
|
||||||
index: 0,
|
index: 0,
|
||||||
type: NodeConnectionType.Main,
|
type: NodeConnectionType.Main,
|
||||||
|
@ -167,13 +226,13 @@ describe('mapLegacyConnectionsToCanvasConnections', () => {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '[1/main/1][2/main/1]',
|
id: connectionIdB,
|
||||||
source: '1',
|
source: sourceA,
|
||||||
target: '2',
|
target: targetB,
|
||||||
sourceHandle: 'outputs/main/1',
|
sourceHandle: sourceHandleB,
|
||||||
targetHandle: 'inputs/main/1',
|
targetHandle: targetHandleB,
|
||||||
data: {
|
data: {
|
||||||
fromNodeName: 'Node A',
|
fromNodeName: nodes[0].name,
|
||||||
source: {
|
source: {
|
||||||
index: 1,
|
index: 1,
|
||||||
type: NodeConnectionType.Main,
|
type: NodeConnectionType.Main,
|
||||||
|
@ -228,15 +287,53 @@ describe('mapLegacyConnectionsToCanvasConnections', () => {
|
||||||
nodes,
|
nodes,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const sourceA = nodes[0].id;
|
||||||
|
const sourceHandleA = createCanvasConnectionHandleString({
|
||||||
|
mode: CanvasConnectionMode.Output,
|
||||||
|
type: NodeConnectionType.Main,
|
||||||
|
index: 0,
|
||||||
|
});
|
||||||
|
const targetA = nodes[1].id;
|
||||||
|
const targetHandleA = createCanvasConnectionHandleString({
|
||||||
|
mode: CanvasConnectionMode.Input,
|
||||||
|
type: NodeConnectionType.Main,
|
||||||
|
index: 0,
|
||||||
|
});
|
||||||
|
const connectionIdA = createCanvasConnectionId({
|
||||||
|
source: sourceA,
|
||||||
|
target: targetA,
|
||||||
|
sourceHandle: sourceHandleA,
|
||||||
|
targetHandle: targetHandleA,
|
||||||
|
});
|
||||||
|
|
||||||
|
const sourceB = nodes[0].id;
|
||||||
|
const sourceHandleB = createCanvasConnectionHandleString({
|
||||||
|
mode: CanvasConnectionMode.Output,
|
||||||
|
type: NodeConnectionType.Main,
|
||||||
|
index: 1,
|
||||||
|
});
|
||||||
|
const targetB = nodes[2].id;
|
||||||
|
const targetHandleB = createCanvasConnectionHandleString({
|
||||||
|
mode: CanvasConnectionMode.Input,
|
||||||
|
type: NodeConnectionType.Main,
|
||||||
|
index: 0,
|
||||||
|
});
|
||||||
|
const connectionIdB = createCanvasConnectionId({
|
||||||
|
source: sourceB,
|
||||||
|
target: targetB,
|
||||||
|
sourceHandle: sourceHandleB,
|
||||||
|
targetHandle: targetHandleB,
|
||||||
|
});
|
||||||
|
|
||||||
expect(result).toEqual([
|
expect(result).toEqual([
|
||||||
{
|
{
|
||||||
id: '[1/main/0][2/main/0]',
|
id: connectionIdA,
|
||||||
source: '1',
|
source: sourceA,
|
||||||
target: '2',
|
target: targetA,
|
||||||
sourceHandle: 'outputs/main/0',
|
sourceHandle: sourceHandleA,
|
||||||
targetHandle: 'inputs/main/0',
|
targetHandle: targetHandleA,
|
||||||
data: {
|
data: {
|
||||||
fromNodeName: 'Node A',
|
fromNodeName: nodes[0].name,
|
||||||
source: {
|
source: {
|
||||||
index: 0,
|
index: 0,
|
||||||
type: NodeConnectionType.Main,
|
type: NodeConnectionType.Main,
|
||||||
|
@ -248,13 +345,13 @@ describe('mapLegacyConnectionsToCanvasConnections', () => {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '[1/main/1][3/main/0]',
|
id: connectionIdB,
|
||||||
source: '1',
|
source: sourceB,
|
||||||
target: '3',
|
target: targetB,
|
||||||
sourceHandle: 'outputs/main/1',
|
sourceHandle: sourceHandleB,
|
||||||
targetHandle: 'inputs/main/0',
|
targetHandle: targetHandleB,
|
||||||
data: {
|
data: {
|
||||||
fromNodeName: 'Node A',
|
fromNodeName: nodes[0].name,
|
||||||
source: {
|
source: {
|
||||||
index: 1,
|
index: 1,
|
||||||
type: NodeConnectionType.Main,
|
type: NodeConnectionType.Main,
|
||||||
|
@ -312,15 +409,72 @@ describe('mapLegacyConnectionsToCanvasConnections', () => {
|
||||||
nodes,
|
nodes,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const sourceA = nodes[0].id;
|
||||||
|
const sourceHandleA = createCanvasConnectionHandleString({
|
||||||
|
mode: CanvasConnectionMode.Output,
|
||||||
|
type: NodeConnectionType.Main,
|
||||||
|
index: 0,
|
||||||
|
});
|
||||||
|
const targetA = nodes[1].id;
|
||||||
|
const targetHandleA = createCanvasConnectionHandleString({
|
||||||
|
mode: CanvasConnectionMode.Input,
|
||||||
|
type: NodeConnectionType.Main,
|
||||||
|
index: 0,
|
||||||
|
});
|
||||||
|
const connectionIdA = createCanvasConnectionId({
|
||||||
|
source: sourceA,
|
||||||
|
target: targetA,
|
||||||
|
sourceHandle: sourceHandleA,
|
||||||
|
targetHandle: targetHandleA,
|
||||||
|
});
|
||||||
|
|
||||||
|
const sourceB = nodes[0].id;
|
||||||
|
const sourceHandleB = createCanvasConnectionHandleString({
|
||||||
|
mode: CanvasConnectionMode.Output,
|
||||||
|
type: NodeConnectionType.AiMemory,
|
||||||
|
index: 0,
|
||||||
|
});
|
||||||
|
const targetB = nodes[2].id;
|
||||||
|
const targetHandleB = createCanvasConnectionHandleString({
|
||||||
|
mode: CanvasConnectionMode.Input,
|
||||||
|
type: NodeConnectionType.AiMemory,
|
||||||
|
index: 1,
|
||||||
|
});
|
||||||
|
const connectionIdB = createCanvasConnectionId({
|
||||||
|
source: sourceB,
|
||||||
|
target: targetB,
|
||||||
|
sourceHandle: sourceHandleB,
|
||||||
|
targetHandle: targetHandleB,
|
||||||
|
});
|
||||||
|
|
||||||
|
const sourceC = nodes[1].id;
|
||||||
|
const sourceHandleC = createCanvasConnectionHandleString({
|
||||||
|
mode: CanvasConnectionMode.Output,
|
||||||
|
type: NodeConnectionType.Main,
|
||||||
|
index: 0,
|
||||||
|
});
|
||||||
|
const targetC = nodes[2].id;
|
||||||
|
const targetHandleC = createCanvasConnectionHandleString({
|
||||||
|
mode: CanvasConnectionMode.Input,
|
||||||
|
type: NodeConnectionType.Main,
|
||||||
|
index: 0,
|
||||||
|
});
|
||||||
|
const connectionIdC = createCanvasConnectionId({
|
||||||
|
source: sourceC,
|
||||||
|
target: targetC,
|
||||||
|
sourceHandle: sourceHandleC,
|
||||||
|
targetHandle: targetHandleC,
|
||||||
|
});
|
||||||
|
|
||||||
expect(result).toEqual([
|
expect(result).toEqual([
|
||||||
{
|
{
|
||||||
id: '[1/main/0][2/main/0]',
|
id: connectionIdA,
|
||||||
source: '1',
|
source: sourceA,
|
||||||
target: '2',
|
target: targetA,
|
||||||
sourceHandle: 'outputs/main/0',
|
sourceHandle: sourceHandleA,
|
||||||
targetHandle: 'inputs/main/0',
|
targetHandle: targetHandleA,
|
||||||
data: {
|
data: {
|
||||||
fromNodeName: 'Node A',
|
fromNodeName: nodes[0].name,
|
||||||
source: {
|
source: {
|
||||||
index: 0,
|
index: 0,
|
||||||
type: NodeConnectionType.Main,
|
type: NodeConnectionType.Main,
|
||||||
|
@ -332,13 +486,13 @@ describe('mapLegacyConnectionsToCanvasConnections', () => {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: `[1/${NodeConnectionType.AiMemory}/0][3/${NodeConnectionType.AiMemory}/1]`,
|
id: connectionIdB,
|
||||||
source: '1',
|
source: sourceB,
|
||||||
target: '3',
|
target: targetB,
|
||||||
sourceHandle: `outputs/${NodeConnectionType.AiMemory}/0`,
|
sourceHandle: sourceHandleB,
|
||||||
targetHandle: `inputs/${NodeConnectionType.AiMemory}/1`,
|
targetHandle: targetHandleB,
|
||||||
data: {
|
data: {
|
||||||
fromNodeName: 'Node A',
|
fromNodeName: nodes[0].name,
|
||||||
source: {
|
source: {
|
||||||
index: 0,
|
index: 0,
|
||||||
type: NodeConnectionType.AiMemory,
|
type: NodeConnectionType.AiMemory,
|
||||||
|
@ -350,13 +504,13 @@ describe('mapLegacyConnectionsToCanvasConnections', () => {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '[2/main/0][3/main/0]',
|
id: connectionIdC,
|
||||||
source: '2',
|
source: sourceC,
|
||||||
target: '3',
|
target: targetC,
|
||||||
sourceHandle: 'outputs/main/0',
|
sourceHandle: sourceHandleC,
|
||||||
targetHandle: 'inputs/main/0',
|
targetHandle: targetHandleC,
|
||||||
data: {
|
data: {
|
||||||
fromNodeName: 'Node B',
|
fromNodeName: nodes[1].name,
|
||||||
source: {
|
source: {
|
||||||
index: 0,
|
index: 0,
|
||||||
type: NodeConnectionType.Main,
|
type: NodeConnectionType.Main,
|
||||||
|
@ -403,15 +557,36 @@ describe('mapLegacyConnectionsToCanvasConnections', () => {
|
||||||
nodes,
|
nodes,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const source = nodes[0].id;
|
||||||
|
const sourceHandle = createCanvasConnectionHandleString({
|
||||||
|
mode: CanvasConnectionMode.Output,
|
||||||
|
type: NodeConnectionType.Main,
|
||||||
|
index: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
const target = nodes[1].id;
|
||||||
|
const targetHandle = createCanvasConnectionHandleString({
|
||||||
|
mode: CanvasConnectionMode.Input,
|
||||||
|
type: NodeConnectionType.Main,
|
||||||
|
index: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const id = createCanvasConnectionId({
|
||||||
|
source,
|
||||||
|
target,
|
||||||
|
sourceHandle,
|
||||||
|
targetHandle,
|
||||||
|
});
|
||||||
|
|
||||||
expect(result).toEqual([
|
expect(result).toEqual([
|
||||||
{
|
{
|
||||||
id: '[1/main/1][2/main/0]',
|
id,
|
||||||
source: '1',
|
source,
|
||||||
target: '2',
|
target,
|
||||||
sourceHandle: 'outputs/main/1',
|
sourceHandle,
|
||||||
targetHandle: 'inputs/main/0',
|
targetHandle,
|
||||||
data: {
|
data: {
|
||||||
fromNodeName: 'Node A',
|
fromNodeName: nodes[0].name,
|
||||||
source: {
|
source: {
|
||||||
index: 1,
|
index: 1,
|
||||||
type: NodeConnectionType.Main,
|
type: NodeConnectionType.Main,
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
import type { IConnection, IConnections, INodeTypeDescription } from 'n8n-workflow';
|
import type { IConnection, IConnections, INodeTypeDescription } from 'n8n-workflow';
|
||||||
import type { INodeUi } from '@/Interface';
|
import type { INodeUi } from '@/Interface';
|
||||||
import type { CanvasConnection, CanvasConnectionPortType, CanvasConnectionPort } from '@/types';
|
import type { CanvasConnection, CanvasConnectionPort } from '@/types';
|
||||||
import { CanvasConnectionMode } from '@/types';
|
import { CanvasConnectionMode } from '@/types';
|
||||||
import type { Connection } from '@vue-flow/core';
|
import type { Connection } from '@vue-flow/core';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
import { isValidCanvasConnectionMode, isValidNodeConnectionType } from '@/utils/typeGuards';
|
import { isValidCanvasConnectionMode, isValidNodeConnectionType } from '@/utils/typeGuards';
|
||||||
import { NodeConnectionType } from 'n8n-workflow';
|
import { NodeConnectionType } from 'n8n-workflow';
|
||||||
|
import type { Connection as VueFlowConnection } from '@vue-flow/core/dist/types/connection';
|
||||||
|
import { PUSH_NODES_OFFSET } from '@/utils/nodeViewUtils';
|
||||||
|
|
||||||
export function mapLegacyConnectionsToCanvasConnections(
|
export function mapLegacyConnectionsToCanvasConnections(
|
||||||
legacyConnections: IConnections,
|
legacyConnections: IConnections,
|
||||||
|
@ -14,33 +16,54 @@ export function mapLegacyConnectionsToCanvasConnections(
|
||||||
const mappedConnections: CanvasConnection[] = [];
|
const mappedConnections: CanvasConnection[] = [];
|
||||||
|
|
||||||
Object.keys(legacyConnections).forEach((fromNodeName) => {
|
Object.keys(legacyConnections).forEach((fromNodeName) => {
|
||||||
const fromId = nodes.find((node) => node.name === fromNodeName)?.id;
|
const fromId = nodes.find((node) => node.name === fromNodeName)?.id ?? '';
|
||||||
const fromConnectionTypes = Object.keys(legacyConnections[fromNodeName]);
|
const fromConnectionTypes = Object.keys(
|
||||||
|
legacyConnections[fromNodeName],
|
||||||
|
) as NodeConnectionType[];
|
||||||
|
|
||||||
fromConnectionTypes.forEach((fromConnectionType) => {
|
fromConnectionTypes.forEach((fromConnectionType) => {
|
||||||
const fromPorts = legacyConnections[fromNodeName][fromConnectionType];
|
const fromPorts = legacyConnections[fromNodeName][fromConnectionType];
|
||||||
fromPorts.forEach((toPorts, fromIndex) => {
|
fromPorts.forEach((toPorts, fromIndex) => {
|
||||||
toPorts.forEach((toPort) => {
|
toPorts.forEach((toPort) => {
|
||||||
const toId = nodes.find((node) => node.name === toPort.node)?.id;
|
const toId = nodes.find((node) => node.name === toPort.node)?.id ?? '';
|
||||||
const toConnectionType = toPort.type;
|
const toConnectionType = toPort.type as NodeConnectionType;
|
||||||
const toIndex = toPort.index;
|
const toIndex = toPort.index;
|
||||||
|
|
||||||
|
const sourceHandle = createCanvasConnectionHandleString({
|
||||||
|
mode: CanvasConnectionMode.Output,
|
||||||
|
type: fromConnectionType,
|
||||||
|
index: fromIndex,
|
||||||
|
});
|
||||||
|
|
||||||
|
const targetHandle = createCanvasConnectionHandleString({
|
||||||
|
mode: CanvasConnectionMode.Input,
|
||||||
|
type: toConnectionType,
|
||||||
|
index: toIndex,
|
||||||
|
});
|
||||||
|
|
||||||
|
const connectionId = createCanvasConnectionId({
|
||||||
|
source: fromId,
|
||||||
|
sourceHandle,
|
||||||
|
target: toId,
|
||||||
|
targetHandle,
|
||||||
|
});
|
||||||
|
|
||||||
if (fromId && toId) {
|
if (fromId && toId) {
|
||||||
mappedConnections.push({
|
mappedConnections.push({
|
||||||
id: `[${fromId}/${fromConnectionType}/${fromIndex}][${toId}/${toConnectionType}/${toIndex}]`,
|
id: connectionId,
|
||||||
source: fromId,
|
source: fromId,
|
||||||
target: toId,
|
target: toId,
|
||||||
sourceHandle: `${CanvasConnectionMode.Output}/${fromConnectionType}/${fromIndex}`,
|
sourceHandle,
|
||||||
targetHandle: `${CanvasConnectionMode.Input}/${toConnectionType}/${toIndex}`,
|
targetHandle,
|
||||||
data: {
|
data: {
|
||||||
fromNodeName,
|
fromNodeName,
|
||||||
source: {
|
source: {
|
||||||
index: fromIndex,
|
index: fromIndex,
|
||||||
type: fromConnectionType as CanvasConnectionPortType,
|
type: fromConnectionType,
|
||||||
},
|
},
|
||||||
target: {
|
target: {
|
||||||
index: toIndex,
|
index: toIndex,
|
||||||
type: toConnectionType as CanvasConnectionPortType,
|
type: toConnectionType,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -71,6 +94,21 @@ export function parseCanvasConnectionHandleString(handle: string | null | undefi
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the width and height of a connection
|
||||||
|
*
|
||||||
|
* @TODO See whether this is actually needed or just a legacy jsPlumb check
|
||||||
|
*/
|
||||||
|
export function getVueFlowConnectorLengths(connection: VueFlowConnection): [number, number] {
|
||||||
|
const connectionId = createCanvasConnectionId(connection);
|
||||||
|
const edgeRef = document.getElementById(connectionId);
|
||||||
|
if (!edgeRef) {
|
||||||
|
return [PUSH_NODES_OFFSET, PUSH_NODES_OFFSET];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [edgeRef.clientWidth, edgeRef.clientHeight];
|
||||||
|
}
|
||||||
|
|
||||||
export function createCanvasConnectionHandleString({
|
export function createCanvasConnectionHandleString({
|
||||||
mode,
|
mode,
|
||||||
type = NodeConnectionType.Main,
|
type = NodeConnectionType.Main,
|
||||||
|
@ -83,6 +121,10 @@ export function createCanvasConnectionHandleString({
|
||||||
return `${mode}/${type}/${index}`;
|
return `${mode}/${type}/${index}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function createCanvasConnectionId(connection: Connection) {
|
||||||
|
return `[${connection.source}/${connection.sourceHandle}][${connection.target}/${connection.targetHandle}]`;
|
||||||
|
}
|
||||||
|
|
||||||
export function mapCanvasConnectionToLegacyConnection(
|
export function mapCanvasConnectionToLegacyConnection(
|
||||||
sourceNode: INodeUi,
|
sourceNode: INodeUi,
|
||||||
targetNode: INodeUi,
|
targetNode: INodeUi,
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { nodeConnectionTypes } from 'n8n-workflow';
|
||||||
import type { IExecutionResponse, ICredentialsResponse, NewCredentialsModal } from '@/Interface';
|
import type { IExecutionResponse, ICredentialsResponse, NewCredentialsModal } from '@/Interface';
|
||||||
import type { jsPlumbDOMElement } from '@jsplumb/browser-ui';
|
import type { jsPlumbDOMElement } from '@jsplumb/browser-ui';
|
||||||
import type { Connection } from '@jsplumb/core';
|
import type { Connection } from '@jsplumb/core';
|
||||||
|
import type { Connection as VueFlowConnection } from '@vue-flow/core';
|
||||||
import type { RouteLocationRaw } from 'vue-router';
|
import type { RouteLocationRaw } from 'vue-router';
|
||||||
import type { CanvasConnectionMode } from '@/types';
|
import type { CanvasConnectionMode } from '@/types';
|
||||||
import { canvasConnectionModes } from '@/types';
|
import { canvasConnectionModes } from '@/types';
|
||||||
|
@ -66,7 +67,7 @@ export function isDateObject(date: unknown): date is Date {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isValidNodeConnectionType(
|
export function isValidNodeConnectionType(
|
||||||
connectionType: string,
|
connectionType: string | undefined,
|
||||||
): connectionType is NodeConnectionType {
|
): connectionType is NodeConnectionType {
|
||||||
return nodeConnectionTypes.includes(connectionType as NodeConnectionType);
|
return nodeConnectionTypes.includes(connectionType as NodeConnectionType);
|
||||||
}
|
}
|
||||||
|
@ -75,6 +76,15 @@ export function isValidCanvasConnectionMode(mode: string): mode is CanvasConnect
|
||||||
return canvasConnectionModes.includes(mode as CanvasConnectionMode);
|
return canvasConnectionModes.includes(mode as CanvasConnectionMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isVueFlowConnection(connection: object): connection is VueFlowConnection {
|
||||||
|
return (
|
||||||
|
'source' in connection &&
|
||||||
|
'target' in connection &&
|
||||||
|
'sourceHandle' in connection &&
|
||||||
|
'targetHandle' in connection
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function isTriggerPanelObject(
|
export function isTriggerPanelObject(
|
||||||
triggerPanel: INodeTypeDescription['triggerPanel'],
|
triggerPanel: INodeTypeDescription['triggerPanel'],
|
||||||
): triggerPanel is TriggerPanelDefinition {
|
): triggerPanel is TriggerPanelDefinition {
|
||||||
|
|
|
@ -80,7 +80,6 @@ import { useTagsStore } from '@/stores/tags.store';
|
||||||
import { usePushConnectionStore } from '@/stores/pushConnection.store';
|
import { usePushConnectionStore } from '@/stores/pushConnection.store';
|
||||||
import { useNDVStore } from '@/stores/ndv.store';
|
import { useNDVStore } from '@/stores/ndv.store';
|
||||||
import { getNodeViewTab } from '@/utils/canvasUtils';
|
import { getNodeViewTab } from '@/utils/canvasUtils';
|
||||||
import { parseCanvasConnectionHandleString } from '@/utils/canvasUtilsV2';
|
|
||||||
import CanvasStopCurrentExecutionButton from '@/components/canvas/elements/buttons/CanvasStopCurrentExecutionButton.vue';
|
import CanvasStopCurrentExecutionButton from '@/components/canvas/elements/buttons/CanvasStopCurrentExecutionButton.vue';
|
||||||
import CanvasStopWaitingForWebhookButton from '@/components/canvas/elements/buttons/CanvasStopWaitingForWebhookButton.vue';
|
import CanvasStopWaitingForWebhookButton from '@/components/canvas/elements/buttons/CanvasStopWaitingForWebhookButton.vue';
|
||||||
import CanvasClearExecutionDataButton from '@/components/canvas/elements/buttons/CanvasClearExecutionDataButton.vue';
|
import CanvasClearExecutionDataButton from '@/components/canvas/elements/buttons/CanvasClearExecutionDataButton.vue';
|
||||||
|
@ -529,14 +528,13 @@ function onCreateConnection(connection: Connection) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function onCreateConnectionCancelled(event: ConnectStartEvent) {
|
function onCreateConnectionCancelled(event: ConnectStartEvent) {
|
||||||
const { type, index } = parseCanvasConnectionHandleString(event.handleId);
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
nodeCreatorStore.openNodeCreatorForConnectingNode({
|
nodeCreatorStore.openNodeCreatorForConnectingNode({
|
||||||
index,
|
connection: {
|
||||||
endpointUuid: event.handleId,
|
source: event.nodeId,
|
||||||
|
sourceHandle: event.handleId,
|
||||||
|
},
|
||||||
eventSource: NODE_CREATOR_OPEN_SOURCES.NODE_CONNECTION_DROP,
|
eventSource: NODE_CREATOR_OPEN_SOURCES.NODE_CONNECTION_DROP,
|
||||||
outputType: type,
|
|
||||||
sourceId: event.nodeId,
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -574,6 +572,8 @@ async function onAddNodesAndConnections(
|
||||||
await addConnections(connections, {
|
await addConnections(connections, {
|
||||||
offsetIndex: editableWorkflow.value.nodes.length - nodes.length,
|
offsetIndex: editableWorkflow.value.nodes.length - nodes.length,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
uiStore.lastSelectedNodeConnection = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onSwitchActiveNode(nodeName: string) {
|
async function onSwitchActiveNode(nodeName: string) {
|
||||||
|
@ -588,6 +588,13 @@ function onOpenNodeCreator(options: ToggleNodeCreatorOptions) {
|
||||||
nodeCreatorStore.openNodeCreator(options);
|
nodeCreatorStore.openNodeCreator(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onClickConnectionAdd(connection: Connection) {
|
||||||
|
nodeCreatorStore.openNodeCreatorForConnectingNode({
|
||||||
|
connection,
|
||||||
|
eventSource: NODE_CREATOR_OPEN_SOURCES.NODE_CONNECTION_ACTION,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Executions
|
* Executions
|
||||||
*/
|
*/
|
||||||
|
@ -1079,6 +1086,7 @@ onBeforeUnmount(() => {
|
||||||
@create:connection="onCreateConnection"
|
@create:connection="onCreateConnection"
|
||||||
@create:connection:cancelled="onCreateConnectionCancelled"
|
@create:connection:cancelled="onCreateConnectionCancelled"
|
||||||
@delete:connection="onDeleteConnection"
|
@delete:connection="onDeleteConnection"
|
||||||
|
@click:connection:add="onClickConnectionAdd"
|
||||||
@click:pane="onClickPane"
|
@click:pane="onClickPane"
|
||||||
>
|
>
|
||||||
<div :class="$style.executionButtons">
|
<div :class="$style.executionButtons">
|
||||||
|
|
Loading…
Reference in a new issue