mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-04 21:22:07 +00:00
Compare commits
3 Commits
fix/load-a
...
allow-copy
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
633011ed4a | ||
|
|
12bdf8971e | ||
|
|
4d012f5fd9 |
@@ -13,6 +13,7 @@ import { ComfyTemplates } from '../helpers/templates'
|
|||||||
import { ComfyMouse } from './ComfyMouse'
|
import { ComfyMouse } from './ComfyMouse'
|
||||||
import { VueNodeHelpers } from './VueNodeHelpers'
|
import { VueNodeHelpers } from './VueNodeHelpers'
|
||||||
import { ComfyNodeSearchBox } from './components/ComfyNodeSearchBox'
|
import { ComfyNodeSearchBox } from './components/ComfyNodeSearchBox'
|
||||||
|
import { Minimap } from './components/Minimap'
|
||||||
import { SettingDialog } from './components/SettingDialog'
|
import { SettingDialog } from './components/SettingDialog'
|
||||||
import {
|
import {
|
||||||
NodeLibrarySidebarTab,
|
NodeLibrarySidebarTab,
|
||||||
@@ -33,6 +34,7 @@ class ComfyMenu {
|
|||||||
private _workflowsTab: WorkflowsSidebarTab | null = null
|
private _workflowsTab: WorkflowsSidebarTab | null = null
|
||||||
private _queueTab: QueueSidebarTab | null = null
|
private _queueTab: QueueSidebarTab | null = null
|
||||||
private _topbar: Topbar | null = null
|
private _topbar: Topbar | null = null
|
||||||
|
private _minimap: Minimap | null = null
|
||||||
|
|
||||||
public readonly sideToolbar: Locator
|
public readonly sideToolbar: Locator
|
||||||
public readonly themeToggleButton: Locator
|
public readonly themeToggleButton: Locator
|
||||||
@@ -70,6 +72,11 @@ class ComfyMenu {
|
|||||||
return this._topbar
|
return this._topbar
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get minimap() {
|
||||||
|
this._minimap ??= new Minimap(this.page)
|
||||||
|
return this._minimap
|
||||||
|
}
|
||||||
|
|
||||||
async toggleTheme() {
|
async toggleTheme() {
|
||||||
await this.themeToggleButton.click()
|
await this.themeToggleButton.click()
|
||||||
await this.page.evaluate(() => {
|
await this.page.evaluate(() => {
|
||||||
|
|||||||
41
browser_tests/fixtures/components/Minimap.ts
Normal file
41
browser_tests/fixtures/components/Minimap.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import type { Locator, Page } from '@playwright/test'
|
||||||
|
|
||||||
|
export class Minimap {
|
||||||
|
constructor(public readonly page: Page) {}
|
||||||
|
|
||||||
|
get mainContainer(): Locator {
|
||||||
|
return this.page.locator('.minimap-main-container')
|
||||||
|
}
|
||||||
|
|
||||||
|
get container(): Locator {
|
||||||
|
return this.page.locator('.litegraph-minimap')
|
||||||
|
}
|
||||||
|
|
||||||
|
get canvas(): Locator {
|
||||||
|
return this.container.locator('.minimap-canvas')
|
||||||
|
}
|
||||||
|
|
||||||
|
get viewport(): Locator {
|
||||||
|
return this.container.locator('.minimap-viewport')
|
||||||
|
}
|
||||||
|
|
||||||
|
get settingsButton(): Locator {
|
||||||
|
return this.container.getByRole('button').first()
|
||||||
|
}
|
||||||
|
|
||||||
|
get closeButton(): Locator {
|
||||||
|
return this.container.getByTestId('close-minmap-button')
|
||||||
|
}
|
||||||
|
|
||||||
|
async clickCanvas(options?: Parameters<Locator['click']>[0]): Promise<void> {
|
||||||
|
await this.canvas.click(options)
|
||||||
|
}
|
||||||
|
|
||||||
|
async clickSettingsButton(): Promise<void> {
|
||||||
|
await this.settingsButton.click()
|
||||||
|
}
|
||||||
|
|
||||||
|
async close(): Promise<void> {
|
||||||
|
await this.closeButton.click()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,11 +2,11 @@ import { expect } from '@playwright/test'
|
|||||||
|
|
||||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||||
|
|
||||||
test.beforeEach(async ({ comfyPage }) => {
|
|
||||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
|
||||||
})
|
|
||||||
|
|
||||||
test.describe('Copy Paste', () => {
|
test.describe('Copy Paste', () => {
|
||||||
|
test.beforeEach(async ({ comfyPage }) => {
|
||||||
|
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||||
|
})
|
||||||
|
|
||||||
test('Can copy and paste node', async ({ comfyPage }) => {
|
test('Can copy and paste node', async ({ comfyPage }) => {
|
||||||
await comfyPage.clickEmptyLatentNode()
|
await comfyPage.clickEmptyLatentNode()
|
||||||
await comfyPage.page.mouse.move(10, 10)
|
await comfyPage.page.mouse.move(10, 10)
|
||||||
@@ -15,6 +15,29 @@ test.describe('Copy Paste', () => {
|
|||||||
await expect(comfyPage.canvas).toHaveScreenshot('copied-node.png')
|
await expect(comfyPage.canvas).toHaveScreenshot('copied-node.png')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('Can copy and paste node after clicking minimap', async ({
|
||||||
|
comfyPage
|
||||||
|
}) => {
|
||||||
|
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
|
||||||
|
await comfyPage.setSetting('Comfy.Minimap.Visible', true)
|
||||||
|
|
||||||
|
const latentNodeTitle = 'Empty Latent Image'
|
||||||
|
const initialLatentNodeCt =
|
||||||
|
await comfyPage.getNodeRefsByTitle(latentNodeTitle)
|
||||||
|
|
||||||
|
await comfyPage.clickEmptyLatentNode()
|
||||||
|
await comfyPage.ctrlC()
|
||||||
|
|
||||||
|
// Click minimap to lose focus.
|
||||||
|
await comfyPage.menu.minimap.clickCanvas({ force: true })
|
||||||
|
|
||||||
|
// Paste node.
|
||||||
|
await comfyPage.ctrlV()
|
||||||
|
const expectedNodeCt = initialLatentNodeCt.length + 1
|
||||||
|
const latentNodeCt = await comfyPage.getNodeRefsByTitle(latentNodeTitle)
|
||||||
|
expect(latentNodeCt.length).toBe(expectedNodeCt)
|
||||||
|
})
|
||||||
|
|
||||||
test('Can copy and paste node with link', async ({ comfyPage }) => {
|
test('Can copy and paste node with link', async ({ comfyPage }) => {
|
||||||
await comfyPage.clickTextEncodeNode1()
|
await comfyPage.clickTextEncodeNode1()
|
||||||
await comfyPage.page.mouse.move(10, 10)
|
await comfyPage.page.mouse.move(10, 10)
|
||||||
|
|||||||
@@ -14,65 +14,70 @@ test.describe('Minimap', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('Validate minimap is visible by default', async ({ comfyPage }) => {
|
test('Validate minimap is visible by default', async ({ comfyPage }) => {
|
||||||
const minimapContainer = comfyPage.page.locator('.litegraph-minimap')
|
const minimap = comfyPage.menu.minimap
|
||||||
|
|
||||||
await expect(minimapContainer).toBeVisible()
|
await expect(minimap.container).toBeVisible()
|
||||||
|
await expect(minimap.canvas).toBeVisible()
|
||||||
const minimapCanvas = minimapContainer.locator('.minimap-canvas')
|
await expect(minimap.viewport).toBeVisible()
|
||||||
await expect(minimapCanvas).toBeVisible()
|
await expect(minimap.container).toHaveCSS('position', 'relative')
|
||||||
|
|
||||||
const minimapViewport = minimapContainer.locator('.minimap-viewport')
|
|
||||||
await expect(minimapViewport).toBeVisible()
|
|
||||||
|
|
||||||
await expect(minimapContainer).toHaveCSS('position', 'relative')
|
|
||||||
|
|
||||||
// position and z-index validation moved to the parent container of the minimap
|
// position and z-index validation moved to the parent container of the minimap
|
||||||
const minimapMainContainer = comfyPage.page.locator(
|
await expect(minimap.mainContainer).toHaveCSS('position', 'absolute')
|
||||||
'.minimap-main-container'
|
await expect(minimap.mainContainer).toHaveCSS('z-index', '1000')
|
||||||
)
|
|
||||||
await expect(minimapMainContainer).toHaveCSS('position', 'absolute')
|
|
||||||
await expect(minimapMainContainer).toHaveCSS('z-index', '1000')
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test('Validate minimap toggle button state', async ({ comfyPage }) => {
|
test('Validate minimap toggle button state', async ({ comfyPage }) => {
|
||||||
|
const minimap = comfyPage.menu.minimap
|
||||||
|
|
||||||
const toggleButton = comfyPage.page.getByTestId('toggle-minimap-button')
|
const toggleButton = comfyPage.page.getByTestId('toggle-minimap-button')
|
||||||
|
|
||||||
await expect(toggleButton).toBeVisible()
|
await expect(toggleButton).toBeVisible()
|
||||||
|
await expect(minimap.container).toBeVisible()
|
||||||
const minimapContainer = comfyPage.page.locator('.litegraph-minimap')
|
|
||||||
await expect(minimapContainer).toBeVisible()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test('Validate minimap can be toggled off and on', async ({ comfyPage }) => {
|
test('Validate minimap can be toggled off and on', async ({ comfyPage }) => {
|
||||||
const minimapContainer = comfyPage.page.locator('.litegraph-minimap')
|
const minimap = comfyPage.menu.minimap
|
||||||
|
|
||||||
|
// Open zoom controls dropdown first
|
||||||
|
const zoomControlsButton = comfyPage.page.getByTestId(
|
||||||
|
'zoom-controls-button'
|
||||||
|
)
|
||||||
|
await zoomControlsButton.click()
|
||||||
|
|
||||||
const toggleButton = comfyPage.page.getByTestId('toggle-minimap-button')
|
const toggleButton = comfyPage.page.getByTestId('toggle-minimap-button')
|
||||||
|
|
||||||
await expect(minimapContainer).toBeVisible()
|
await expect(minimap.container).toBeVisible()
|
||||||
|
|
||||||
await toggleButton.click()
|
await toggleButton.click()
|
||||||
await comfyPage.nextFrame()
|
await comfyPage.nextFrame()
|
||||||
|
|
||||||
await expect(minimapContainer).not.toBeVisible()
|
await expect(minimap.container).not.toBeVisible()
|
||||||
|
|
||||||
await toggleButton.click()
|
await toggleButton.click()
|
||||||
await comfyPage.nextFrame()
|
await comfyPage.nextFrame()
|
||||||
|
|
||||||
await expect(minimapContainer).toBeVisible()
|
await expect(minimap.container).toBeVisible()
|
||||||
|
|
||||||
|
// Open zoom controls dropdown again to verify button text
|
||||||
|
await zoomControlsButton.click()
|
||||||
|
await comfyPage.nextFrame()
|
||||||
|
|
||||||
|
await expect(toggleButton).toContainText('Hide Minimap')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('Validate minimap keyboard shortcut Alt+M', async ({ comfyPage }) => {
|
test('Validate minimap keyboard shortcut Alt+M', async ({ comfyPage }) => {
|
||||||
const minimapContainer = comfyPage.page.locator('.litegraph-minimap')
|
const minimap = comfyPage.menu.minimap
|
||||||
|
|
||||||
await expect(minimapContainer).toBeVisible()
|
await expect(minimap.container).toBeVisible()
|
||||||
|
|
||||||
await comfyPage.page.keyboard.press('Alt+KeyM')
|
await comfyPage.page.keyboard.press('Alt+KeyM')
|
||||||
await comfyPage.nextFrame()
|
await comfyPage.nextFrame()
|
||||||
|
|
||||||
await expect(minimapContainer).not.toBeVisible()
|
await expect(minimap.container).not.toBeVisible()
|
||||||
|
|
||||||
await comfyPage.page.keyboard.press('Alt+KeyM')
|
await comfyPage.page.keyboard.press('Alt+KeyM')
|
||||||
await comfyPage.nextFrame()
|
await comfyPage.nextFrame()
|
||||||
|
|
||||||
await expect(minimapContainer).toBeVisible()
|
await expect(minimap.container).toBeVisible()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
></div>
|
></div>
|
||||||
|
|
||||||
<ButtonGroup
|
<ButtonGroup
|
||||||
|
id="graph-canvas-controls"
|
||||||
class="absolute right-0 bottom-0 z-[1200] flex-row gap-1 border-[1px] border-node-border bg-interface-panel-surface p-2"
|
class="absolute right-0 bottom-0 z-[1200] flex-row gap-1 border-[1px] border-node-border bg-interface-panel-surface p-2"
|
||||||
:style="stringifiedMinimapStyles.buttonGroupStyles"
|
:style="stringifiedMinimapStyles.buttonGroupStyles"
|
||||||
@wheel="canvasInteractions.handleWheel"
|
@wheel="canvasInteractions.handleWheel"
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useEventListener } from '@vueuse/core'
|
import { useEventListener } from '@vueuse/core'
|
||||||
|
|
||||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||||
|
import { isEventTargetInGraph } from '@/workbench/eventHelpers'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds a handler on copy that serializes selected nodes to JSON
|
* Adds a handler on copy that serializes selected nodes to JSON
|
||||||
@@ -20,14 +21,14 @@ export const useCopy = () => {
|
|||||||
// Default system copy
|
// Default system copy
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const isTargetInGraph =
|
// Check if target is graph canvas or within graph UI (minimap, controls, etc.)
|
||||||
e.target.classList.contains('litegraph') ||
|
if (!isEventTargetInGraph(e.target)) {
|
||||||
e.target.classList.contains('graph-canvas-container') ||
|
return
|
||||||
e.target.id === 'graph-canvas'
|
}
|
||||||
|
|
||||||
// copy nodes and clear clipboard
|
// copy nodes and clear clipboard
|
||||||
const canvas = canvasStore.canvas
|
const canvas = canvasStore.canvas
|
||||||
if (isTargetInGraph && canvas?.selectedItems) {
|
if (canvas?.selectedItems) {
|
||||||
canvas.copyToClipboard()
|
canvas.copyToClipboard()
|
||||||
// clearData doesn't remove images from clipboard
|
// clearData doesn't remove images from clipboard
|
||||||
e.clipboardData?.setData('text', ' ')
|
e.clipboardData?.setData('text', ' ')
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
|||||||
import { app } from '@/scripts/app'
|
import { app } from '@/scripts/app'
|
||||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||||
import { isAudioNode, isImageNode, isVideoNode } from '@/utils/litegraphUtil'
|
import { isAudioNode, isImageNode, isVideoNode } from '@/utils/litegraphUtil'
|
||||||
|
import { isEventTargetInGraph } from '@/workbench/eventHelpers'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds a handler on paste that extracts and loads images or workflows from pasted JSON data
|
* Adds a handler on paste that extracts and loads images or workflows from pasted JSON data
|
||||||
@@ -38,14 +39,10 @@ export const usePaste = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
useEventListener(document, 'paste', async (e) => {
|
useEventListener(document, 'paste', async (e) => {
|
||||||
const isTargetInGraph =
|
// Check if target is graph canvas or within graph UI (minimap, controls, etc.)
|
||||||
e.target instanceof Element &&
|
if (!isEventTargetInGraph(e.target)) {
|
||||||
(e.target.classList.contains('litegraph') ||
|
return
|
||||||
e.target.classList.contains('graph-canvas-container') ||
|
}
|
||||||
e.target.id === 'graph-canvas')
|
|
||||||
|
|
||||||
// If the target is not in the graph, we don't want to handle the paste event
|
|
||||||
if (!isTargetInGraph) return
|
|
||||||
|
|
||||||
// ctrl+shift+v is used to paste nodes with connections
|
// ctrl+shift+v is used to paste nodes with connections
|
||||||
// this is handled by litegraph
|
// this is handled by litegraph
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
v-if="visible && initialized"
|
v-if="visible && initialized"
|
||||||
|
id="comfy-minimap"
|
||||||
ref="minimapRef"
|
ref="minimapRef"
|
||||||
class="minimap-main-container absolute right-0 bottom-[58px] z-1000 flex"
|
class="minimap-main-container absolute right-0 bottom-[58px] z-1000 flex"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ export const useKeybindingService = () => {
|
|||||||
const settingStore = useSettingStore()
|
const settingStore = useSettingStore()
|
||||||
const dialogStore = useDialogStore()
|
const dialogStore = useDialogStore()
|
||||||
|
|
||||||
|
// Keys that LiteGraph handles but aren't in core keybindings
|
||||||
|
const canvasBindedKeys = ['Delete', 'Backspace']
|
||||||
|
|
||||||
// Helper function to determine if an event should be forwarded to canvas
|
// Helper function to determine if an event should be forwarded to canvas
|
||||||
const shouldForwardToCanvas = (event: KeyboardEvent): boolean => {
|
const shouldForwardToCanvas = (event: KeyboardEvent): boolean => {
|
||||||
// Don't forward if modifier keys are pressed (except shift)
|
// Don't forward if modifier keys are pressed (except shift)
|
||||||
@@ -22,10 +25,7 @@ export const useKeybindingService = () => {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keys that LiteGraph handles but aren't in core keybindings
|
return canvasBindedKeys.includes(event.key)
|
||||||
const canvasKeys = ['Delete', 'Backspace']
|
|
||||||
|
|
||||||
return canvasKeys.includes(event.key)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const keybindHandler = async function (event: KeyboardEvent) {
|
const keybindHandler = async function (event: KeyboardEvent) {
|
||||||
|
|||||||
31
src/workbench/eventHelpers.ts
Normal file
31
src/workbench/eventHelpers.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
/**
|
||||||
|
* Utility functions for handling workbench events
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if an event target is within the graph canvas or related UI elements
|
||||||
|
* (minimap, canvas controls, etc.)
|
||||||
|
*
|
||||||
|
* Used by clipboard handlers to determine if copy/paste events should be
|
||||||
|
* intercepted for graph operations vs. allowing default browser behavior
|
||||||
|
* for text inputs and other UI elements.
|
||||||
|
*
|
||||||
|
* @param target - The event target to check
|
||||||
|
* @returns true if the target is within graph-related UI elements
|
||||||
|
*/
|
||||||
|
export function isEventTargetInGraph(target: EventTarget | null): boolean {
|
||||||
|
if (!(target instanceof Element)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
target.id === 'graph-canvas' ||
|
||||||
|
target.id === 'comfy-minimap' ||
|
||||||
|
target.id === 'graph-canvas-controls' ||
|
||||||
|
target.classList.contains('graph-canvas-container') ||
|
||||||
|
target.classList.contains('litegraph') ||
|
||||||
|
target.closest('#comfy-minimap') !== null ||
|
||||||
|
target.closest('#graph-canvas-controls') !== null ||
|
||||||
|
target.closest('#graph-canvas-container') !== null
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user