From 7245213ed6e44b9acbb2b390c0c245f1c4b0f4d7 Mon Sep 17 00:00:00 2001 From: Alexander Brown Date: Wed, 10 Sep 2025 23:17:06 -0700 Subject: [PATCH 1/2] Fix: In standard mode, don't stop when you hit a Vue node. (#5445) * fix: Forward the scrolling events to the litegraph canvas. * prior-art: Use the existing event forwarding logic from useCanvasInteractions (h/t Ben) * fix: Get proper scaling from properties in the original event, fix browser zoom * tests: Fix missing property on mock * types: Cleanup type annotations in the test * cleanup: Initialize the mocks in place. * tests: extract createMockPointerEvent * tests: extract createMockWheelEvent * tests: extract createMockLGraphCanvas * tests: Add additional assertion for stopPropagation * tests: Comment pruning, test rename suggested by @arjansingh --- src/components/graph/GraphCanvas.vue | 4 + .../graph/useCanvasInteractions.ts | 25 ++- .../graph/useCanvasInteractions.test.ts | 160 +++++++----------- 3 files changed, 88 insertions(+), 101 deletions(-) diff --git a/src/components/graph/GraphCanvas.vue b/src/components/graph/GraphCanvas.vue index ee9bea3ba..28509a3ab 100644 --- a/src/components/graph/GraphCanvas.vue +++ b/src/components/graph/GraphCanvas.vue @@ -36,6 +36,7 @@ v-if="isVueNodesEnabled && comfyApp.canvas && comfyAppReady" :canvas="comfyApp.canvas" @transform-update="handleTransformUpdate" + @wheel.capture="canvasInteractions.forwardEventToCanvas" > settingStore.get('Comfy.UseNewMenu') !== 'Disabled' ) diff --git a/src/composables/graph/useCanvasInteractions.ts b/src/composables/graph/useCanvasInteractions.ts index 79edb1fb3..05d0b7e28 100644 --- a/src/composables/graph/useCanvasInteractions.ts +++ b/src/composables/graph/useCanvasInteractions.ts @@ -24,14 +24,12 @@ export function useCanvasInteractions() { const handleWheel = (event: WheelEvent) => { // In standard mode, Ctrl+wheel should go to canvas for zoom if (isStandardNavMode.value && (event.ctrlKey || event.metaKey)) { - event.preventDefault() // Prevent browser zoom forwardEventToCanvas(event) return } // In legacy mode, all wheel events go to canvas for zoom if (!isStandardNavMode.value) { - event.preventDefault() forwardEventToCanvas(event) return } @@ -68,9 +66,30 @@ export function useCanvasInteractions() { ) => { const canvasEl = app.canvas?.canvas if (!canvasEl) return + event.preventDefault() + event.stopPropagation() + + if (event instanceof WheelEvent) { + const { clientX, clientY, deltaX, deltaY, ctrlKey, metaKey, shiftKey } = + event + canvasEl.dispatchEvent( + new WheelEvent('wheel', { + clientX, + clientY, + deltaX, + deltaY, + ctrlKey, + metaKey, + shiftKey + }) + ) + return + } // Create new event with same properties - const EventConstructor = event.constructor as typeof WheelEvent + const EventConstructor = event.constructor as + | typeof MouseEvent + | typeof PointerEvent const newEvent = new EventConstructor(event.type, event) canvasEl.dispatchEvent(newEvent) } diff --git a/tests-ui/tests/composables/graph/useCanvasInteractions.test.ts b/tests-ui/tests/composables/graph/useCanvasInteractions.test.ts index 47e6fb75d..b2c670f3e 100644 --- a/tests-ui/tests/composables/graph/useCanvasInteractions.test.ts +++ b/tests-ui/tests/composables/graph/useCanvasInteractions.test.ts @@ -1,12 +1,19 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { useCanvasInteractions } from '@/composables/graph/useCanvasInteractions' +import type { LGraphCanvas } from '@/lib/litegraph/src/litegraph' import { useCanvasStore } from '@/stores/graphStore' import { useSettingStore } from '@/stores/settingStore' // Mock stores -vi.mock('@/stores/graphStore') -vi.mock('@/stores/settingStore') +vi.mock('@/stores/graphStore', () => { + const getCanvas = vi.fn() + return { useCanvasStore: vi.fn(() => ({ getCanvas })) } +}) +vi.mock('@/stores/settingStore', () => { + const getFn = vi.fn() + return { useSettingStore: vi.fn(() => ({ get: getFn })) } +}) vi.mock('@/scripts/app', () => ({ app: { canvas: { @@ -17,105 +24,86 @@ vi.mock('@/scripts/app', () => ({ } })) +function createMockLGraphCanvas(read_only = true): LGraphCanvas { + const mockCanvas: Partial = { read_only } + return mockCanvas as LGraphCanvas +} + +function createMockPointerEvent( + buttons: PointerEvent['buttons'] = 1 +): PointerEvent { + const mockEvent: Partial = { + buttons, + preventDefault: vi.fn(), + stopPropagation: vi.fn() + } + return mockEvent as PointerEvent +} + +function createMockWheelEvent(ctrlKey = false, metaKey = false): WheelEvent { + const mockEvent: Partial = { + ctrlKey, + metaKey, + preventDefault: vi.fn(), + stopPropagation: vi.fn() + } + return mockEvent as WheelEvent +} + describe('useCanvasInteractions', () => { beforeEach(() => { - vi.clearAllMocks() - vi.mocked(useCanvasStore, { partial: true }).mockReturnValue({ - getCanvas: vi.fn() - }) - vi.mocked(useSettingStore, { partial: true }).mockReturnValue({ - get: vi.fn() - }) + vi.resetAllMocks() }) describe('handlePointer', () => { - it('should forward space+drag events to canvas when read_only is true', () => { - // Setup - const mockCanvas = { read_only: true } + it('should intercept left mouse events when canvas is read_only to enable space+drag navigation', () => { const { getCanvas } = useCanvasStore() - vi.mocked(getCanvas).mockReturnValue(mockCanvas as any) + const mockCanvas = createMockLGraphCanvas(true) + vi.mocked(getCanvas).mockReturnValue(mockCanvas) const { handlePointer } = useCanvasInteractions() - // Create mock pointer event - const mockEvent = { - buttons: 1, // Left mouse button - preventDefault: vi.fn(), - stopPropagation: vi.fn() - } satisfies Partial + const mockEvent = createMockPointerEvent(1) // Left Mouse Button + handlePointer(mockEvent) - // Test - handlePointer(mockEvent as unknown as PointerEvent) - - // Verify expect(mockEvent.preventDefault).toHaveBeenCalled() expect(mockEvent.stopPropagation).toHaveBeenCalled() }) it('should forward middle mouse button events to canvas', () => { - // Setup - const mockCanvas = { read_only: false } const { getCanvas } = useCanvasStore() - vi.mocked(getCanvas).mockReturnValue(mockCanvas as any) - + const mockCanvas = createMockLGraphCanvas(false) + vi.mocked(getCanvas).mockReturnValue(mockCanvas) const { handlePointer } = useCanvasInteractions() - // Create mock pointer event with middle button - const mockEvent = { - buttons: 4, // Middle mouse button - preventDefault: vi.fn(), - stopPropagation: vi.fn() - } satisfies Partial + const mockEvent = createMockPointerEvent(4) // Middle mouse button + handlePointer(mockEvent) - // Test - handlePointer(mockEvent as unknown as PointerEvent) - - // Verify expect(mockEvent.preventDefault).toHaveBeenCalled() expect(mockEvent.stopPropagation).toHaveBeenCalled() }) it('should not prevent default when canvas is not in read_only mode and not middle button', () => { - // Setup - const mockCanvas = { read_only: false } const { getCanvas } = useCanvasStore() - vi.mocked(getCanvas).mockReturnValue(mockCanvas as any) - + const mockCanvas = createMockLGraphCanvas(false) + vi.mocked(getCanvas).mockReturnValue(mockCanvas) const { handlePointer } = useCanvasInteractions() - // Create mock pointer event - const mockEvent = { - buttons: 1, // Left mouse button - preventDefault: vi.fn(), - stopPropagation: vi.fn() - } satisfies Partial + const mockEvent = createMockPointerEvent(1) + handlePointer(mockEvent) - // Test - handlePointer(mockEvent as unknown as PointerEvent) - - // Verify - should not prevent default (let media handle normally) expect(mockEvent.preventDefault).not.toHaveBeenCalled() expect(mockEvent.stopPropagation).not.toHaveBeenCalled() }) it('should return early when canvas is null', () => { - // Setup const { getCanvas } = useCanvasStore() - vi.mocked(getCanvas).mockReturnValue(null as any) - + vi.mocked(getCanvas).mockReturnValue(null as unknown as LGraphCanvas) // TODO: Fix misaligned types const { handlePointer } = useCanvasInteractions() - // Create mock pointer event that would normally trigger forwarding - const mockEvent = { - buttons: 1, // Left mouse button - would trigger space+drag if canvas had read_only=true - preventDefault: vi.fn(), - stopPropagation: vi.fn() - } satisfies Partial + const mockEvent = createMockPointerEvent(1) + handlePointer(mockEvent) - // Test - handlePointer(mockEvent as unknown as PointerEvent) - - // Verify early return - no event methods should be called at all expect(getCanvas).toHaveBeenCalled() expect(mockEvent.preventDefault).not.toHaveBeenCalled() expect(mockEvent.stopPropagation).not.toHaveBeenCalled() @@ -124,66 +112,42 @@ describe('useCanvasInteractions', () => { describe('handleWheel', () => { it('should forward ctrl+wheel events to canvas in standard nav mode', () => { - // Setup const { get } = useSettingStore() vi.mocked(get).mockReturnValue('standard') const { handleWheel } = useCanvasInteractions() - // Create mock wheel event with ctrl key - const mockEvent = { - ctrlKey: true, - metaKey: false, - preventDefault: vi.fn() - } satisfies Partial + // Ctrl key pressed + const mockEvent = createMockWheelEvent(true) - // Test - handleWheel(mockEvent as unknown as WheelEvent) + handleWheel(mockEvent) - // Verify expect(mockEvent.preventDefault).toHaveBeenCalled() + expect(mockEvent.stopPropagation).toHaveBeenCalled() }) it('should forward all wheel events to canvas in legacy nav mode', () => { - // Setup const { get } = useSettingStore() vi.mocked(get).mockReturnValue('legacy') - const { handleWheel } = useCanvasInteractions() - // Create mock wheel event without modifiers - const mockEvent = { - ctrlKey: false, - metaKey: false, - preventDefault: vi.fn() - } satisfies Partial + const mockEvent = createMockWheelEvent() + handleWheel(mockEvent) - // Test - handleWheel(mockEvent as unknown as WheelEvent) - - // Verify expect(mockEvent.preventDefault).toHaveBeenCalled() + expect(mockEvent.stopPropagation).toHaveBeenCalled() }) it('should not prevent default for regular wheel events in standard nav mode', () => { - // Setup const { get } = useSettingStore() vi.mocked(get).mockReturnValue('standard') - const { handleWheel } = useCanvasInteractions() - // Create mock wheel event without modifiers - const mockEvent = { - ctrlKey: false, - metaKey: false, - preventDefault: vi.fn() - } satisfies Partial + const mockEvent = createMockWheelEvent() + handleWheel(mockEvent) - // Test - handleWheel(mockEvent as unknown as WheelEvent) - - // Verify - should not prevent default (let component handle normally) expect(mockEvent.preventDefault).not.toHaveBeenCalled() + expect(mockEvent.stopPropagation).not.toHaveBeenCalled() }) }) }) From 6ea021d595142e5e0b1468273ea81c50961749ea Mon Sep 17 00:00:00 2001 From: Jin Yi Date: Thu, 11 Sep 2025 15:19:04 +0900 Subject: [PATCH 2/2] feat: Auto-close LoadWorkflowWarning dialog when all missing nodes are installed (#5321) * feat: Auto-close LoadWorkflowWarning dialog when all missing nodes are installed - Add computed property to check if all missing nodes are installed - Watch for completion and automatically close dialog with 500ms delay - Show success toast notification when installation completes - Add translation key for success message This improves UX by automatically dismissing the warning dialog once the user has successfully installed all missing nodes through the manager. * fix: settimeout to nexttick * [auto-fix] Apply ESLint and Prettier fixes --------- Co-authored-by: GitHub Action --- .../dialog/content/LoadWorkflowWarning.vue | 34 ++++++++++++++++++- src/locales/en/main.json | 1 + 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/src/components/dialog/content/LoadWorkflowWarning.vue b/src/components/dialog/content/LoadWorkflowWarning.vue index 54a006bee..cc159292a 100644 --- a/src/components/dialog/content/LoadWorkflowWarning.vue +++ b/src/components/dialog/content/LoadWorkflowWarning.vue @@ -53,13 +53,16 @@