Compare commits

...

4 Commits

Author SHA1 Message Date
github-actions
2601ee307d [automated] Update test expectations 2026-05-07 08:58:39 +00:00
Kelly Yang
acc68e478a fix: use processSelect to trigger onSelectionChange after node placement
canvas.select() only sets selectionChanged flag but never calls
onSelectionChange or setDirty, so Vue reactive state and the canvas
render were never updated. processSelect() completes all three steps
in the correct order.
2026-05-07 01:45:19 -07:00
Kelly Yang
a11a2841ed fix: defer node selection to nextTick to outlast processMouseUp deselectAll
The click-to-place flow fires pointerup on the canvas after endDrag
places the node. processMouseDown had already recorded the position as
empty canvas and set pointer.onClick to processSelect(null), which calls
deselectAll() in processMouseUp - overwriting any selection we set
synchronously in addNodeOnGraph.

Deferring the select/deselectAll to the next microtask (nextTick) ensures
it runs after processMouseUp completes, so the newly placed node ends up
selected regardless of how the canvas event cycle resolved.
2026-05-07 01:20:40 -07:00
Kelly Yang
f71fb2e9dd fix: select node after placing it on graph from node library
After placement via click or drag from the node library sidebar, the
node was not entered into canvas.selected_nodes. The ghost path already
handled this via startGhostPlacement, but the direct-add path had no
equivalent selection call.

Call canvas.deselectAll() + canvas.select(node) after graph.add() for
all non-ghost placements so the behavior is consistent regardless of
how a node is added.
2026-05-07 01:04:01 -07:00
9 changed files with 79 additions and 1 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

After

Width:  |  Height:  |  Size: 96 KiB

View File

@@ -2,13 +2,34 @@ import { createTestingPinia } from '@pinia/testing'
import { setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest'
const mockProcessSelect = vi.hoisted(() => vi.fn())
const mockGraphAdd = vi.hoisted(() => vi.fn())
vi.mock('@/scripts/app', () => ({
app: { canvas: undefined },
app: { canvas: undefined, graph: null },
ComfyApp: class {}
}))
vi.mock('@/renderer/core/canvas/canvasStore', () => ({
useCanvasStore: vi.fn(() => ({
canvas: { processSelect: mockProcessSelect }
}))
}))
vi.mock('@/platform/workflow/management/stores/workflowStore', () => ({
useWorkflowStore: vi.fn(() => ({ activeSubgraph: null }))
}))
vi.mock('@/stores/subgraphStore', () => ({
useSubgraphStore: vi.fn(() => ({ typePrefix: 'SubgraphBlueprint.' }))
}))
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
import type { ComfyNodeDef as ComfyNodeDefV1 } from '@/schemas/nodeDefSchema'
import { app } from '@/scripts/app'
import { useLitegraphService } from '@/services/litegraphService'
import { nextTick } from 'vue'
describe('useLitegraphService().getCanvasCenter', () => {
beforeEach(() => {
@@ -41,3 +62,50 @@ describe('useLitegraphService().getCanvasCenter', () => {
expect(center).toEqual([110, 70])
})
})
describe('useLitegraphService().addNodeOnGraph', () => {
beforeEach(() => {
setActivePinia(createTestingPinia({ stubActions: false }))
mockProcessSelect.mockReset()
mockGraphAdd.mockReset()
Reflect.set(app, 'canvas', undefined)
Reflect.set(app, 'graph', { add: mockGraphAdd })
})
it('selects the node after placing it on the graph', async () => {
const fakeNode = { id: 1, flags: {} }
vi.spyOn(LiteGraph, 'createNode').mockReturnValue(
fakeNode as unknown as LGraphNode
)
const nodeDef = {
name: 'TestNode',
display_name: 'Test Node'
} as unknown as ComfyNodeDefV1
useLitegraphService().addNodeOnGraph(nodeDef, { pos: [0, 0] })
await nextTick()
expect(mockProcessSelect).toHaveBeenCalledOnce()
expect(mockProcessSelect).toHaveBeenCalledWith(fakeNode, undefined)
})
it('does not select the node when placing in ghost mode', async () => {
const fakeNode = { id: 1, flags: {} }
vi.spyOn(LiteGraph, 'createNode').mockReturnValue(
fakeNode as unknown as LGraphNode
)
const nodeDef = {
name: 'TestNode',
display_name: 'Test Node'
} as unknown as ComfyNodeDefV1
useLitegraphService().addNodeOnGraph(
nodeDef,
{ pos: [0, 0] },
{ ghost: true }
)
await nextTick()
expect(mockProcessSelect).not.toHaveBeenCalled()
})
})

View File

@@ -62,6 +62,8 @@ import { useSubgraphStore } from '@/stores/subgraphStore'
import { useFavoritedWidgetsStore } from '@/stores/workspace/favoritedWidgetsStore'
import { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore'
import { useWidgetStore } from '@/stores/widgetStore'
import { nextTick } from 'vue'
import { normalizeI18nKey } from '@/utils/formatUtil'
import {
isAnimatedOutput,
@@ -944,6 +946,14 @@ export const useLitegraphService = () => {
if (!graph || !node) return null
graph.add(node, addOptions)
if (!addOptions?.ghost) {
const canvas = canvasStore.canvas
if (canvas) {
void nextTick(() => {
canvas.processSelect(node, undefined)
})
}
}
return node
}