mirror of
https://github.com/n8n-io/n8n.git
synced 2026-08-30 18:01:23 +08:00
feat(editor): Improve sticky note behavior during node insertion (#25207)
This commit is contained in:
@@ -5588,6 +5588,469 @@ describe('useCanvasOperations', () => {
|
||||
expect(nodesToMove).toHaveLength(2);
|
||||
expect(nodesToMove.find((n) => n.name === 'Start')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should classify sticky notes as stretch-only when they contain the source node and insertion point is inside the sticky note', () => {
|
||||
/**
|
||||
* Visual representation of the test scenario:
|
||||
*
|
||||
* Before insertion:
|
||||
* ┌─────────────────────────────┐
|
||||
* │ Sticky Note (50, 50) │
|
||||
* │ │
|
||||
* │ [Source]────────────────> [Target]
|
||||
* │ (100, 100) (400, 100)
|
||||
* │ │
|
||||
* └─────────────────────────────┘
|
||||
*
|
||||
* Insertion point: (250, 100) - between Source and Target
|
||||
*
|
||||
* Expected behavior:
|
||||
* - Sticky should ONLY stretch (not move) because it contains the source node
|
||||
* - Source node acts as an anchor point
|
||||
* - Target node will shift right
|
||||
* - Sticky will expand to accommodate the new node while staying anchored
|
||||
*/
|
||||
const sourceNode = createTestNode({ id: 'source', name: 'Source', position: [100, 100] });
|
||||
const targetNode = createTestNode({ id: 'target', name: 'Target', position: [400, 100] });
|
||||
const stickyNote = createTestNode({
|
||||
id: 'sticky',
|
||||
name: 'Sticky',
|
||||
type: STICKY_NODE_TYPE,
|
||||
position: [50, 50],
|
||||
parameters: { width: 300, height: 200 },
|
||||
});
|
||||
|
||||
const pinia = createTestingPinia({
|
||||
initialState: {
|
||||
[STORES.WORKFLOWS]: {
|
||||
workflow: createTestWorkflow({
|
||||
nodes: [sourceNode, targetNode, stickyNote],
|
||||
connections: {
|
||||
[sourceNode.name]: {
|
||||
main: [[{ node: targetNode.name, type: NodeConnectionTypes.Main, index: 0 }]],
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
});
|
||||
setActivePinia(pinia);
|
||||
|
||||
const { getNodesToShift } = useCanvasOperations();
|
||||
const insertPosition: [number, number] = [250, 100];
|
||||
|
||||
const { nodesToMove, stickiesToStretch, stickiesToMoveAndStretch } = getNodesToShift(
|
||||
insertPosition,
|
||||
'Source',
|
||||
);
|
||||
|
||||
// Sticky containing source node should only stretch (anchored)
|
||||
expect(stickiesToStretch).toHaveLength(1);
|
||||
expect(stickiesToStretch[0].id).toBe('sticky');
|
||||
expect(stickiesToMoveAndStretch).toHaveLength(0);
|
||||
expect(nodesToMove.find((n) => n.id === 'sticky')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should classify sticky notes to only move when they are far from the insertion point', () => {
|
||||
/**
|
||||
* Visual representation:
|
||||
*
|
||||
* Before insertion:
|
||||
* ┌─────────────┐
|
||||
* │ Sticky │
|
||||
* [Source]───────────────────────────> [Target] │ (600, 50) │
|
||||
* (100, 100) (500, 100) │ │
|
||||
* └─────────────┘
|
||||
*
|
||||
* Insertion point: (250, 100)
|
||||
*
|
||||
* Expected behavior:
|
||||
* - Sticky is entirely to the right and far from insertion point
|
||||
* - Sticky should ONLY move (no stretching needed)
|
||||
* - Target node will also shift right
|
||||
*/
|
||||
const sourceNode = createTestNode({ id: 'source', name: 'Source', position: [100, 100] });
|
||||
const targetNode = createTestNode({ id: 'target', name: 'Target', position: [500, 100] });
|
||||
const stickyNote = createTestNode({
|
||||
id: 'sticky',
|
||||
name: 'Sticky',
|
||||
type: STICKY_NODE_TYPE,
|
||||
position: [600, 50],
|
||||
parameters: { width: 200, height: 200 },
|
||||
});
|
||||
|
||||
const pinia = createTestingPinia({
|
||||
initialState: {
|
||||
[STORES.WORKFLOWS]: {
|
||||
workflow: createTestWorkflow({
|
||||
nodes: [sourceNode, targetNode, stickyNote],
|
||||
connections: {
|
||||
[sourceNode.name]: {
|
||||
main: [[{ node: targetNode.name, type: NodeConnectionTypes.Main, index: 0 }]],
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
});
|
||||
setActivePinia(pinia);
|
||||
|
||||
const { getNodesToShift } = useCanvasOperations();
|
||||
const insertPosition: [number, number] = [250, 100];
|
||||
|
||||
const { nodesToMove, stickiesToStretch, stickiesToMoveAndStretch } = getNodesToShift(
|
||||
insertPosition,
|
||||
'Source',
|
||||
);
|
||||
|
||||
// Sticky far from insertion should only move
|
||||
expect(stickiesToMoveAndStretch).toHaveLength(0);
|
||||
expect(stickiesToStretch).toHaveLength(0);
|
||||
expect(nodesToMove).toHaveLength(2); // target + sticky
|
||||
expect(nodesToMove).toContainEqual(expect.objectContaining({ id: 'target' }));
|
||||
expect(nodesToMove).toContainEqual(expect.objectContaining({ id: 'sticky' }));
|
||||
});
|
||||
|
||||
it('should not move sticky notes that do not overlap vertically with the insertion area', () => {
|
||||
/**
|
||||
* Visual representation:
|
||||
*
|
||||
* [Source]──────────────────────> [Target]
|
||||
* (100, 100) (400, 100)
|
||||
* ↑
|
||||
* Insertion: (250, 100)
|
||||
*
|
||||
*
|
||||
*
|
||||
* ┌─────────────┐
|
||||
* │ Sticky │
|
||||
* │ (300, 500) │ ← Far below, no vertical overlap
|
||||
* │ │
|
||||
* └─────────────┘
|
||||
*
|
||||
* Expected behavior:
|
||||
* - Sticky is far below the insertion area (no vertical overlap)
|
||||
* - Sticky should NOT be affected (no move, no stretch)
|
||||
*/
|
||||
const sourceNode = createTestNode({ id: 'source', name: 'Source', position: [100, 100] });
|
||||
const targetNode = createTestNode({ id: 'target', name: 'Target', position: [400, 100] });
|
||||
const stickyNote = createTestNode({
|
||||
id: 'sticky',
|
||||
name: 'Sticky',
|
||||
type: STICKY_NODE_TYPE,
|
||||
position: [300, 500],
|
||||
parameters: { width: 200, height: 200 },
|
||||
});
|
||||
|
||||
const pinia = createTestingPinia({
|
||||
initialState: {
|
||||
[STORES.WORKFLOWS]: {
|
||||
workflow: createTestWorkflow({
|
||||
nodes: [sourceNode, targetNode, stickyNote],
|
||||
connections: {
|
||||
[sourceNode.name]: {
|
||||
main: [[{ node: targetNode.name, type: NodeConnectionTypes.Main, index: 0 }]],
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
});
|
||||
setActivePinia(pinia);
|
||||
|
||||
const { getNodesToShift } = useCanvasOperations();
|
||||
const insertPosition: [number, number] = [250, 100];
|
||||
|
||||
const { nodesToMove, stickiesToStretch, stickiesToMoveAndStretch } = getNodesToShift(
|
||||
insertPosition,
|
||||
'Source',
|
||||
);
|
||||
|
||||
// Sticky not overlapping vertically should not be affected
|
||||
expect(stickiesToStretch).toHaveLength(0);
|
||||
expect(stickiesToMoveAndStretch).toHaveLength(0);
|
||||
expect(nodesToMove).toHaveLength(1); // only target moves
|
||||
expect(nodesToMove).toContainEqual(expect.objectContaining({ id: 'target' }));
|
||||
expect(nodesToMove.find((n) => n.id === 'sticky')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should track associated nodes for sticky notes', () => {
|
||||
/**
|
||||
* Visual representation:
|
||||
*
|
||||
* ┌─────────┐
|
||||
* │ Sticky │
|
||||
* [Source]──────────────────-─────────>│[Target] │
|
||||
* (100, 100) │(400,100)│
|
||||
* └─────────┘
|
||||
* ↑
|
||||
* Insertion: (250, 100)
|
||||
*
|
||||
* Expected behavior:
|
||||
* - Sticky contains the target node (center proximity check)
|
||||
* - Target node will move, so sticky will move too
|
||||
* - Sticky should track Target as an associated node for stretching calculations
|
||||
*/
|
||||
const sourceNode = createTestNode({ id: 'source', name: 'Source', position: [100, 100] });
|
||||
const targetNode = createTestNode({ id: 'target', name: 'Target', position: [400, 100] });
|
||||
const stickyNote = createTestNode({
|
||||
id: 'sticky',
|
||||
name: 'Sticky',
|
||||
type: STICKY_NODE_TYPE,
|
||||
position: [370, 70],
|
||||
parameters: { width: 100, height: 100 },
|
||||
});
|
||||
|
||||
const pinia = createTestingPinia({
|
||||
initialState: {
|
||||
[STORES.WORKFLOWS]: {
|
||||
workflow: createTestWorkflow({
|
||||
nodes: [sourceNode, targetNode, stickyNote],
|
||||
connections: {
|
||||
[sourceNode.name]: {
|
||||
main: [[{ node: targetNode.name, type: NodeConnectionTypes.Main, index: 0 }]],
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
});
|
||||
setActivePinia(pinia);
|
||||
|
||||
const { getNodesToShift } = useCanvasOperations();
|
||||
const insertPosition: [number, number] = [250, 100];
|
||||
|
||||
const { nodesToMove, stickyAssociatedNodes } = getNodesToShift(insertPosition, 'Source');
|
||||
|
||||
// Should track the target node as associated with the sticky
|
||||
expect(nodesToMove).toHaveLength(2); // target + sticky
|
||||
expect(nodesToMove).toContainEqual(expect.objectContaining({ id: 'target' }));
|
||||
expect(nodesToMove).toContainEqual(expect.objectContaining({ id: 'sticky' }));
|
||||
|
||||
const associatedNodes = stickyAssociatedNodes.get('sticky');
|
||||
expect(associatedNodes).toBeDefined();
|
||||
expect(associatedNodes).toHaveLength(1);
|
||||
expect(associatedNodes?.[0].id).toBe('target');
|
||||
});
|
||||
|
||||
it('should handle multiple sticky notes with different behaviors', () => {
|
||||
/**
|
||||
* Visual representation:
|
||||
*
|
||||
* ┌──────────────────────┐ ┌───────┐ ┌────────────┐
|
||||
* │ Sticky-Anchor │ │Sticky │ │Sticky-Move │
|
||||
* │ │ │[Tgt] │ │ │
|
||||
* │ [Source]──────────────────────>│(400) │ │ (600, 50) │
|
||||
* │ (100, 100) │ └───────┘ │ │
|
||||
* │ │ └────────────┘
|
||||
* └──────────────────────┘
|
||||
* ↑
|
||||
* Insertion: (250, 100)
|
||||
*
|
||||
* Expected behavior:
|
||||
* - Sticky-Anchor: Contains source node → ONLY stretch (anchored)
|
||||
* - Sticky-WithTarget: Contains target node that will move → move + track association
|
||||
* - Sticky-Move: Far to the right → ONLY move
|
||||
*/
|
||||
const sourceNode = createTestNode({ id: 'source', name: 'Source', position: [100, 100] });
|
||||
const targetNode = createTestNode({ id: 'target', name: 'Target', position: [400, 100] });
|
||||
const stickyAnchor = createTestNode({
|
||||
id: 'sticky-anchor',
|
||||
name: 'StickyAnchor',
|
||||
type: STICKY_NODE_TYPE,
|
||||
position: [50, 50],
|
||||
parameters: { width: 300, height: 200 },
|
||||
});
|
||||
const stickyWithTarget = createTestNode({
|
||||
id: 'sticky-with-target',
|
||||
name: 'StickyWithTarget',
|
||||
type: STICKY_NODE_TYPE,
|
||||
position: [370, 70],
|
||||
parameters: { width: 100, height: 100 },
|
||||
});
|
||||
const stickyMove = createTestNode({
|
||||
id: 'sticky-move',
|
||||
name: 'StickyMove',
|
||||
type: STICKY_NODE_TYPE,
|
||||
position: [600, 50],
|
||||
parameters: { width: 200, height: 200 },
|
||||
});
|
||||
|
||||
const pinia = createTestingPinia({
|
||||
initialState: {
|
||||
[STORES.WORKFLOWS]: {
|
||||
workflow: createTestWorkflow({
|
||||
nodes: [sourceNode, targetNode, stickyAnchor, stickyWithTarget, stickyMove],
|
||||
connections: {
|
||||
[sourceNode.name]: {
|
||||
main: [[{ node: targetNode.name, type: NodeConnectionTypes.Main, index: 0 }]],
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
});
|
||||
setActivePinia(pinia);
|
||||
|
||||
const { getNodesToShift } = useCanvasOperations();
|
||||
const insertPosition: [number, number] = [250, 100];
|
||||
|
||||
const { nodesToMove, stickiesToStretch, stickyAssociatedNodes } = getNodesToShift(
|
||||
insertPosition,
|
||||
'Source',
|
||||
);
|
||||
|
||||
// Sticky containing source should only stretch
|
||||
expect(stickiesToStretch).toHaveLength(1);
|
||||
expect(stickiesToStretch).toContainEqual(expect.objectContaining({ id: 'sticky-anchor' }));
|
||||
|
||||
// Sticky containing target should move (and track associated nodes)
|
||||
expect(nodesToMove).toHaveLength(3); // target + sticky-with-target + sticky-move
|
||||
expect(nodesToMove).toContainEqual(expect.objectContaining({ id: 'target' }));
|
||||
expect(nodesToMove).toContainEqual(expect.objectContaining({ id: 'sticky-with-target' }));
|
||||
expect(stickyAssociatedNodes.get('sticky-with-target')).toHaveLength(1);
|
||||
|
||||
// Sticky far to the right should only move
|
||||
expect(nodesToMove).toContainEqual(expect.objectContaining({ id: 'sticky-move' }));
|
||||
});
|
||||
|
||||
it('should handle sticky notes with nodes at the center boundary', () => {
|
||||
/**
|
||||
* Visual representation:
|
||||
*
|
||||
* ┌────────────────────┐
|
||||
* │ Sticky (200, 50) │
|
||||
* [Source]───│───────────────────────────> [Target]
|
||||
* (100, 100) │ ↑ │ (400, 100)
|
||||
* │ Insertion point │
|
||||
* │ (250, 100) │
|
||||
* └────────────────────┘
|
||||
*
|
||||
* Expected behavior:
|
||||
* - Sticky overlaps the insertion point (inside its bounds)
|
||||
* - Sticky does NOT contain source node
|
||||
* - Sticky should ONLY stretch to accommodate the new node
|
||||
*/
|
||||
const sourceNode = createTestNode({ id: 'source', name: 'Source', position: [100, 100] });
|
||||
const targetNode = createTestNode({ id: 'target', name: 'Target', position: [400, 100] });
|
||||
const stickyNote = createTestNode({
|
||||
id: 'sticky',
|
||||
name: 'Sticky',
|
||||
type: STICKY_NODE_TYPE,
|
||||
position: [200, 50],
|
||||
parameters: { width: 200, height: 200 },
|
||||
});
|
||||
|
||||
const pinia = createTestingPinia({
|
||||
initialState: {
|
||||
[STORES.WORKFLOWS]: {
|
||||
workflow: createTestWorkflow({
|
||||
nodes: [sourceNode, targetNode, stickyNote],
|
||||
connections: {
|
||||
[sourceNode.name]: {
|
||||
main: [[{ node: targetNode.name, type: NodeConnectionTypes.Main, index: 0 }]],
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
});
|
||||
setActivePinia(pinia);
|
||||
|
||||
const { getNodesToShift } = useCanvasOperations();
|
||||
const insertPosition: [number, number] = [250, 100];
|
||||
|
||||
const { stickiesToStretch } = getNodesToShift(insertPosition, 'Source');
|
||||
|
||||
// Sticky overlapping insertion point should stretch
|
||||
expect(stickiesToStretch).toHaveLength(1);
|
||||
expect(stickiesToStretch[0].id).toBe('sticky');
|
||||
});
|
||||
|
||||
it('should track multiple associated nodes for sticky stretching', () => {
|
||||
/**
|
||||
* Visual representation:
|
||||
*
|
||||
* ┌──────────┐
|
||||
* │ Sticky │
|
||||
* ┌────────>│[Target1] │
|
||||
* │ │ (400,80) │
|
||||
* [Source]──────────────────┤ │ │
|
||||
* (100, 100) │ │[Target2] │
|
||||
* └────────>│(400,120) │
|
||||
* └──────────┘
|
||||
* ↑
|
||||
* Insertion: (250, 100)
|
||||
*
|
||||
* Expected behavior:
|
||||
* - Sticky contains BOTH target nodes
|
||||
* - Both target nodes will move
|
||||
* - Sticky should track BOTH targets as associated nodes
|
||||
* - This ensures proper stretching to encompass all associated nodes
|
||||
*/
|
||||
const sourceNode = createTestNode({
|
||||
id: 'source',
|
||||
name: 'Source',
|
||||
position: [100, 100],
|
||||
});
|
||||
|
||||
const targetNode1 = createTestNode({
|
||||
id: 'target1',
|
||||
name: 'Target1',
|
||||
position: [400, 80],
|
||||
});
|
||||
|
||||
const targetNode2 = createTestNode({
|
||||
id: 'target2',
|
||||
name: 'Target2',
|
||||
position: [400, 120],
|
||||
});
|
||||
|
||||
const stickyNote = createTestNode({
|
||||
id: 'sticky',
|
||||
name: 'Sticky',
|
||||
type: STICKY_NODE_TYPE,
|
||||
position: [370, 50],
|
||||
parameters: { width: 100, height: 150 },
|
||||
});
|
||||
|
||||
const pinia = createTestingPinia({
|
||||
initialState: {
|
||||
[STORES.WORKFLOWS]: {
|
||||
workflow: createTestWorkflow({
|
||||
nodes: [sourceNode, targetNode1, targetNode2, stickyNote],
|
||||
connections: {
|
||||
[sourceNode.name]: {
|
||||
main: [
|
||||
[
|
||||
{ node: targetNode1.name, type: NodeConnectionTypes.Main, index: 0 },
|
||||
{ node: targetNode2.name, type: NodeConnectionTypes.Main, index: 0 },
|
||||
],
|
||||
],
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
});
|
||||
setActivePinia(pinia);
|
||||
|
||||
const { getNodesToShift } = useCanvasOperations();
|
||||
const insertPosition: [number, number] = [250, 100];
|
||||
|
||||
const { nodesToMove, stickyAssociatedNodes } = getNodesToShift(insertPosition, 'Source');
|
||||
|
||||
// Should track both target nodes as associated with the sticky
|
||||
expect(nodesToMove).toHaveLength(3); // target1 + target2 + sticky
|
||||
expect(nodesToMove).toContainEqual(expect.objectContaining({ id: 'target1' }));
|
||||
expect(nodesToMove).toContainEqual(expect.objectContaining({ id: 'target2' }));
|
||||
expect(nodesToMove).toContainEqual(expect.objectContaining({ id: 'sticky' }));
|
||||
|
||||
const associatedNodes = stickyAssociatedNodes.get('sticky');
|
||||
expect(associatedNodes).toBeDefined();
|
||||
expect(associatedNodes).toHaveLength(2);
|
||||
expect(associatedNodes?.map((n) => n.id).sort()).toEqual(['target1', 'target2']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createConnectionToLastInteractedWithNode - HITL node handling', () => {
|
||||
|
||||
@@ -1420,12 +1420,9 @@ export function useCanvasOperations() {
|
||||
lastInteractedWithNode.position[1] + yOffset,
|
||||
];
|
||||
|
||||
// When inserting via edge plus button, adjust Y to fit within overlapping sticky notes
|
||||
// When inserting via edge plus button, keep Y aligned to preserve vertical line
|
||||
if (lastInteractedWithNodeConnection) {
|
||||
const adjustedY = getYPositionForStickyOverlap(position, nodeSize);
|
||||
if (adjustedY !== null) {
|
||||
position = [position[0], adjustedY];
|
||||
}
|
||||
position = [position[0], lastInteractedWithNode.position[1]];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1521,36 +1518,108 @@ export function useCanvasOperations() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all nodes that should be shifted when inserting a node.
|
||||
* Determines which nodes and sticky notes need to be moved or stretched when inserting a new node.
|
||||
*
|
||||
* Algorithm:
|
||||
* 1. Find nodes that overlap with or are to the right of insertion area (similar Y)
|
||||
* 2. Add nodes connected to them (downstream)
|
||||
* 3. Filter to only include nodes that need to move
|
||||
* 1. Find regular nodes that overlap with or are to the right of insertion area
|
||||
* 2. Add downstream connected nodes
|
||||
* 3. Classify sticky notes based on:
|
||||
* - Whether they contain the source node (anchored = stretch only)
|
||||
* - Whether they contain nodes that will move
|
||||
* - Their position relative to insertion point
|
||||
*
|
||||
* @returns Classification of nodes and stickies with their associated nodes for stretching
|
||||
*/
|
||||
function getNodesToShift(
|
||||
insertPosition: XYPosition,
|
||||
sourceNodeName: string,
|
||||
): { nodesToMove: INodeUi[]; stickiesToStretch: INodeUi[] } {
|
||||
nodeSize: [number, number] = DEFAULT_NODE_SIZE,
|
||||
): {
|
||||
nodesToMove: INodeUi[];
|
||||
stickiesToStretch: INodeUi[];
|
||||
stickiesToMoveAndStretch: INodeUi[];
|
||||
stickyAssociatedNodes: Map<string, INodeUi[]>;
|
||||
} {
|
||||
const allNodes = Object.values(workflowsStore.nodesByName);
|
||||
const insertX = insertPosition[0];
|
||||
const insertY = insertPosition[1];
|
||||
const yTolerance = DEFAULT_NODE_SIZE[1] * 2; // Nodes within ~2 node heights are considered "similar Y"
|
||||
|
||||
const getNodeCenter = (node: INodeUi) => ({
|
||||
x: node.position[0] + DEFAULT_NODE_SIZE[0] / 2,
|
||||
y: node.position[1] + DEFAULT_NODE_SIZE[1] / 2,
|
||||
});
|
||||
|
||||
const isSimilarY = (node: INodeUi) => Math.abs(node.position[1] - insertY) <= yTolerance;
|
||||
|
||||
const overlapsInsertX = (node: INodeUi) => {
|
||||
const nodeRightEdge = node.position[0] + DEFAULT_NODE_SIZE[0];
|
||||
return nodeRightEdge > insertX || node.position[0] >= insertX;
|
||||
};
|
||||
|
||||
/** Checks if a node is fully contained within a sticky note's bounds */
|
||||
const isNodeInsideSticky = (
|
||||
node: INodeUi,
|
||||
sticky: INodeUi,
|
||||
stickyRect: ReturnType<typeof getNodeRect>,
|
||||
) => {
|
||||
return (
|
||||
node.position[0] >= sticky.position[0] &&
|
||||
node.position[0] + DEFAULT_NODE_SIZE[0] <= sticky.position[0] + stickyRect.width &&
|
||||
node.position[1] >= sticky.position[1] &&
|
||||
node.position[1] + DEFAULT_NODE_SIZE[1] <= sticky.position[1] + stickyRect.height
|
||||
);
|
||||
};
|
||||
|
||||
/** Checks if two centers are within threshold distance (used for node-sticky association) */
|
||||
const areCentersClose = (
|
||||
nodeCenter: { x: number; y: number },
|
||||
stickyCenter: { x: number; y: number },
|
||||
threshold: { x: number; y: number },
|
||||
) => {
|
||||
return (
|
||||
Math.abs(nodeCenter.x - stickyCenter.x) <= threshold.x &&
|
||||
Math.abs(nodeCenter.y - stickyCenter.y) <= threshold.y
|
||||
);
|
||||
};
|
||||
|
||||
/** Determines if a node is associated with a sticky (inside it or centers are close) */
|
||||
const isNodeAssociatedWithSticky = (
|
||||
node: INodeUi,
|
||||
sticky: INodeUi,
|
||||
stickyRect: ReturnType<typeof getNodeRect>,
|
||||
stickyCenter: { x: number; y: number },
|
||||
threshold: { x: number; y: number },
|
||||
) => {
|
||||
return (
|
||||
isNodeInsideSticky(node, sticky, stickyRect) ||
|
||||
areCentersClose(getNodeCenter(node), stickyCenter, threshold)
|
||||
);
|
||||
};
|
||||
|
||||
/** Returns all nodes from the given list that are associated with the sticky */
|
||||
const getAssociatedNodes = (
|
||||
sticky: INodeUi,
|
||||
stickyRect: ReturnType<typeof getNodeRect>,
|
||||
stickyCenter: { x: number; y: number },
|
||||
threshold: { x: number; y: number },
|
||||
nodesToCheck: INodeUi[],
|
||||
) => {
|
||||
return nodesToCheck.filter((node) =>
|
||||
isNodeAssociatedWithSticky(node, sticky, stickyRect, stickyCenter, threshold),
|
||||
);
|
||||
};
|
||||
|
||||
// Step 1: Find initial candidates - nodes that overlap with or are to the right of insertion
|
||||
// A node overlaps if its right edge extends into the insertion area
|
||||
const initialCandidates = allNodes.filter((node) => {
|
||||
if (node.type === STICKY_NODE_TYPE) return false;
|
||||
if (node.name === sourceNodeName) return false;
|
||||
const isSimilarY = Math.abs(node.position[1] - insertY) <= yTolerance;
|
||||
// Node overlaps or is to the right if its right edge is past the insertion X
|
||||
// or its left edge is at/past the insertion X
|
||||
const nodeRightEdge = node.position[0] + DEFAULT_NODE_SIZE[0];
|
||||
const overlapsOrIsToTheRight = nodeRightEdge > insertX || node.position[0] >= insertX;
|
||||
return isSimilarY && overlapsOrIsToTheRight;
|
||||
return isSimilarY(node) && overlapsInsertX(node);
|
||||
});
|
||||
|
||||
// Step 2: Add all downstream connected nodes from initial candidates
|
||||
const candidateNames = new Set(initialCandidates.map((n) => n.name));
|
||||
const candidateNames = new Set(initialCandidates.map((node) => node.name));
|
||||
for (const candidate of initialCandidates) {
|
||||
const downstream = workflowHelpers.getConnectedNodes(
|
||||
'downstream',
|
||||
@@ -1564,13 +1633,9 @@ export function useCanvasOperations() {
|
||||
if (!node) {
|
||||
return false;
|
||||
}
|
||||
const nodeRightEdge = node.position[0] + DEFAULT_NODE_SIZE[0];
|
||||
|
||||
// Check if the current node visually overlaps with, or is entirely to the right of,
|
||||
// the new insertion point (insertX).
|
||||
const overlapsOrIsToTheRight = nodeRightEdge > insertX || node.position[0] >= insertX;
|
||||
|
||||
return overlapsOrIsToTheRight;
|
||||
return overlapsInsertX(node);
|
||||
})
|
||||
.forEach((name) => candidateNames.add(name));
|
||||
}
|
||||
@@ -1581,18 +1646,29 @@ export function useCanvasOperations() {
|
||||
return candidateNames.has(node.name);
|
||||
});
|
||||
|
||||
// Step 4: Find sticky notes that need moving or stretching
|
||||
// Step 4: Classify sticky notes behavior
|
||||
// Stickies can: move (shift right), stretch (expand width), or both
|
||||
// Special case: stickies containing the source node are anchored (stretch only)
|
||||
const stickiesToStretch: INodeUi[] = [];
|
||||
const stickiesToMove: INodeUi[] = [];
|
||||
const stickiesToMoveAndStretch: INodeUi[] = [];
|
||||
const stickyAssociatedNodes = new Map<string, INodeUi[]>();
|
||||
const stickyNodes = allNodes.filter((node) => node.type === STICKY_NODE_TYPE);
|
||||
|
||||
// Calculate the vertical area that will be affected (insertion point + nodes being moved)
|
||||
// Calculate the vertical area affected by insertion
|
||||
const affectedMinY = Math.min(insertY, ...regularNodesToMove.map((n) => n.position[1]));
|
||||
const affectedMaxY = Math.max(
|
||||
insertY + DEFAULT_NODE_SIZE[1],
|
||||
insertY + nodeSize[1],
|
||||
...regularNodesToMove.map((n) => n.position[1] + DEFAULT_NODE_SIZE[1]),
|
||||
);
|
||||
|
||||
const sourceNode = workflowsStore.nodesByName[sourceNodeName];
|
||||
const nodeCenterThreshold = {
|
||||
x: nodeSize[0] / 2,
|
||||
y: nodeSize[1] / 2,
|
||||
};
|
||||
|
||||
// Process each sticky to determine its behavior
|
||||
for (const sticky of stickyNodes) {
|
||||
const stickyRect = getNodeRect(sticky);
|
||||
const stickyLeftEdge = sticky.position[0];
|
||||
@@ -1600,108 +1676,136 @@ export function useCanvasOperations() {
|
||||
const stickyTop = sticky.position[1];
|
||||
const stickyBottom = stickyTop + stickyRect.height;
|
||||
const overlapsVertically = !(stickyBottom <= affectedMinY || stickyTop >= affectedMaxY);
|
||||
const isInsertionInsideSticky = insertX >= stickyLeftEdge && insertX <= stickyRightEdge;
|
||||
|
||||
// Sticky should be moved if its left edge is at or past the insertion position
|
||||
if (stickyLeftEdge >= insertX && overlapsVertically) {
|
||||
stickiesToMove.push(sticky);
|
||||
}
|
||||
// Sticky should be stretched if:
|
||||
// 1. Its left edge is before the insertion position
|
||||
// 2. Its right edge extends into or past the insertion position
|
||||
// 3. It overlaps vertically with the affected area
|
||||
else if (stickyLeftEdge < insertX && stickyRightEdge >= insertX && overlapsVertically) {
|
||||
const stickyCenter = {
|
||||
x: sticky.position[0] + stickyRect.width / 2,
|
||||
y: sticky.position[1] + stickyRect.height / 2,
|
||||
};
|
||||
|
||||
// Priority 1: Stickies containing the source node are anchored (stretch only)
|
||||
const sourceNodeInsideSticky =
|
||||
sourceNode &&
|
||||
isNodeAssociatedWithSticky(
|
||||
sourceNode,
|
||||
sticky,
|
||||
stickyRect,
|
||||
stickyCenter,
|
||||
nodeCenterThreshold,
|
||||
);
|
||||
|
||||
// a left edge before the insertion position
|
||||
if (sourceNodeInsideSticky && isInsertionInsideSticky) {
|
||||
const associatedNodes = getAssociatedNodes(
|
||||
sticky,
|
||||
stickyRect,
|
||||
stickyCenter,
|
||||
nodeCenterThreshold,
|
||||
regularNodesToMove,
|
||||
);
|
||||
stickyAssociatedNodes.set(sticky.id, associatedNodes);
|
||||
stickiesToStretch.push(sticky);
|
||||
continue;
|
||||
}
|
||||
|
||||
const associatedNodes = getAssociatedNodes(
|
||||
sticky,
|
||||
stickyRect,
|
||||
stickyCenter,
|
||||
nodeCenterThreshold,
|
||||
regularNodesToMove,
|
||||
);
|
||||
|
||||
if (associatedNodes.length > 0) {
|
||||
stickyAssociatedNodes.set(sticky.id, associatedNodes);
|
||||
}
|
||||
|
||||
const associatedWithMovedNode = associatedNodes.length > 0;
|
||||
|
||||
if (!overlapsVertically) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (associatedWithMovedNode) {
|
||||
// Sticky has nodes that will move - check if new node will be close enough to the sticky
|
||||
const newNodeRightEdge = insertX + nodeSize[0];
|
||||
// If the new node's right edge is within 2/3 of PUSH_NODES_OFFSET from the sticky's left edge,
|
||||
// stretch the sticky to include the new node
|
||||
const isNewNodeCloseToSticky =
|
||||
newNodeRightEdge > stickyLeftEdge + (2 * PUSH_NODES_OFFSET) / 3;
|
||||
|
||||
if (isNewNodeCloseToSticky) {
|
||||
// New node is close enough to sticky - move AND stretch
|
||||
stickiesToMoveAndStretch.push(sticky);
|
||||
} else {
|
||||
// New node is too far from sticky - just move
|
||||
stickiesToMove.push(sticky);
|
||||
}
|
||||
} else if (isInsertionInsideSticky) {
|
||||
// Sticky overlaps insertion but has no moving nodes - just stretch
|
||||
stickiesToStretch.push(sticky);
|
||||
} else if (stickyLeftEdge >= insertX) {
|
||||
// Sticky is entirely to the right - move it
|
||||
stickiesToMove.push(sticky);
|
||||
}
|
||||
}
|
||||
|
||||
// Combine regular nodes and stickies to move
|
||||
const nodesToMove = [...regularNodesToMove, ...stickiesToMove];
|
||||
const nodesToMove = [...regularNodesToMove, ...stickiesToMove, ...stickiesToMoveAndStretch];
|
||||
|
||||
return { nodesToMove, stickiesToStretch };
|
||||
return { nodesToMove, stickiesToStretch, stickiesToMoveAndStretch, stickyAssociatedNodes };
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the insertion position overlaps with a sticky note and returns
|
||||
* an adjusted Y position that fits within the sticky's content area.
|
||||
* Only adjusts Y if there's enough space for insertion (no downstream nodes blocking).
|
||||
* Returns null if no adjustment is needed.
|
||||
*/
|
||||
function getYPositionForStickyOverlap(
|
||||
insertPosition: XYPosition,
|
||||
nodeSize: [number, number],
|
||||
): number | null {
|
||||
const allNodes = Object.values(workflowsStore.nodesByName);
|
||||
const stickyNodes = allNodes.filter((node) => node.type === STICKY_NODE_TYPE);
|
||||
const regularNodes = allNodes.filter((node) => node.type !== STICKY_NODE_TYPE);
|
||||
|
||||
// Add a small margin to detect nodes that are touching (edge-to-edge)
|
||||
const margin = GRID_SIZE;
|
||||
const insertRect = {
|
||||
x: insertPosition[0],
|
||||
y: insertPosition[1],
|
||||
width: nodeSize[0] + margin,
|
||||
height: nodeSize[1],
|
||||
};
|
||||
|
||||
// First check if there are any regular nodes blocking the insertion position
|
||||
// If so, downstream nodes will be shifted and we should keep the original Y
|
||||
for (const node of regularNodes) {
|
||||
const nodeRect = getNodeRect(node);
|
||||
if (doRectsOverlap(insertRect, nodeRect)) {
|
||||
// There's a regular node blocking - don't adjust Y, let shift logic handle it
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// No regular nodes blocking - check if we need to adjust Y for sticky overlap
|
||||
for (const sticky of stickyNodes) {
|
||||
const stickyRect = getNodeRect(sticky);
|
||||
|
||||
// Check if insertion position overlaps with this sticky
|
||||
if (doRectsOverlap(insertRect, stickyRect)) {
|
||||
// Sticky header takes up approximately 40px (title area)
|
||||
const STICKY_HEADER_HEIGHT = 40;
|
||||
const stickyContentTop = sticky.position[1] + STICKY_HEADER_HEIGHT;
|
||||
const stickyContentBottom = sticky.position[1] + stickyRect.height - GRID_SIZE;
|
||||
|
||||
// Calculate the ideal Y position centered in the sticky's content area
|
||||
const contentHeight = stickyContentBottom - stickyContentTop;
|
||||
const centeredY = stickyContentTop + (contentHeight - nodeSize[1]) / 2;
|
||||
|
||||
// Ensure the position is grid-aligned
|
||||
return Math.round(centeredY / GRID_SIZE) * GRID_SIZE;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stretches a sticky note by increasing its width
|
||||
* Stretches a sticky note horizontally to ensure it surrounds the inserted node
|
||||
* and all associated nodes with padding.
|
||||
*/
|
||||
function stretchStickyNote(
|
||||
sticky: INodeUi,
|
||||
stretchAmount: number,
|
||||
insertPosition: XYPosition,
|
||||
nodeSize: [number, number],
|
||||
associatedNodes: INodeUi[],
|
||||
{ trackHistory = false }: { trackHistory?: boolean },
|
||||
) {
|
||||
const currentWidth = (sticky.parameters.width as number) || DEFAULT_NODE_SIZE[0];
|
||||
const newWidth = currentWidth + stretchAmount;
|
||||
const padding = 20;
|
||||
const stickyRect = getNodeRect(sticky);
|
||||
const currentLeft = sticky.position[0];
|
||||
const currentRight = currentLeft + stickyRect.width;
|
||||
|
||||
const newParameters = {
|
||||
...sticky.parameters,
|
||||
width: newWidth,
|
||||
};
|
||||
// Start with insertion bounds
|
||||
let targetLeft = insertPosition[0] - padding;
|
||||
let targetRight = insertPosition[0] + nodeSize[0] + padding;
|
||||
|
||||
replaceNodeParameters(sticky.id, sticky.parameters as INodeParameters, newParameters, {
|
||||
// Expand to include all associated nodes (get fresh positions from store as they may have moved)
|
||||
for (const node of associatedNodes) {
|
||||
const updatedNode = workflowsStore.getNodeById(node.id);
|
||||
if (!updatedNode) continue;
|
||||
const nodeLeft = updatedNode.position[0] - padding;
|
||||
const nodeRight = updatedNode.position[0] + DEFAULT_NODE_SIZE[0] + padding;
|
||||
targetLeft = Math.min(targetLeft, nodeLeft);
|
||||
targetRight = Math.max(targetRight, nodeRight);
|
||||
}
|
||||
|
||||
const newLeft = Math.min(currentLeft, targetLeft);
|
||||
const newRight = Math.max(currentRight, targetRight);
|
||||
const newWidth = newRight - newLeft;
|
||||
|
||||
const newParameters: INodeParameters = { ...sticky.parameters, width: newWidth };
|
||||
replaceNodeParameters(sticky.id, sticky.parameters, newParameters, {
|
||||
trackHistory,
|
||||
trackBulk: false,
|
||||
});
|
||||
|
||||
if (newLeft !== currentLeft) {
|
||||
updateNodePosition(sticky.id, { x: newLeft, y: sticky.position[1] }, { trackHistory });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves downstream nodes when inserting a node between existing nodes.
|
||||
* Only moves nodes if there isn't enough space for the new node.
|
||||
* Sticky notes that overlap the insertion area are stretched instead of moved.
|
||||
* Stickies at the insertion point are repositioned behind the new node.
|
||||
*/
|
||||
function shiftDownstreamNodesPosition(
|
||||
sourceNodeName: string,
|
||||
@@ -1732,9 +1836,10 @@ export function useCanvasOperations() {
|
||||
}
|
||||
|
||||
// Get nodes to shift and stickies to stretch
|
||||
const { nodesToMove, stickiesToStretch } = getNodesToShift(insertPosition, sourceNodeName);
|
||||
const { nodesToMove, stickiesToStretch, stickiesToMoveAndStretch, stickyAssociatedNodes } =
|
||||
getNodesToShift(insertPosition, sourceNodeName, nodeSize);
|
||||
|
||||
// Move regular nodes to the right
|
||||
// Move regular nodes and stickies to the right
|
||||
for (const node of nodesToMove) {
|
||||
updateNodePosition(
|
||||
node.id,
|
||||
@@ -1746,9 +1851,18 @@ export function useCanvasOperations() {
|
||||
);
|
||||
}
|
||||
|
||||
// Stretch sticky notes instead of moving them
|
||||
// Stretch stickies that moved and also need to encompass the new node
|
||||
for (const sticky of stickiesToMoveAndStretch) {
|
||||
const updatedSticky = workflowsStore.getNodeById(sticky.id);
|
||||
if (!updatedSticky) continue;
|
||||
const associatedNodes = stickyAssociatedNodes.get(sticky.id) ?? [];
|
||||
stretchStickyNote(updatedSticky, insertPosition, nodeSize, associatedNodes, { trackHistory });
|
||||
}
|
||||
|
||||
// Stretch sticky notes that span the insertion area
|
||||
for (const sticky of stickiesToStretch) {
|
||||
stretchStickyNote(sticky, margin, { trackHistory });
|
||||
const associatedNodes = stickyAssociatedNodes.get(sticky.id) ?? [];
|
||||
stretchStickyNote(sticky, insertPosition, nodeSize, associatedNodes, { trackHistory });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+69
@@ -55,4 +55,73 @@ test.describe('PAY-4367: Node shifting in cyclic workflows', () => {
|
||||
await expect(n8n.canvas.nodeByName('HTTP Request')).toBeVisible();
|
||||
await expect(n8n.canvas.getCanvasNodes()).toHaveCount(5);
|
||||
});
|
||||
|
||||
test('should stretch sticky note when inserting node in front of it', async ({ n8n }) => {
|
||||
// Workflow with a pink sticky note ("Sticky Note14") between Edit Fields and A node
|
||||
// The sticky should stretch to encompass the new node when inserted close to it
|
||||
await n8n.start.fromBlankCanvas();
|
||||
await n8n.canvas.importWorkflow('Bug_node_insertions_sticky.json', 'Sticky Insert Test');
|
||||
|
||||
const pinkSticky = n8n.canvas.sticky.getStickies().filter({ hasText: 'Insert here' });
|
||||
await expect(pinkSticky).toBeVisible();
|
||||
|
||||
const stickyBefore = await pinkSticky.boundingBox();
|
||||
|
||||
// ACT: Insert node between Edit Fields and A (in front of the pink sticky)
|
||||
await n8n.canvas.addNodeBetweenNodes('Edit Fields', 'A', 'HTTP Request');
|
||||
|
||||
const stickyAfter = await pinkSticky.boundingBox();
|
||||
|
||||
// ASSERT: Sticky should have stretched (width increased) to encompass the new node
|
||||
await expect(n8n.canvas.nodeByName('HTTP Request')).toBeVisible();
|
||||
expect(stickyAfter?.width).toBeGreaterThan(stickyBefore?.width ?? 0);
|
||||
|
||||
const newNode = await n8n.canvas.nodeByName('HTTP Request').boundingBox();
|
||||
|
||||
// The new node should be horizontally between the sticky's left and right edges
|
||||
// (with some tolerance for padding/stretching)
|
||||
expect(newNode?.x).toBeGreaterThanOrEqual((stickyAfter?.x ?? 0) - 50);
|
||||
expect((newNode?.x ?? 0) + (newNode?.width ?? 0)).toBeLessThanOrEqual(
|
||||
(stickyAfter?.x ?? 0) + (stickyAfter?.width ?? 0) + 50,
|
||||
);
|
||||
});
|
||||
|
||||
test('should not associate node with stickies when inserting between two separate sticky notes', async ({
|
||||
n8n,
|
||||
}) => {
|
||||
// Workflow with two sticky notes: "Sticky Note20" (pink) and "Note for A5" (yellow)
|
||||
// Inserting a node between "Get a post3" and "A5" should place it in the gap between stickies
|
||||
// The node should NOT be associated with either sticky (no stretching)
|
||||
await n8n.start.fromBlankCanvas();
|
||||
await n8n.canvas.importWorkflow(
|
||||
'Bug_node_insertions_between_stickies.json',
|
||||
'Between Stickies Test',
|
||||
);
|
||||
|
||||
const pinkSticky = n8n.canvas.sticky.getStickies().filter({ hasText: 'Insert here' });
|
||||
const yellowSticky = n8n.canvas.sticky.getStickies().filter({ hasText: 'Note for A' });
|
||||
|
||||
await expect(pinkSticky).toBeVisible();
|
||||
await expect(yellowSticky).toBeVisible();
|
||||
|
||||
const pinkStickyBefore = await pinkSticky.boundingBox();
|
||||
const yellowStickyBefore = await yellowSticky.boundingBox();
|
||||
|
||||
// ACT: Insert node between "Get a post3" and "A5" (in the gap between the two stickies)
|
||||
await n8n.canvas.addNodeBetweenNodes('Get a post3', 'A5', 'HTTP Request');
|
||||
await expect(n8n.canvas.nodeByName('HTTP Request')).toBeVisible();
|
||||
|
||||
const pinkStickyAfter = await pinkSticky.boundingBox();
|
||||
const yellowStickyAfter = await yellowSticky.boundingBox();
|
||||
|
||||
//Stickies should both maintain their width (not stretch to include the new node)
|
||||
expect(pinkStickyAfter?.width).toBe(pinkStickyBefore?.width);
|
||||
expect(yellowStickyAfter?.width).toBe(yellowStickyBefore?.width);
|
||||
|
||||
const newNode = await n8n.canvas.nodeByName('HTTP Request').boundingBox();
|
||||
|
||||
// The new node should be between the pink and yellow stickies
|
||||
expect(newNode?.x).toBeGreaterThan((pinkStickyAfter?.x ?? 0) + (pinkStickyAfter?.width ?? 0));
|
||||
expect((newNode?.x ?? 0) + (newNode?.width ?? 0)).toBeLessThan(yellowStickyAfter?.x ?? 0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
{
|
||||
"name": "Bug - node insertions",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {
|
||||
"options": {}
|
||||
},
|
||||
"type": "n8n-nodes-base.splitInBatches",
|
||||
"typeVersion": 3,
|
||||
"position": [3872, 3056],
|
||||
"id": "e8bf4c20-c8e8-4fa6-9466-bf63548c7a7a",
|
||||
"name": "Loop Over Items5"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"options": {}
|
||||
},
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.4,
|
||||
"position": [4096, 3056],
|
||||
"id": "544c42bd-155e-416c-abf5-5f15e6a4c0f9",
|
||||
"name": "Edit Fields2"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"options": {}
|
||||
},
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.4,
|
||||
"position": [4528, 3056],
|
||||
"id": "08735c37-aae9-4dd0-afea-4c4f08e37f72",
|
||||
"name": "A5"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"content": "## Note for A",
|
||||
"height": 224,
|
||||
"width": 160
|
||||
},
|
||||
"type": "n8n-nodes-base.stickyNote",
|
||||
"position": [4496, 2992],
|
||||
"typeVersion": 1,
|
||||
"id": "1ae636b2-eeae-4378-88a0-b4d49c38a4d8",
|
||||
"name": "Note for A5"
|
||||
},
|
||||
{
|
||||
"parameters": {},
|
||||
"type": "n8n-nodes-base.noOp",
|
||||
"typeVersion": 1,
|
||||
"position": [4528, 3296],
|
||||
"id": "13d29e35-b6f1-4128-b656-db970718543d",
|
||||
"name": "C10"
|
||||
},
|
||||
{
|
||||
"parameters": {},
|
||||
"type": "n8n-nodes-base.noOp",
|
||||
"typeVersion": 1,
|
||||
"position": [3872, 3328],
|
||||
"id": "a822d276-6391-49ce-bd15-7eb41b2c5a36",
|
||||
"name": "C11"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"content": "Insert here",
|
||||
"width": 246,
|
||||
"color": 3
|
||||
},
|
||||
"type": "n8n-nodes-base.stickyNote",
|
||||
"position": [4176, 3024],
|
||||
"typeVersion": 1,
|
||||
"id": "17b0cc51-d867-4a55-b9a1-68625839759f",
|
||||
"name": "Sticky Note20"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"operation": "get"
|
||||
},
|
||||
"type": "n8n-nodes-base.reddit",
|
||||
"typeVersion": 1,
|
||||
"position": [4304, 3056],
|
||||
"id": "e1129855-44a1-4967-b271-79c949e9a365",
|
||||
"name": "Get a post3"
|
||||
}
|
||||
],
|
||||
"pinData": {},
|
||||
"connections": {
|
||||
"Loop Over Items5": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Edit Fields2",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Edit Fields2": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Get a post3",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"A5": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "C10",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"C10": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "C11",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"C11": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Loop Over Items5",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Get a post3": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "A5",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {
|
||||
"executionOrder": "v1",
|
||||
"binaryMode": "separate",
|
||||
"availableInMCP": false
|
||||
},
|
||||
"versionId": "1362ec5c-52ae-47af-8169-ad6410e3e2eb",
|
||||
"meta": {
|
||||
"templateCredsSetupCompleted": true,
|
||||
"instanceId": "0113851022152b5a0ec6c5db30b5953a2e16759b15b7f941dc4f60ecce106633"
|
||||
},
|
||||
"id": "gNUjNMHkr4jvR2MIrzu78",
|
||||
"tags": []
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
{
|
||||
"name": "Bug - node insertions",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {
|
||||
"options": {}
|
||||
},
|
||||
"type": "n8n-nodes-base.splitInBatches",
|
||||
"typeVersion": 3,
|
||||
"position": [1472, 736],
|
||||
"id": "16e710d5-b78b-4e57-b044-bc00f0a19dfd",
|
||||
"name": "Loop Over Items"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"options": {}
|
||||
},
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.4,
|
||||
"position": [1696, 736],
|
||||
"id": "3caf8c4e-c605-4939-bd4d-12c20025f9ed",
|
||||
"name": "Edit Fields"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"options": {}
|
||||
},
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.4,
|
||||
"position": [1904, 736],
|
||||
"id": "02cf7192-ce83-4efd-a45d-8ef0f85d24d4",
|
||||
"name": "A"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"content": "## Note for A",
|
||||
"height": 224,
|
||||
"width": 160
|
||||
},
|
||||
"type": "n8n-nodes-base.stickyNote",
|
||||
"position": [1872, 672],
|
||||
"typeVersion": 1,
|
||||
"id": "f0cd8fa7-4838-49e1-a428-a7109ebc48ad",
|
||||
"name": "Note for A"
|
||||
},
|
||||
{
|
||||
"parameters": {},
|
||||
"type": "n8n-nodes-base.noOp",
|
||||
"typeVersion": 1,
|
||||
"position": [1904, 976],
|
||||
"id": "b51783d1-9ee8-424e-a2f0-a64189920aed",
|
||||
"name": "C"
|
||||
},
|
||||
{
|
||||
"parameters": {},
|
||||
"type": "n8n-nodes-base.noOp",
|
||||
"typeVersion": 1,
|
||||
"position": [1472, 1008],
|
||||
"id": "e0a704c5-4796-456c-b58a-1b8ef0394877",
|
||||
"name": "C1"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"content": "Insert here",
|
||||
"width": 150,
|
||||
"color": 3
|
||||
},
|
||||
"type": "n8n-nodes-base.stickyNote",
|
||||
"position": [1776, 704],
|
||||
"typeVersion": 1,
|
||||
"id": "e246f382-08ca-4a55-bb2e-af1140c8b15d",
|
||||
"name": "Sticky Note14"
|
||||
}
|
||||
],
|
||||
"pinData": {},
|
||||
"connections": {
|
||||
"Loop Over Items": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Edit Fields",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Edit Fields": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "A",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"A": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "C",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"C": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "C1",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"C1": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Loop Over Items",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {
|
||||
"executionOrder": "v1",
|
||||
"binaryMode": "separate",
|
||||
"availableInMCP": false
|
||||
},
|
||||
"versionId": "cfab06c6-1663-4cb5-b6f8-0d1150d0e7d7",
|
||||
"meta": {
|
||||
"templateCredsSetupCompleted": true,
|
||||
"instanceId": "0113851022152b5a0ec6c5db30b5953a2e16759b15b7f941dc4f60ecce106633"
|
||||
},
|
||||
"id": "gNUjNMHkr4jvR2MIrzu78",
|
||||
"tags": []
|
||||
}
|
||||
Reference in New Issue
Block a user