feat(editor): Improve canvas node insertion position (#14289)

This commit is contained in:
Alex Grozav
2025-05-13 19:38:10 +03:00
committed by GitHub
parent 3eb1c1c783
commit 102c67628c
10 changed files with 406 additions and 96 deletions

View File

@@ -58,6 +58,7 @@ import type {
CanvasConnectionPort,
CanvasNode,
CanvasNodeMoveEvent,
ViewportBoundaries,
} from '@/types';
import { CanvasConnectionMode } from '@/types';
import {
@@ -72,7 +73,9 @@ import {
CONFIGURABLE_NODE_SIZE,
CONFIGURATION_NODE_SIZE,
DEFAULT_NODE_SIZE,
DEFAULT_VIEWPORT_BOUNDARIES,
generateOffsets,
getNodesGroupSize,
PUSH_NODES_OFFSET,
} from '@/utils/nodeViewUtils';
import type { Connection } from '@vue-flow/core';
@@ -116,6 +119,7 @@ type AddNodesBaseOptions = {
trackHistory?: boolean;
keepPristine?: boolean;
telemetry?: boolean;
viewport?: ViewportBoundaries;
};
type AddNodesOptions = AddNodesBaseOptions & {
@@ -526,7 +530,10 @@ export function useCanvasOperations({ router }: { router: ReturnType<typeof useR
);
}
async function addNodes(nodes: AddedNodesAndConnections['nodes'], options: AddNodesOptions = {}) {
async function addNodes(
nodes: AddedNodesAndConnections['nodes'],
{ viewport, ...options }: AddNodesOptions = {},
) {
let insertPosition = options.position;
let lastAddedNode: INodeUi | undefined;
const addedNodes: INodeUi[] = [];
@@ -546,7 +553,7 @@ export function useCanvasOperations({ router }: { router: ReturnType<typeof useR
historyStore.startRecordingUndo();
}
for (const nodeAddData of nodesWithTypeVersion) {
for (const [index, nodeAddData] of nodesWithTypeVersion.entries()) {
const { isAutoAdd, openDetail: openNDV, ...node } = nodeAddData;
const position = node.position ?? insertPosition;
const nodeTypeDescription = requireNodeTypeDescription(node.type, node.typeVersion);
@@ -560,6 +567,7 @@ export function useCanvasOperations({ router }: { router: ReturnType<typeof useR
nodeTypeDescription,
{
...options,
...(index === 0 ? { viewport } : {}),
openNDV,
isAutoAdd,
},
@@ -634,7 +642,9 @@ export function useCanvasOperations({ router }: { router: ReturnType<typeof useR
): INodeUi {
checkMaxNodesOfTypeReached(nodeTypeDescription);
const nodeData = resolveNodeData(node, nodeTypeDescription);
const nodeData = resolveNodeData(node, nodeTypeDescription, {
viewport: options.viewport,
});
if (!nodeData) {
throw new Error(i18n.baseText('nodeViewV2.showError.failedToCreateNode'));
}
@@ -797,12 +807,15 @@ export function useCanvasOperations({ router }: { router: ReturnType<typeof useR
function resolveNodeData(
node: AddNodeDataWithTypeVersion,
nodeTypeDescription: INodeTypeDescription,
options: { viewport?: ViewportBoundaries } = {},
) {
const id = node.id ?? nodeHelpers.assignNodeId(node as INodeUi);
const name = node.name ?? (nodeTypeDescription.defaults.name as string);
const type = nodeTypeDescription.name;
const typeVersion = node.typeVersion;
const position = resolveNodePosition(node as INodeUi, nodeTypeDescription);
const position = resolveNodePosition(node as INodeUi, nodeTypeDescription, {
viewport: options.viewport,
});
const disabled = node.disabled ?? false;
const parameters = node.parameters ?? {};
@@ -883,14 +896,8 @@ export function useCanvasOperations({ router }: { router: ReturnType<typeof useR
function resolveNodePosition(
node: Omit<INodeUi, 'position'> & { position?: INodeUi['position'] },
nodeTypeDescription: INodeTypeDescription,
options: { viewport?: ViewportBoundaries } = {},
) {
let position: XYPosition | undefined = node.position;
let pushOffsets: XYPosition = [40, 40];
if (position) {
return NodeViewUtils.getNewNodePosition(workflowsStore.allNodes, position, pushOffsets);
}
// Available when
// - clicking the plus button of a node handle
// - dragging an edge / connection of a node handle
@@ -907,6 +914,17 @@ export function useCanvasOperations({ router }: { router: ReturnType<typeof useR
const nodeSize =
connectionType === NodeConnectionTypes.Main ? DEFAULT_NODE_SIZE : CONFIGURATION_NODE_SIZE;
let pushOffsets: XYPosition = [nodeSize[0] / 2, nodeSize[1] / 2];
let position: XYPosition | undefined = node.position;
if (position) {
return NodeViewUtils.getNewNodePosition(workflowsStore.allNodes, position, {
offset: pushOffsets,
size: nodeSize,
viewport: options.viewport,
normalize: false,
});
}
if (lastInteractedWithNode) {
const lastInteractedWithNodeTypeDescription = nodeTypesStore.getNodeType(
@@ -1056,7 +1074,11 @@ export function useCanvasOperations({ router }: { router: ReturnType<typeof useR
}
}
return NodeViewUtils.getNewNodePosition(workflowsStore.allNodes, position, pushOffsets);
return NodeViewUtils.getNewNodePosition(workflowsStore.allNodes, position, {
offset: pushOffsets,
size: nodeSize,
viewport: options.viewport,
});
}
function resolveNodeName(node: INodeUi) {
@@ -1503,7 +1525,7 @@ export function useCanvasOperations({ router }: { router: ReturnType<typeof useR
async function addImportedNodesToWorkflow(
data: IWorkflowDataUpdate,
{ trackBulk = true, trackHistory = false } = {},
{ trackBulk = true, trackHistory = false, viewport = DEFAULT_VIEWPORT_BOUNDARIES } = {},
): Promise<IWorkflowDataUpdate> {
// Because nodes with the same name maybe already exist, it could
// be needed that they have to be renamed. Also could it be possible
@@ -1650,7 +1672,11 @@ export function useCanvasOperations({ router }: { router: ReturnType<typeof useR
historyStore.startRecordingUndo();
}
await addNodes(Object.values(tempWorkflow.nodes), { trackBulk: false, trackHistory });
await addNodes(Object.values(tempWorkflow.nodes), {
trackBulk: false,
trackHistory,
viewport,
});
await addConnections(
mapLegacyConnectionsToCanvasConnections(
tempWorkflow.connectionsBySourceNode,
@@ -1674,8 +1700,17 @@ export function useCanvasOperations({ router }: { router: ReturnType<typeof useR
async function importWorkflowData(
workflowData: IWorkflowDataUpdate,
source: string,
importTags = true,
{ trackBulk = true, trackHistory = true } = {},
{
importTags = true,
trackBulk = true,
trackHistory = true,
viewport,
}: {
importTags?: boolean;
trackBulk?: boolean;
trackHistory?: boolean;
viewport?: ViewportBoundaries;
} = {},
): Promise<IWorkflowDataUpdate> {
uiStore.resetLastInteractedWith();
@@ -1767,10 +1802,19 @@ export function useCanvasOperations({ router }: { router: ReturnType<typeof useR
// the user
workflowHelpers.updateNodePositions(
workflowData,
NodeViewUtils.getNewNodePosition(editableWorkflow.value.nodes, lastClickPosition.value),
NodeViewUtils.getNewNodePosition(editableWorkflow.value.nodes, lastClickPosition.value, {
...(workflowData.nodes && workflowData.nodes.length > 1
? { size: getNodesGroupSize(workflowData.nodes) }
: {}),
viewport,
}),
);
await addImportedNodesToWorkflow(workflowData, { trackBulk, trackHistory });
await addImportedNodesToWorkflow(workflowData, {
trackBulk,
trackHistory,
viewport,
});
if (importTags && settingsStore.areTagsEnabled && Array.isArray(workflowData.tags)) {
await importWorkflowTags(workflowData);
@@ -1920,9 +1964,12 @@ export function useCanvasOperations({ router }: { router: ReturnType<typeof useR
return filteredConnections;
}
async function duplicateNodes(ids: string[]) {
async function duplicateNodes(ids: string[], options: { viewport?: ViewportBoundaries } = {}) {
const workflowData = deepCopy(getNodesToSave(workflowsStore.getNodesByIds(ids)));
const result = await importWorkflowData(workflowData, 'duplicate', false);
const result = await importWorkflowData(workflowData, 'duplicate', {
viewport: options.viewport,
importTags: false,
});
return result.nodes?.map((node) => node.id).filter(isPresent) ?? [];
}