mirror of
https://github.com/n8n-io/n8n.git
synced 2025-01-12 13:27:31 -08:00
feat(editor): Overhaul node insert position computation in new canvas (no-changelog) (#10637)
This commit is contained in:
parent
e5aba60aff
commit
32ce65c1af
|
@ -1805,6 +1805,7 @@ export type ToggleNodeCreatorOptions = {
|
||||||
createNodeActive: boolean;
|
createNodeActive: boolean;
|
||||||
source?: NodeCreatorOpenSource;
|
source?: NodeCreatorOpenSource;
|
||||||
nodeCreatorView?: NodeFilterType;
|
nodeCreatorView?: NodeFilterType;
|
||||||
|
hasAddedNodes?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AppliedThemeOption = 'light' | 'dark';
|
export type AppliedThemeOption = 'light' | 'dark';
|
||||||
|
|
|
@ -49,18 +49,18 @@ export const mockNode = ({
|
||||||
}) => mock<INodeUi>({ id, name, type, position, disabled, issues, typeVersion, parameters });
|
}) => mock<INodeUi>({ id, name, type, position, disabled, issues, typeVersion, parameters });
|
||||||
|
|
||||||
export const mockNodeTypeDescription = ({
|
export const mockNodeTypeDescription = ({
|
||||||
name,
|
name = SET_NODE_TYPE,
|
||||||
version = 1,
|
version = 1,
|
||||||
credentials = [],
|
credentials = [],
|
||||||
inputs = [NodeConnectionType.Main],
|
inputs = [NodeConnectionType.Main],
|
||||||
outputs = [NodeConnectionType.Main],
|
outputs = [NodeConnectionType.Main],
|
||||||
}: {
|
}: {
|
||||||
name: INodeTypeDescription['name'];
|
name?: INodeTypeDescription['name'];
|
||||||
version?: INodeTypeDescription['version'];
|
version?: INodeTypeDescription['version'];
|
||||||
credentials?: INodeTypeDescription['credentials'];
|
credentials?: INodeTypeDescription['credentials'];
|
||||||
inputs?: INodeTypeDescription['inputs'];
|
inputs?: INodeTypeDescription['inputs'];
|
||||||
outputs?: INodeTypeDescription['outputs'];
|
outputs?: INodeTypeDescription['outputs'];
|
||||||
}) =>
|
} = {}) =>
|
||||||
mock<INodeTypeDescription>({
|
mock<INodeTypeDescription>({
|
||||||
name,
|
name,
|
||||||
displayName: name,
|
displayName: name,
|
||||||
|
|
|
@ -23,7 +23,7 @@ const LazyNodeCreator = defineAsyncComponent(
|
||||||
);
|
);
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
createNodeActive: false,
|
createNodeActive: false, // Determines if the node creator is open
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
@ -88,13 +88,15 @@ function addStickyNote() {
|
||||||
emit('addNodes', getAddedNodesAndConnections([{ type: STICKY_NODE_TYPE, position }]));
|
emit('addNodes', getAddedNodesAndConnections([{ type: STICKY_NODE_TYPE, position }]));
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeNodeCreator() {
|
function closeNodeCreator(hasAddedNodes = false) {
|
||||||
emit('toggleNodeCreator', { createNodeActive: false });
|
if (props.createNodeActive) {
|
||||||
|
emit('toggleNodeCreator', { createNodeActive: false, hasAddedNodes });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function nodeTypeSelected(nodeTypes: string[]) {
|
function nodeTypeSelected(nodeTypes: string[]) {
|
||||||
emit('addNodes', getAddedNodesAndConnections(nodeTypes.map((type) => ({ type }))));
|
emit('addNodes', getAddedNodesAndConnections(nodeTypes.map((type) => ({ type }))));
|
||||||
closeNodeCreator();
|
closeNodeCreator(true);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -101,8 +101,8 @@ watch(
|
||||||
);
|
);
|
||||||
|
|
||||||
// Close node creator when the last view stacks is closed
|
// Close node creator when the last view stacks is closed
|
||||||
watch(viewStacksLength, (viewStacksLength) => {
|
watch(viewStacksLength, (value) => {
|
||||||
if (viewStacksLength === 0) {
|
if (value === 0) {
|
||||||
emit('closeNodeCreator');
|
emit('closeNodeCreator');
|
||||||
setShowScrim(false);
|
setShowScrim(false);
|
||||||
}
|
}
|
||||||
|
|
|
@ -57,7 +57,11 @@ const emit = defineEmits<{
|
||||||
'create:connection:start': [handle: ConnectStartEvent];
|
'create:connection:start': [handle: ConnectStartEvent];
|
||||||
'create:connection': [connection: Connection];
|
'create:connection': [connection: Connection];
|
||||||
'create:connection:end': [connection: Connection, event?: MouseEvent];
|
'create:connection:end': [connection: Connection, event?: MouseEvent];
|
||||||
'create:connection:cancelled': [handle: ConnectStartEvent, event?: MouseEvent];
|
'create:connection:cancelled': [
|
||||||
|
handle: ConnectStartEvent,
|
||||||
|
position: XYPosition,
|
||||||
|
event?: MouseEvent,
|
||||||
|
];
|
||||||
'click:connection:add': [connection: Connection];
|
'click:connection:add': [connection: Connection];
|
||||||
'click:pane': [position: XYPosition];
|
'click:pane': [position: XYPosition];
|
||||||
'run:workflow': [];
|
'run:workflow': [];
|
||||||
|
@ -227,7 +231,7 @@ function onConnectEnd(event?: MouseEvent) {
|
||||||
if (connectedHandle.value) {
|
if (connectedHandle.value) {
|
||||||
emit('create:connection:end', connectedHandle.value, event);
|
emit('create:connection:end', connectedHandle.value, event);
|
||||||
} else if (connectingHandle.value) {
|
} else if (connectingHandle.value) {
|
||||||
emit('create:connection:cancelled', connectingHandle.value, event);
|
emit('create:connection:cancelled', connectingHandle.value, getProjectedPosition(event), event);
|
||||||
}
|
}
|
||||||
|
|
||||||
connectedHandle.value = undefined;
|
connectedHandle.value = undefined;
|
||||||
|
@ -291,14 +295,19 @@ function emitWithLastSelectedNode(emitFn: (id: string) => void) {
|
||||||
const defaultZoom = 1;
|
const defaultZoom = 1;
|
||||||
const zoom = ref(defaultZoom);
|
const zoom = ref(defaultZoom);
|
||||||
|
|
||||||
function onClickPane(event: MouseEvent) {
|
function getProjectedPosition(event?: MouseEvent) {
|
||||||
const bounds = viewportRef.value?.getBoundingClientRect() ?? { left: 0, top: 0 };
|
const bounds = viewportRef.value?.getBoundingClientRect() ?? { left: 0, top: 0 };
|
||||||
const position = project({
|
const offsetX = event?.clientX ?? 0;
|
||||||
x: event.offsetX - bounds.left,
|
const offsetY = event?.clientY ?? 0;
|
||||||
y: event.offsetY - bounds.top,
|
|
||||||
});
|
|
||||||
|
|
||||||
emit('click:pane', position);
|
return project({
|
||||||
|
x: offsetX - bounds.left,
|
||||||
|
y: offsetY - bounds.top,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function onClickPane(event: MouseEvent) {
|
||||||
|
emit('click:pane', getProjectedPosition(event));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onFitView() {
|
async function onFitView() {
|
||||||
|
|
|
@ -145,8 +145,8 @@ function openContextMenu(event: MouseEvent) {
|
||||||
*/
|
*/
|
||||||
|
|
||||||
&.configuration {
|
&.configuration {
|
||||||
--canvas-node--width: 76px;
|
--canvas-node--width: 80px;
|
||||||
--canvas-node--height: 76px;
|
--canvas-node--height: 80px;
|
||||||
|
|
||||||
background: var(--canvas-node--background, var(--node-type-supplemental-background));
|
background: var(--canvas-node--background, var(--node-type-supplemental-background));
|
||||||
border: var(--canvas-node-border-width) solid
|
border: var(--canvas-node-border-width) solid
|
||||||
|
|
|
@ -9,7 +9,7 @@ exports[`useCanvasOperations > copyNodes > should copy nodes 1`] = `
|
||||||
"parameters": {},
|
"parameters": {},
|
||||||
"id": "1",
|
"id": "1",
|
||||||
"name": "Node 1",
|
"name": "Node 1",
|
||||||
"type": "type",
|
"type": "n8n-nodes-base.set",
|
||||||
"position": [
|
"position": [
|
||||||
40,
|
40,
|
||||||
40
|
40
|
||||||
|
@ -20,7 +20,7 @@ exports[`useCanvasOperations > copyNodes > should copy nodes 1`] = `
|
||||||
"parameters": {},
|
"parameters": {},
|
||||||
"id": "2",
|
"id": "2",
|
||||||
"name": "Node 2",
|
"name": "Node 2",
|
||||||
"type": "type",
|
"type": "n8n-nodes-base.set",
|
||||||
"position": [
|
"position": [
|
||||||
40,
|
40,
|
||||||
40
|
40
|
||||||
|
@ -44,7 +44,7 @@ exports[`useCanvasOperations > cutNodes > should copy and delete nodes 1`] = `
|
||||||
"parameters": {},
|
"parameters": {},
|
||||||
"id": "1",
|
"id": "1",
|
||||||
"name": "Node 1",
|
"name": "Node 1",
|
||||||
"type": "type",
|
"type": "n8n-nodes-base.set",
|
||||||
"position": [
|
"position": [
|
||||||
40,
|
40,
|
||||||
40
|
40
|
||||||
|
@ -55,7 +55,7 @@ exports[`useCanvasOperations > cutNodes > should copy and delete nodes 1`] = `
|
||||||
"parameters": {},
|
"parameters": {},
|
||||||
"id": "2",
|
"id": "2",
|
||||||
"name": "Node 2",
|
"name": "Node 2",
|
||||||
"type": "type",
|
"type": "n8n-nodes-base.set",
|
||||||
"position": [
|
"position": [
|
||||||
40,
|
40,
|
||||||
40
|
40
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -17,7 +17,7 @@ import { useDataSchema } from '@/composables/useDataSchema';
|
||||||
import { useExternalHooks } from '@/composables/useExternalHooks';
|
import { useExternalHooks } from '@/composables/useExternalHooks';
|
||||||
import { useI18n } from '@/composables/useI18n';
|
import { useI18n } from '@/composables/useI18n';
|
||||||
import { useNodeHelpers } from '@/composables/useNodeHelpers';
|
import { useNodeHelpers } from '@/composables/useNodeHelpers';
|
||||||
import { usePinnedData, type PinDataSource } from '@/composables/usePinnedData';
|
import { type PinDataSource, usePinnedData } from '@/composables/usePinnedData';
|
||||||
import { useTelemetry } from '@/composables/useTelemetry';
|
import { useTelemetry } from '@/composables/useTelemetry';
|
||||||
import { useToast } from '@/composables/useToast';
|
import { useToast } from '@/composables/useToast';
|
||||||
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
|
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
|
||||||
|
@ -65,6 +65,13 @@ import {
|
||||||
parseCanvasConnectionHandleString,
|
parseCanvasConnectionHandleString,
|
||||||
} from '@/utils/canvasUtilsV2';
|
} from '@/utils/canvasUtilsV2';
|
||||||
import * as NodeViewUtils from '@/utils/nodeViewUtils';
|
import * as NodeViewUtils from '@/utils/nodeViewUtils';
|
||||||
|
import {
|
||||||
|
CONFIGURABLE_NODE_SIZE,
|
||||||
|
CONFIGURATION_NODE_SIZE,
|
||||||
|
DEFAULT_NODE_SIZE,
|
||||||
|
GRID_SIZE,
|
||||||
|
PUSH_NODES_OFFSET,
|
||||||
|
} from '@/utils/nodeViewUtils';
|
||||||
import { isValidNodeConnectionType } from '@/utils/typeGuards';
|
import { isValidNodeConnectionType } from '@/utils/typeGuards';
|
||||||
import type { Connection } from '@vue-flow/core';
|
import type { Connection } from '@vue-flow/core';
|
||||||
import type {
|
import type {
|
||||||
|
@ -358,6 +365,7 @@ export function useCanvasOperations({ router }: { router: ReturnType<typeof useR
|
||||||
|
|
||||||
function setNodeSelected(id?: string) {
|
function setNodeSelected(id?: string) {
|
||||||
if (!id) {
|
if (!id) {
|
||||||
|
uiStore.lastInteractedWithNodeId = null;
|
||||||
uiStore.lastSelectedNode = '';
|
uiStore.lastSelectedNode = '';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -367,6 +375,7 @@ export function useCanvasOperations({ router }: { router: ReturnType<typeof useR
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
uiStore.lastInteractedWithNodeId = id;
|
||||||
uiStore.lastSelectedNode = node.name;
|
uiStore.lastSelectedNode = node.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -834,73 +843,91 @@ export function useCanvasOperations({ router }: { router: ReturnType<typeof useR
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveNodePosition(node: INodeUi, nodeTypeDescription: INodeTypeDescription) {
|
function resolveNodePosition(
|
||||||
if (node.position) {
|
node: Omit<INodeUi, 'position'> & { position?: INodeUi['position'] },
|
||||||
return NodeViewUtils.getNewNodePosition(
|
nodeTypeDescription: INodeTypeDescription,
|
||||||
canvasStore.getNodesWithPlaceholderNode(),
|
) {
|
||||||
node.position,
|
let position: XYPosition | undefined = node.position;
|
||||||
);
|
let pushOffsets: XYPosition = [40, 40];
|
||||||
}
|
|
||||||
|
|
||||||
|
// Available when
|
||||||
|
// - clicking the plus button of a node handle
|
||||||
|
// - dragging an edge / connection of a node handle
|
||||||
|
// - selecting a node, adding a node via the node creator
|
||||||
const lastInteractedWithNode = uiStore.lastInteractedWithNode;
|
const lastInteractedWithNode = uiStore.lastInteractedWithNode;
|
||||||
|
// Available when clicking the plus button of a node edge / connection
|
||||||
const lastInteractedWithNodeConnection = uiStore.lastInteractedWithNodeConnection;
|
const lastInteractedWithNodeConnection = uiStore.lastInteractedWithNodeConnection;
|
||||||
|
// Available when dragging an edge / connection from a node
|
||||||
|
const lastInteractedWithNodeHandle = uiStore.lastInteractedWithNodeHandle;
|
||||||
|
|
||||||
|
const { type: connectionType, index: connectionIndex } = parseCanvasConnectionHandleString(
|
||||||
|
lastInteractedWithNodeHandle ?? lastInteractedWithNodeConnection?.sourceHandle ?? '',
|
||||||
|
);
|
||||||
|
|
||||||
|
const nodeSize =
|
||||||
|
connectionType === NodeConnectionType.Main ? DEFAULT_NODE_SIZE : CONFIGURATION_NODE_SIZE;
|
||||||
|
|
||||||
if (lastInteractedWithNode) {
|
if (lastInteractedWithNode) {
|
||||||
const lastSelectedNodeTypeDescription = nodeTypesStore.getNodeType(
|
const lastInteractedWithNodeTypeDescription = nodeTypesStore.getNodeType(
|
||||||
lastInteractedWithNode.type,
|
lastInteractedWithNode.type,
|
||||||
lastInteractedWithNode.typeVersion,
|
lastInteractedWithNode.typeVersion,
|
||||||
);
|
);
|
||||||
const lastInteractedWithNodeObject = editableWorkflowObject.value.getNode(
|
|
||||||
lastInteractedWithNode.name,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (lastInteractedWithNodeConnection) {
|
const newNodeInsertPosition = uiStore.lastCancelledConnectionPosition;
|
||||||
shiftDownstreamNodesPosition(lastInteractedWithNode.name, NodeViewUtils.PUSH_NODES_OFFSET, {
|
|
||||||
trackHistory: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// This position is set in `onMouseUp` when pulling connections
|
|
||||||
const newNodeInsertPosition = canvasStore.newNodeInsertPosition;
|
|
||||||
if (newNodeInsertPosition) {
|
if (newNodeInsertPosition) {
|
||||||
canvasStore.newNodeInsertPosition = null;
|
// When pulling / cancelling a connection.
|
||||||
return NodeViewUtils.getNewNodePosition(workflowsStore.allNodes, [
|
// The new node should be placed at the same position as the mouse up event,
|
||||||
newNodeInsertPosition[0] + NodeViewUtils.GRID_SIZE,
|
// designated by the `newNodeInsertPosition` value.
|
||||||
newNodeInsertPosition[1] - NodeViewUtils.NODE_SIZE / 2,
|
|
||||||
]);
|
|
||||||
} else {
|
|
||||||
let yOffset = 0;
|
|
||||||
|
|
||||||
// Compute the y offset for the new node based on the number of main outputs of the source node
|
const xOffset = connectionType === NodeConnectionType.Main ? 0 : -nodeSize[0] / 2;
|
||||||
if (uiStore.lastInteractedWithNodeConnection) {
|
const yOffset = connectionType === NodeConnectionType.Main ? -nodeSize[1] / 2 : 0;
|
||||||
const sourceNodeType = nodeTypesStore.getNodeType(
|
|
||||||
lastInteractedWithNode.type,
|
position = [newNodeInsertPosition[0] + xOffset, newNodeInsertPosition[1] + yOffset];
|
||||||
lastInteractedWithNode.typeVersion,
|
|
||||||
|
uiStore.lastCancelledConnectionPosition = null;
|
||||||
|
} else if (lastInteractedWithNodeTypeDescription) {
|
||||||
|
// When
|
||||||
|
// - clicking the plus button of a node handle
|
||||||
|
// - clicking the plus button of a node edge / connection
|
||||||
|
// - selecting a node, adding a node via the node creator
|
||||||
|
|
||||||
|
let yOffset = 0;
|
||||||
|
if (lastInteractedWithNodeConnection) {
|
||||||
|
// When clicking the plus button of a node edge / connection
|
||||||
|
// Compute the y offset for the new node based on the number of main outputs of the source node
|
||||||
|
// and shift the downstream nodes accordingly
|
||||||
|
|
||||||
|
shiftDownstreamNodesPosition(lastInteractedWithNode.name, PUSH_NODES_OFFSET, {
|
||||||
|
trackHistory: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const yOffsetValuesByOutputCount = [
|
||||||
|
[-nodeSize[1], nodeSize[1]],
|
||||||
|
[-nodeSize[1] - 2 * GRID_SIZE, 0, nodeSize[1] - 2 * GRID_SIZE],
|
||||||
|
[
|
||||||
|
-2 * nodeSize[1] - 2 * GRID_SIZE,
|
||||||
|
-nodeSize[1],
|
||||||
|
nodeSize[1],
|
||||||
|
2 * nodeSize[1] - 2 * GRID_SIZE,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
const lastInteractedWithNodeOutputs = NodeHelpers.getNodeOutputs(
|
||||||
|
editableWorkflowObject.value,
|
||||||
|
lastInteractedWithNode,
|
||||||
|
lastInteractedWithNodeTypeDescription,
|
||||||
|
);
|
||||||
|
const lastInteractedWithNodeOutputTypes = NodeHelpers.getConnectionTypes(
|
||||||
|
lastInteractedWithNodeOutputs,
|
||||||
|
);
|
||||||
|
const lastInteractedWithNodeMainOutputs = lastInteractedWithNodeOutputTypes.filter(
|
||||||
|
(output) => output === NodeConnectionType.Main,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (sourceNodeType) {
|
if (lastInteractedWithNodeMainOutputs.length > 1) {
|
||||||
const offsets = [
|
const yOffsetValues =
|
||||||
[-100, 100],
|
yOffsetValuesByOutputCount[lastInteractedWithNodeMainOutputs.length - 2];
|
||||||
[-140, 0, 140],
|
yOffset = yOffsetValues[connectionIndex];
|
||||||
[-240, -100, 100, 240],
|
|
||||||
];
|
|
||||||
|
|
||||||
const sourceNodeOutputs = NodeHelpers.getNodeOutputs(
|
|
||||||
editableWorkflowObject.value,
|
|
||||||
lastInteractedWithNode,
|
|
||||||
sourceNodeType,
|
|
||||||
);
|
|
||||||
const sourceNodeOutputTypes = NodeHelpers.getConnectionTypes(sourceNodeOutputs);
|
|
||||||
const sourceNodeOutputMainOutputs = sourceNodeOutputTypes.filter(
|
|
||||||
(output) => output === NodeConnectionType.Main,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (sourceNodeOutputMainOutputs.length > 1) {
|
|
||||||
const { index: sourceOutputIndex } = parseCanvasConnectionHandleString(
|
|
||||||
uiStore.lastInteractedWithNodeConnection.sourceHandle,
|
|
||||||
);
|
|
||||||
const offset = offsets[sourceNodeOutputMainOutputs.length - 2];
|
|
||||||
yOffset = offset[sourceOutputIndex];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -913,80 +940,96 @@ export function useCanvasOperations({ router }: { router: ReturnType<typeof useR
|
||||||
// with only "main" outputs.
|
// with only "main" outputs.
|
||||||
outputs = NodeHelpers.getNodeOutputs(
|
outputs = NodeHelpers.getNodeOutputs(
|
||||||
editableWorkflowObject.value,
|
editableWorkflowObject.value,
|
||||||
node,
|
node as INode,
|
||||||
nodeTypeDescription,
|
nodeTypeDescription,
|
||||||
);
|
);
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
const outputTypes = NodeHelpers.getConnectionTypes(outputs);
|
const outputTypes = NodeHelpers.getConnectionTypes(outputs);
|
||||||
|
const lastInteractedWithNodeObject = editableWorkflowObject.value.getNode(
|
||||||
|
lastInteractedWithNode.name,
|
||||||
|
);
|
||||||
|
|
||||||
// If node has only scoped outputs, position it below the last selected node
|
pushOffsets = [100, 0];
|
||||||
if (lastSelectedNodeTypeDescription) {
|
|
||||||
|
if (
|
||||||
|
outputTypes.length > 0 &&
|
||||||
|
outputTypes.every((outputName) => outputName !== NodeConnectionType.Main) &&
|
||||||
|
lastInteractedWithNodeObject
|
||||||
|
) {
|
||||||
|
// When the added node has only non-main outputs (configuration nodes)
|
||||||
|
// We want to place the new node directly below the last interacted with node.
|
||||||
|
|
||||||
|
const lastInteractedWithNodeInputs = NodeHelpers.getNodeInputs(
|
||||||
|
editableWorkflowObject.value,
|
||||||
|
lastInteractedWithNodeObject,
|
||||||
|
lastInteractedWithNodeTypeDescription,
|
||||||
|
);
|
||||||
|
const lastInteractedWithNodeInputTypes = NodeHelpers.getConnectionTypes(
|
||||||
|
lastInteractedWithNodeInputs,
|
||||||
|
);
|
||||||
|
const lastInteractedWithNodeScopedInputTypes = (
|
||||||
|
lastInteractedWithNodeInputTypes || []
|
||||||
|
).filter((input) => input !== NodeConnectionType.Main);
|
||||||
|
const scopedConnectionIndex = lastInteractedWithNodeScopedInputTypes.findIndex(
|
||||||
|
(inputType) => outputs[0] === inputType,
|
||||||
|
);
|
||||||
|
|
||||||
|
const lastInteractedWithNodeWidthDivisions = Math.max(
|
||||||
|
lastInteractedWithNodeScopedInputTypes.length + 1,
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
|
||||||
|
position = [
|
||||||
|
lastInteractedWithNode.position[0] +
|
||||||
|
(CONFIGURABLE_NODE_SIZE[0] / lastInteractedWithNodeWidthDivisions) *
|
||||||
|
(scopedConnectionIndex + 1) -
|
||||||
|
nodeSize[0] / 2,
|
||||||
|
lastInteractedWithNode.position[1] + PUSH_NODES_OFFSET,
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
// When the node has only main outputs, mixed outputs, or no outputs at all
|
||||||
|
// We want to place the new node directly to the right of the last interacted with node.
|
||||||
|
|
||||||
|
const lastInteractedWithNodeInputs = NodeHelpers.getNodeInputs(
|
||||||
|
editableWorkflowObject.value,
|
||||||
|
lastInteractedWithNode,
|
||||||
|
lastInteractedWithNodeTypeDescription,
|
||||||
|
);
|
||||||
|
const lastInteractedWithNodeInputTypes = NodeHelpers.getConnectionTypes(
|
||||||
|
lastInteractedWithNodeInputs,
|
||||||
|
);
|
||||||
|
|
||||||
|
let pushOffset = PUSH_NODES_OFFSET;
|
||||||
if (
|
if (
|
||||||
lastInteractedWithNodeObject &&
|
!!lastInteractedWithNodeInputTypes.find((input) => input !== NodeConnectionType.Main)
|
||||||
outputTypes.length > 0 &&
|
|
||||||
outputTypes.every((outputName) => outputName !== NodeConnectionType.Main)
|
|
||||||
) {
|
) {
|
||||||
const lastSelectedInputs = NodeHelpers.getNodeInputs(
|
// If the node has scoped inputs, push it down a bit more
|
||||||
editableWorkflowObject.value,
|
pushOffset += 140;
|
||||||
lastInteractedWithNodeObject,
|
|
||||||
lastSelectedNodeTypeDescription,
|
|
||||||
);
|
|
||||||
const lastSelectedInputTypes = NodeHelpers.getConnectionTypes(lastSelectedInputs);
|
|
||||||
|
|
||||||
const scopedConnectionIndex = (lastSelectedInputTypes || [])
|
|
||||||
.filter((input) => input !== NodeConnectionType.Main)
|
|
||||||
.findIndex((inputType) => outputs[0] === inputType);
|
|
||||||
|
|
||||||
return NodeViewUtils.getNewNodePosition(
|
|
||||||
workflowsStore.allNodes,
|
|
||||||
[
|
|
||||||
lastInteractedWithNode.position[0] +
|
|
||||||
(NodeViewUtils.NODE_SIZE /
|
|
||||||
(Math.max(lastSelectedNodeTypeDescription?.inputs?.length ?? 1), 1)) *
|
|
||||||
scopedConnectionIndex,
|
|
||||||
lastInteractedWithNode.position[1] + NodeViewUtils.PUSH_NODES_OFFSET,
|
|
||||||
],
|
|
||||||
[100, 0],
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// Has only main outputs or no outputs at all
|
|
||||||
const inputs = NodeHelpers.getNodeInputs(
|
|
||||||
editableWorkflowObject.value,
|
|
||||||
lastInteractedWithNode,
|
|
||||||
lastSelectedNodeTypeDescription,
|
|
||||||
);
|
|
||||||
const inputsTypes = NodeHelpers.getConnectionTypes(inputs);
|
|
||||||
|
|
||||||
let pushOffset = NodeViewUtils.PUSH_NODES_OFFSET;
|
|
||||||
if (!!inputsTypes.find((input) => input !== NodeConnectionType.Main)) {
|
|
||||||
// If the node has scoped inputs, push it down a bit more
|
|
||||||
pushOffset += 150;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If a node is active then add the new node directly after the current one
|
|
||||||
return NodeViewUtils.getNewNodePosition(
|
|
||||||
workflowsStore.allNodes,
|
|
||||||
[
|
|
||||||
lastInteractedWithNode.position[0] + pushOffset,
|
|
||||||
lastInteractedWithNode.position[1] + yOffset,
|
|
||||||
],
|
|
||||||
[100, 0],
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If a node is active then add the new node directly after the current one
|
||||||
|
position = [
|
||||||
|
lastInteractedWithNode.position[0] + pushOffset,
|
||||||
|
lastInteractedWithNode.position[1] + yOffset,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If added node is a trigger and it's the first one added to the canvas
|
if (!position) {
|
||||||
// we place it at canvasAddButtonPosition to replace the canvas add button
|
if (nodeTypesStore.isTriggerNode(node.type) && triggerNodes.value.length === 0) {
|
||||||
const position = (
|
// When added node is a trigger, and it's the first one added to the canvas
|
||||||
nodeTypesStore.isTriggerNode(node.type) && triggerNodes.value.length === 0
|
// we place it at root to replace the canvas add button
|
||||||
? [0, 0]
|
|
||||||
: // If no node is active find a free spot
|
|
||||||
lastClickPosition.value
|
|
||||||
) as XYPosition;
|
|
||||||
|
|
||||||
return NodeViewUtils.getNewNodePosition(workflowsStore.allNodes, position);
|
position = [0, 0];
|
||||||
|
} else {
|
||||||
|
// When no position is set, we place the node at the last clicked position
|
||||||
|
|
||||||
|
position = lastClickPosition.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return NodeViewUtils.getNewNodePosition(workflowsStore.allNodes, position, pushOffsets);
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveNodeName(node: INodeUi) {
|
function resolveNodeName(node: INodeUi) {
|
||||||
|
@ -1219,7 +1262,7 @@ export function useCanvasOperations({ router }: { router: ReturnType<typeof useR
|
||||||
|
|
||||||
function resetWorkspace() {
|
function resetWorkspace() {
|
||||||
// Reset node creator
|
// Reset node creator
|
||||||
nodeCreatorStore.openNodeCreator({ createNodeActive: false });
|
nodeCreatorStore.setNodeCreatorState({ createNodeActive: false });
|
||||||
nodeCreatorStore.setShowScrim(false);
|
nodeCreatorStore.setShowScrim(false);
|
||||||
|
|
||||||
// Make sure that if there is a waiting test-webhook, it gets removed
|
// Make sure that if there is a waiting test-webhook, it gets removed
|
||||||
|
@ -1380,7 +1423,7 @@ export function useCanvasOperations({ router }: { router: ReturnType<typeof useR
|
||||||
|
|
||||||
// Create a workflow with the new nodes and connections that we can use
|
// Create a workflow with the new nodes and connections that we can use
|
||||||
// the rename method
|
// the rename method
|
||||||
const tempWorkflow: Workflow = workflowHelpers.getWorkflow(createNodes, newConnections);
|
const tempWorkflow: Workflow = workflowsStore.getWorkflow(createNodes, newConnections);
|
||||||
|
|
||||||
// Rename all the nodes of which the name changed
|
// Rename all the nodes of which the name changed
|
||||||
for (oldName in nodeNameTable) {
|
for (oldName in nodeNameTable) {
|
||||||
|
@ -1708,6 +1751,7 @@ export function useCanvasOperations({ router }: { router: ReturnType<typeof useR
|
||||||
requireNodeTypeDescription,
|
requireNodeTypeDescription,
|
||||||
addNodes,
|
addNodes,
|
||||||
addNode,
|
addNode,
|
||||||
|
resolveNodePosition,
|
||||||
revertAddNode,
|
revertAddNode,
|
||||||
updateNodesPosition,
|
updateNodesPosition,
|
||||||
updateNodePosition,
|
updateNodePosition,
|
||||||
|
@ -1726,6 +1770,7 @@ export function useCanvasOperations({ router }: { router: ReturnType<typeof useR
|
||||||
copyNodes,
|
copyNodes,
|
||||||
cutNodes,
|
cutNodes,
|
||||||
duplicateNodes,
|
duplicateNodes,
|
||||||
|
getNodesToSave,
|
||||||
revertDeleteNode,
|
revertDeleteNode,
|
||||||
addConnections,
|
addConnections,
|
||||||
createConnection,
|
createConnection,
|
||||||
|
|
|
@ -428,6 +428,7 @@ export const useCredentialsStore = defineStore(STORES.CREDENTIALS, () => {
|
||||||
// #endregion
|
// #endregion
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
state,
|
||||||
getCredentialOwnerName,
|
getCredentialOwnerName,
|
||||||
getCredentialsByType,
|
getCredentialsByType,
|
||||||
getCredentialById,
|
getCredentialById,
|
||||||
|
|
|
@ -90,7 +90,7 @@ export const useNodeCreatorStore = defineStore(STORES.NODE_CREATOR, () => {
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (creatorView) {
|
if (creatorView) {
|
||||||
openNodeCreator({
|
setNodeCreatorState({
|
||||||
createNodeActive: true,
|
createNodeActive: true,
|
||||||
nodeCreatorView: creatorView,
|
nodeCreatorView: creatorView,
|
||||||
});
|
});
|
||||||
|
@ -110,7 +110,7 @@ export const useNodeCreatorStore = defineStore(STORES.NODE_CREATOR, () => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function openNodeCreator({
|
function setNodeCreatorState({
|
||||||
source,
|
source,
|
||||||
createNodeActive,
|
createNodeActive,
|
||||||
nodeCreatorView,
|
nodeCreatorView,
|
||||||
|
@ -200,7 +200,6 @@ export const useNodeCreatorStore = defineStore(STORES.NODE_CREATOR, () => {
|
||||||
uiStore.lastSelectedNode = sourceNode.name;
|
uiStore.lastSelectedNode = sourceNode.name;
|
||||||
uiStore.lastSelectedNodeEndpointUuid = connection.sourceHandle ?? null;
|
uiStore.lastSelectedNodeEndpointUuid = connection.sourceHandle ?? null;
|
||||||
uiStore.lastSelectedNodeOutputIndex = index;
|
uiStore.lastSelectedNodeOutputIndex = index;
|
||||||
// canvasStore.newNodeInsertPosition = null;
|
|
||||||
|
|
||||||
if (isVueFlowConnection(connection)) {
|
if (isVueFlowConnection(connection)) {
|
||||||
uiStore.lastInteractedWithNodeConnection = connection;
|
uiStore.lastInteractedWithNodeConnection = connection;
|
||||||
|
@ -208,7 +207,7 @@ export const useNodeCreatorStore = defineStore(STORES.NODE_CREATOR, () => {
|
||||||
uiStore.lastInteractedWithNodeHandle = connection.sourceHandle ?? null;
|
uiStore.lastInteractedWithNodeHandle = connection.sourceHandle ?? null;
|
||||||
uiStore.lastInteractedWithNodeId = sourceNode.id;
|
uiStore.lastInteractedWithNodeId = sourceNode.id;
|
||||||
|
|
||||||
openNodeCreator({
|
setNodeCreatorState({
|
||||||
source: eventSource,
|
source: eventSource,
|
||||||
createNodeActive: true,
|
createNodeActive: true,
|
||||||
nodeCreatorView,
|
nodeCreatorView,
|
||||||
|
@ -231,7 +230,7 @@ export const useNodeCreatorStore = defineStore(STORES.NODE_CREATOR, () => {
|
||||||
ndvStore.activeNodeName = null;
|
ndvStore.activeNodeName = null;
|
||||||
setSelectedView(TRIGGER_NODE_CREATOR_VIEW);
|
setSelectedView(TRIGGER_NODE_CREATOR_VIEW);
|
||||||
setShowScrim(true);
|
setShowScrim(true);
|
||||||
openNodeCreator({
|
setNodeCreatorState({
|
||||||
source,
|
source,
|
||||||
createNodeActive: true,
|
createNodeActive: true,
|
||||||
nodeCreatorView: TRIGGER_NODE_CREATOR_VIEW,
|
nodeCreatorView: TRIGGER_NODE_CREATOR_VIEW,
|
||||||
|
@ -276,7 +275,7 @@ export const useNodeCreatorStore = defineStore(STORES.NODE_CREATOR, () => {
|
||||||
setOpenSource,
|
setOpenSource,
|
||||||
setActions,
|
setActions,
|
||||||
setMergeNodes,
|
setMergeNodes,
|
||||||
openNodeCreator,
|
setNodeCreatorState,
|
||||||
openSelectiveNodeCreator,
|
openSelectiveNodeCreator,
|
||||||
openNodeCreatorForConnectingNode,
|
openNodeCreatorForConnectingNode,
|
||||||
openNodeCreatorForTriggerNodes,
|
openNodeCreatorForTriggerNodes,
|
||||||
|
|
|
@ -200,6 +200,7 @@ export const useUIStore = defineStore(STORES.UI, () => {
|
||||||
const lastInteractedWithNodeConnection = ref<Connection | null>(null);
|
const lastInteractedWithNodeConnection = ref<Connection | null>(null);
|
||||||
const lastInteractedWithNodeHandle = ref<string | null>(null);
|
const lastInteractedWithNodeHandle = ref<string | null>(null);
|
||||||
const lastInteractedWithNodeId = ref<string | null>(null);
|
const lastInteractedWithNodeId = ref<string | null>(null);
|
||||||
|
const lastCancelledConnectionPosition = ref<XYPosition | null>(null);
|
||||||
|
|
||||||
const settingsStore = useSettingsStore();
|
const settingsStore = useSettingsStore();
|
||||||
const workflowsStore = useWorkflowsStore();
|
const workflowsStore = useWorkflowsStore();
|
||||||
|
@ -624,6 +625,7 @@ export const useUIStore = defineStore(STORES.UI, () => {
|
||||||
lastInteractedWithNodeConnection.value = null;
|
lastInteractedWithNodeConnection.value = null;
|
||||||
lastInteractedWithNodeHandle.value = null;
|
lastInteractedWithNodeHandle.value = null;
|
||||||
lastInteractedWithNodeId.value = null;
|
lastInteractedWithNodeId.value = null;
|
||||||
|
lastCancelledConnectionPosition.value = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -652,6 +654,7 @@ export const useUIStore = defineStore(STORES.UI, () => {
|
||||||
lastInteractedWithNodeHandle,
|
lastInteractedWithNodeHandle,
|
||||||
lastInteractedWithNodeId,
|
lastInteractedWithNodeId,
|
||||||
lastInteractedWithNode,
|
lastInteractedWithNode,
|
||||||
|
lastCancelledConnectionPosition,
|
||||||
nodeViewOffsetPosition,
|
nodeViewOffsetPosition,
|
||||||
nodeViewMoveInProgress,
|
nodeViewMoveInProgress,
|
||||||
nodeViewInitialized,
|
nodeViewInitialized,
|
||||||
|
|
|
@ -103,9 +103,8 @@ export function mapLegacyConnectionToCanvasConnection(
|
||||||
export function parseCanvasConnectionHandleString(handle: string | null | undefined) {
|
export function parseCanvasConnectionHandleString(handle: string | null | undefined) {
|
||||||
const [mode, type, index] = (handle ?? '').split('/');
|
const [mode, type, index] = (handle ?? '').split('/');
|
||||||
|
|
||||||
const resolvedType = isValidNodeConnectionType(type) ? type : NodeConnectionType.Main;
|
|
||||||
const resolvedMode = isValidCanvasConnectionMode(mode) ? mode : CanvasConnectionMode.Output;
|
const resolvedMode = isValidCanvasConnectionMode(mode) ? mode : CanvasConnectionMode.Output;
|
||||||
|
const resolvedType = isValidNodeConnectionType(type) ? type : NodeConnectionType.Main;
|
||||||
let resolvedIndex = parseInt(index, 10);
|
let resolvedIndex = parseInt(index, 10);
|
||||||
if (isNaN(resolvedIndex)) {
|
if (isNaN(resolvedIndex)) {
|
||||||
resolvedIndex = 0;
|
resolvedIndex = 0;
|
||||||
|
|
|
@ -38,6 +38,9 @@ const MIN_X_TO_SHOW_OUTPUT_LABEL = 90;
|
||||||
const MIN_Y_TO_SHOW_OUTPUT_LABEL = 100;
|
const MIN_Y_TO_SHOW_OUTPUT_LABEL = 100;
|
||||||
|
|
||||||
export const NODE_SIZE = 100;
|
export const NODE_SIZE = 100;
|
||||||
|
export const DEFAULT_NODE_SIZE = [100, 100];
|
||||||
|
export const CONFIGURATION_NODE_SIZE = [80, 80];
|
||||||
|
export const CONFIGURABLE_NODE_SIZE = [256, 100];
|
||||||
export const PLACEHOLDER_TRIGGER_NODE_SIZE = 100;
|
export const PLACEHOLDER_TRIGGER_NODE_SIZE = 100;
|
||||||
export const DEFAULT_START_POSITION_X = 180;
|
export const DEFAULT_START_POSITION_X = 180;
|
||||||
export const DEFAULT_START_POSITION_Y = 240;
|
export const DEFAULT_START_POSITION_Y = 240;
|
||||||
|
|
|
@ -34,7 +34,11 @@ import type {
|
||||||
ToggleNodeCreatorOptions,
|
ToggleNodeCreatorOptions,
|
||||||
XYPosition,
|
XYPosition,
|
||||||
} from '@/Interface';
|
} from '@/Interface';
|
||||||
import type { Connection, ViewportTransform } from '@vue-flow/core';
|
import type {
|
||||||
|
Connection,
|
||||||
|
ViewportTransform,
|
||||||
|
XYPosition as VueFlowXYPosition,
|
||||||
|
} from '@vue-flow/core';
|
||||||
import type {
|
import type {
|
||||||
CanvasConnectionCreateData,
|
CanvasConnectionCreateData,
|
||||||
CanvasEventBusEvents,
|
CanvasEventBusEvents,
|
||||||
|
@ -738,12 +742,20 @@ function onRevertCreateConnection({ connection }: { connection: [IConnection, IC
|
||||||
revertCreateConnection(connection);
|
revertCreateConnection(connection);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onCreateConnectionCancelled(event: ConnectStartEvent, mouseEvent?: MouseEvent) {
|
function onCreateConnectionCancelled(
|
||||||
|
event: ConnectStartEvent,
|
||||||
|
position: VueFlowXYPosition,
|
||||||
|
mouseEvent?: MouseEvent,
|
||||||
|
) {
|
||||||
const preventDefault = (mouseEvent?.target as HTMLElement).classList?.contains('clickable');
|
const preventDefault = (mouseEvent?.target as HTMLElement).classList?.contains('clickable');
|
||||||
if (preventDefault) {
|
if (preventDefault) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
uiStore.lastInteractedWithNodeId = event.nodeId;
|
||||||
|
uiStore.lastInteractedWithNodeHandle = event.handleId;
|
||||||
|
uiStore.lastCancelledConnectionPosition = [position.x, position.y];
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
nodeCreatorStore.openNodeCreatorForConnectingNode({
|
nodeCreatorStore.openNodeCreatorForConnectingNode({
|
||||||
connection: {
|
connection: {
|
||||||
|
@ -874,11 +886,15 @@ async function onOpenNodeCreatorForTriggerNodes(source: NodeCreatorOpenSource) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function onOpenNodeCreatorFromCanvas(source: NodeCreatorOpenSource) {
|
function onOpenNodeCreatorFromCanvas(source: NodeCreatorOpenSource) {
|
||||||
onOpenNodeCreator({ createNodeActive: true, source });
|
onToggleNodeCreator({ createNodeActive: true, source });
|
||||||
}
|
}
|
||||||
|
|
||||||
function onOpenNodeCreator(options: ToggleNodeCreatorOptions) {
|
function onToggleNodeCreator(options: ToggleNodeCreatorOptions) {
|
||||||
nodeCreatorStore.openNodeCreator(options);
|
nodeCreatorStore.setNodeCreatorState(options);
|
||||||
|
|
||||||
|
if (!options.createNodeActive && !options.hasAddedNodes) {
|
||||||
|
uiStore.resetLastInteractedWith();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onCreateSticky() {
|
function onCreateSticky() {
|
||||||
|
@ -1378,7 +1394,6 @@ function selectNodes(ids: string[]) {
|
||||||
|
|
||||||
function onClickPane(position: CanvasNode['position']) {
|
function onClickPane(position: CanvasNode['position']) {
|
||||||
lastClickPosition.value = [position.x, position.y];
|
lastClickPosition.value = [position.x, position.y];
|
||||||
canvasStore.newNodeInsertPosition = [position.x, position.y];
|
|
||||||
uiStore.isCreateNodeActive = false;
|
uiStore.isCreateNodeActive = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1563,7 +1578,7 @@ onDeactivated(() => {
|
||||||
v-if="!isCanvasReadOnly"
|
v-if="!isCanvasReadOnly"
|
||||||
:create-node-active="uiStore.isCreateNodeActive"
|
:create-node-active="uiStore.isCreateNodeActive"
|
||||||
:node-view-scale="viewportTransform.zoom"
|
:node-view-scale="viewportTransform.zoom"
|
||||||
@toggle-node-creator="onOpenNodeCreator"
|
@toggle-node-creator="onToggleNodeCreator"
|
||||||
@add-nodes="onAddNodesAndConnections"
|
@add-nodes="onAddNodesAndConnections"
|
||||||
/>
|
/>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
|
Loading…
Reference in a new issue