mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-12 16:40:05 +00:00
fix: skip node metadata paste when media node is selected (#9773)
## Summary - When a media node (LoadImage/LoadAudio/LoadVideo) is selected and the clipboard contains stale node metadata from a prior Ctrl+C, pasting skips the node-metadata deserialization so that the paste falls through to litegraph's default handler instead of incorrectly pasting the old copied node. - Fixes Comfy-Org/ComfyUI#12896 ## Root Cause The paste handler in `usePaste.ts` checks clipboard `text/html` for `data-metadata` (serialized node data) **before** falling through to litegraph's default paste. When a user copies a node, then copies a web image, the browser clipboard may retain the old `data-metadata` in `text/html` while the image data is not available as a `DataTransferItem` file. This causes the stale node to be pasted instead of the image. ## Fix Skip `pasteClipboardItems()` when a media node is selected, allowing the paste to fall through to litegraph's default handler which can handle the clipboard content appropriately. ## Test plan - [x] Added unit test verifying node metadata paste is skipped when media node is selected - [x] Manual: Copy a node → copy a web image → select LoadImage node → Ctrl+V → verify image is pasted, not the node ## AS IS https://github.com/user-attachments/assets/210d77d3-5c49-4e38-91b7-b9d9ea0e7ca0 ## TO BE https://github.com/user-attachments/assets/b68e4582-0c57-48b8-9ed9-0b3243bb1554 🤖 Generated with [Claude Code](https://claude.com/claude-code) ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-9773-fix-skip-node-metadata-paste-when-media-node-is-selected-3216d73d3650814d92dadcd0c0ec79c7) by [Unito](https://www.unito.io) --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: GitHub Action <action@github.com>
This commit is contained in:
47
browser_tests/assets/nodes/load_image_with_ksampler.json
Normal file
47
browser_tests/assets/nodes/load_image_with_ksampler.json
Normal file
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"last_node_id": 2,
|
||||
"last_link_id": 0,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 1,
|
||||
"type": "LoadImage",
|
||||
"pos": [50, 50],
|
||||
"size": [315, 314],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{ "name": "IMAGE", "type": "IMAGE", "links": null },
|
||||
{ "name": "MASK", "type": "MASK", "links": null }
|
||||
],
|
||||
"properties": { "Node name for S&R": "LoadImage" },
|
||||
"widgets_values": ["example.png", "image"]
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"type": "KSampler",
|
||||
"pos": [500, 50],
|
||||
"size": [315, 262],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{ "name": "model", "type": "MODEL", "link": null },
|
||||
{ "name": "positive", "type": "CONDITIONING", "link": null },
|
||||
{ "name": "negative", "type": "CONDITIONING", "link": null },
|
||||
{ "name": "latent_image", "type": "LATENT", "link": null }
|
||||
],
|
||||
"outputs": [{ "name": "LATENT", "type": "LATENT", "links": null }],
|
||||
"properties": { "Node name for S&R": "KSampler" },
|
||||
"widgets_values": [0, "randomize", 20, 8, "euler", "normal", 1]
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": { "offset": [0, 0], "scale": 1 }
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
58
browser_tests/tests/imagePastePriority.spec.ts
Normal file
58
browser_tests/tests/imagePastePriority.spec.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
test.describe(
|
||||
'Image paste priority over stale node metadata',
|
||||
{ tag: ['@node'] },
|
||||
() => {
|
||||
test('Should not paste copied node when a LoadImage node is selected and clipboard has stale node metadata', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.workflow.loadWorkflow('nodes/load_image_with_ksampler')
|
||||
|
||||
const initialCount = await comfyPage.nodeOps.getGraphNodesCount()
|
||||
expect(initialCount).toBe(2)
|
||||
|
||||
// Copy the KSampler node (puts data-metadata in clipboard)
|
||||
const ksamplerNodes =
|
||||
await comfyPage.nodeOps.getNodeRefsByType('KSampler')
|
||||
await ksamplerNodes[0].copy()
|
||||
|
||||
// Select the LoadImage node
|
||||
const loadImageNodes =
|
||||
await comfyPage.nodeOps.getNodeRefsByType('LoadImage')
|
||||
await loadImageNodes[0].click('title')
|
||||
|
||||
// Simulate pasting when clipboard has stale node metadata (text/html
|
||||
// with data-metadata) but no image file items. This replicates the bug
|
||||
// scenario: user copied a node, then copied a web image (which replaces
|
||||
// clipboard files but may leave stale text/html with node metadata).
|
||||
await comfyPage.page.evaluate(() => {
|
||||
const nodeData = { nodes: [{ type: 'KSampler', id: 99 }] }
|
||||
const base64 = btoa(JSON.stringify(nodeData))
|
||||
const html =
|
||||
'<meta charset="utf-8"><div><span data-metadata="' +
|
||||
base64 +
|
||||
'"></span></div><span style="white-space:pre-wrap;">Text</span>'
|
||||
|
||||
const dataTransfer = new DataTransfer()
|
||||
dataTransfer.setData('text/html', html)
|
||||
|
||||
const event = new ClipboardEvent('paste', {
|
||||
clipboardData: dataTransfer,
|
||||
bubbles: true,
|
||||
cancelable: true
|
||||
})
|
||||
document.dispatchEvent(event)
|
||||
})
|
||||
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Node count should remain the same — stale node metadata should NOT
|
||||
// be deserialized when a media node is selected.
|
||||
const finalCount = await comfyPage.nodeOps.getGraphNodesCount()
|
||||
expect(finalCount).toBe(initialCount)
|
||||
})
|
||||
}
|
||||
)
|
||||
@@ -595,6 +595,34 @@ describe('usePaste', () => {
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('should skip node metadata paste when a media node is selected', async () => {
|
||||
const mockNode = createMockLGraphNode({
|
||||
is_selected: true,
|
||||
pasteFile: vi.fn(),
|
||||
pasteFiles: vi.fn()
|
||||
})
|
||||
mockCanvas.current_node = mockNode
|
||||
vi.mocked(isImageNode).mockReturnValue(true)
|
||||
|
||||
usePaste()
|
||||
|
||||
const nodeData = { nodes: [{ type: 'KSampler' }] }
|
||||
const encoded = btoa(JSON.stringify(nodeData))
|
||||
const html = `<div data-metadata="${encoded}"></div>`
|
||||
|
||||
const dataTransfer = new DataTransfer()
|
||||
dataTransfer.setData('text/html', html)
|
||||
dataTransfer.setData('text/plain', 'some text')
|
||||
|
||||
const event = new ClipboardEvent('paste', { clipboardData: dataTransfer })
|
||||
document.dispatchEvent(event)
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(mockCanvas._deserializeItems).not.toHaveBeenCalled()
|
||||
expect(mockCanvas.pasteFromClipboard).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('cloneDataTransfer', () => {
|
||||
|
||||
@@ -229,7 +229,10 @@ export const usePaste = () => {
|
||||
return
|
||||
}
|
||||
}
|
||||
if (pasteClipboardItems(data)) return
|
||||
|
||||
const isMediaNodeSelected =
|
||||
isImageNodeSelected || isVideoNodeSelected || isAudioNodeSelected
|
||||
if (!isMediaNodeSelected && pasteClipboardItems(data)) return
|
||||
|
||||
// No image found. Look for node data
|
||||
data = data.getData('text/plain')
|
||||
|
||||
Reference in New Issue
Block a user