mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-01-26 19:09:52 +00:00
Fix group node copy paste (#1069)
* Fix group node copy paste * nit --------- Co-authored-by: huchenlei <huchenlei@proton.me>
This commit is contained in:
@@ -269,7 +269,7 @@ class Topbar {
|
||||
|
||||
const tabName = path[0]
|
||||
const topLevelMenu = this.page.locator(
|
||||
`.top-menubar .p-menubar-item:has-text("${tabName}")`
|
||||
`.top-menubar .p-menubar-item-label:text-is("${tabName}")`
|
||||
)
|
||||
await topLevelMenu.waitFor({ state: 'visible' })
|
||||
await topLevelMenu.click()
|
||||
@@ -1130,6 +1130,11 @@ export class NodeReference {
|
||||
await this.comfyPage.moveMouseToEmptyArea()
|
||||
}
|
||||
}
|
||||
async copy() {
|
||||
await this.click('title')
|
||||
await this.comfyPage.ctrlC()
|
||||
await this.comfyPage.nextFrame()
|
||||
}
|
||||
async connectWidget(
|
||||
originSlotIndex: number,
|
||||
targetNode: NodeReference,
|
||||
|
||||
404
browser_tests/assets/group_node_v1.3.3.json
Normal file
404
browser_tests/assets/group_node_v1.3.3.json
Normal file
@@ -0,0 +1,404 @@
|
||||
{
|
||||
"last_node_id": 10,
|
||||
"last_link_id": 9,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 10,
|
||||
"type": "workflow>group_node",
|
||||
"pos": {
|
||||
"0": 26,
|
||||
"1": 186
|
||||
},
|
||||
"size": {
|
||||
"0": 400,
|
||||
"1": 390
|
||||
},
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [],
|
||||
"properties": {
|
||||
"Node name for S&R": "workflow>group_node"
|
||||
},
|
||||
"widgets_values": [
|
||||
512,
|
||||
512,
|
||||
1,
|
||||
"v1-5-pruned-emaonly.ckpt",
|
||||
"beautiful scenery nature glass bottle landscape, , purple galaxy bottle,",
|
||||
"text, watermark",
|
||||
156680208700286,
|
||||
"randomize",
|
||||
20,
|
||||
8,
|
||||
"euler",
|
||||
"normal",
|
||||
1,
|
||||
"ComfyUI"
|
||||
]
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"groupNodes": {
|
||||
"group_node": {
|
||||
"nodes": [
|
||||
{
|
||||
"id": -1,
|
||||
"type": "EmptyLatentImage",
|
||||
"pos": {
|
||||
"0": 473,
|
||||
"1": 609
|
||||
},
|
||||
"size": {
|
||||
"0": 315,
|
||||
"1": 106
|
||||
},
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "LATENT",
|
||||
"type": "LATENT",
|
||||
"links": [],
|
||||
"slot_index": 0
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "EmptyLatentImage"
|
||||
},
|
||||
"widgets_values": [
|
||||
512,
|
||||
512,
|
||||
1
|
||||
],
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"id": -1,
|
||||
"type": "CheckpointLoaderSimple",
|
||||
"pos": {
|
||||
"0": 26,
|
||||
"1": 474
|
||||
},
|
||||
"size": {
|
||||
"0": 315,
|
||||
"1": 98
|
||||
},
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "MODEL",
|
||||
"type": "MODEL",
|
||||
"links": [],
|
||||
"slot_index": 0
|
||||
},
|
||||
{
|
||||
"name": "CLIP",
|
||||
"type": "CLIP",
|
||||
"links": [],
|
||||
"slot_index": 1
|
||||
},
|
||||
{
|
||||
"name": "VAE",
|
||||
"type": "VAE",
|
||||
"links": [],
|
||||
"slot_index": 2
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CheckpointLoaderSimple"
|
||||
},
|
||||
"widgets_values": [
|
||||
"v1-5-pruned-emaonly.ckpt"
|
||||
],
|
||||
"index": 1
|
||||
},
|
||||
{
|
||||
"id": -1,
|
||||
"type": "CLIPTextEncode",
|
||||
"pos": {
|
||||
"0": 415,
|
||||
"1": 186
|
||||
},
|
||||
"size": {
|
||||
"0": 422.84503173828125,
|
||||
"1": 164.31304931640625
|
||||
},
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "clip",
|
||||
"type": "CLIP",
|
||||
"link": null
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "CONDITIONING",
|
||||
"type": "CONDITIONING",
|
||||
"links": [],
|
||||
"slot_index": 0
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CLIPTextEncode"
|
||||
},
|
||||
"widgets_values": [
|
||||
"beautiful scenery nature glass bottle landscape, , purple galaxy bottle,"
|
||||
],
|
||||
"index": 2
|
||||
},
|
||||
{
|
||||
"id": -1,
|
||||
"type": "CLIPTextEncode",
|
||||
"pos": {
|
||||
"0": 413,
|
||||
"1": 389
|
||||
},
|
||||
"size": {
|
||||
"0": 425.27801513671875,
|
||||
"1": 180.6060791015625
|
||||
},
|
||||
"flags": {},
|
||||
"order": 3,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "clip",
|
||||
"type": "CLIP",
|
||||
"link": null
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "CONDITIONING",
|
||||
"type": "CONDITIONING",
|
||||
"links": [],
|
||||
"slot_index": 0
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "CLIPTextEncode"
|
||||
},
|
||||
"widgets_values": [
|
||||
"text, watermark"
|
||||
],
|
||||
"index": 3
|
||||
},
|
||||
{
|
||||
"id": -1,
|
||||
"type": "KSampler",
|
||||
"pos": {
|
||||
"0": 863,
|
||||
"1": 186
|
||||
},
|
||||
"size": {
|
||||
"0": 315,
|
||||
"1": 262
|
||||
},
|
||||
"flags": {},
|
||||
"order": 4,
|
||||
"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": [],
|
||||
"slot_index": 0
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "KSampler"
|
||||
},
|
||||
"widgets_values": [
|
||||
156680208700286,
|
||||
"randomize",
|
||||
20,
|
||||
8,
|
||||
"euler",
|
||||
"normal",
|
||||
1
|
||||
],
|
||||
"index": 4
|
||||
},
|
||||
{
|
||||
"id": -1,
|
||||
"type": "VAEDecode",
|
||||
"pos": {
|
||||
"0": 1209,
|
||||
"1": 188
|
||||
},
|
||||
"size": {
|
||||
"0": 210,
|
||||
"1": 46
|
||||
},
|
||||
"flags": {},
|
||||
"order": 5,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "samples",
|
||||
"type": "LATENT",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"name": "vae",
|
||||
"type": "VAE",
|
||||
"link": null
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [],
|
||||
"slot_index": 0
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "VAEDecode"
|
||||
},
|
||||
"index": 5
|
||||
},
|
||||
{
|
||||
"id": -1,
|
||||
"type": "SaveImage",
|
||||
"pos": {
|
||||
"0": 1451,
|
||||
"1": 189
|
||||
},
|
||||
"size": {
|
||||
"0": 210,
|
||||
"1": 58
|
||||
},
|
||||
"flags": {},
|
||||
"order": 6,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": null
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {},
|
||||
"widgets_values": [
|
||||
"ComfyUI"
|
||||
],
|
||||
"index": 6
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[
|
||||
1,
|
||||
1,
|
||||
2,
|
||||
0,
|
||||
4,
|
||||
"CLIP"
|
||||
],
|
||||
[
|
||||
1,
|
||||
1,
|
||||
3,
|
||||
0,
|
||||
4,
|
||||
"CLIP"
|
||||
],
|
||||
[
|
||||
1,
|
||||
0,
|
||||
4,
|
||||
0,
|
||||
4,
|
||||
"MODEL"
|
||||
],
|
||||
[
|
||||
2,
|
||||
0,
|
||||
4,
|
||||
1,
|
||||
6,
|
||||
"CONDITIONING"
|
||||
],
|
||||
[
|
||||
3,
|
||||
0,
|
||||
4,
|
||||
2,
|
||||
7,
|
||||
"CONDITIONING"
|
||||
],
|
||||
[
|
||||
0,
|
||||
0,
|
||||
4,
|
||||
3,
|
||||
5,
|
||||
"LATENT"
|
||||
],
|
||||
[
|
||||
4,
|
||||
0,
|
||||
5,
|
||||
0,
|
||||
3,
|
||||
"LATENT"
|
||||
],
|
||||
[
|
||||
1,
|
||||
2,
|
||||
5,
|
||||
1,
|
||||
4,
|
||||
"VAE"
|
||||
],
|
||||
[
|
||||
5,
|
||||
0,
|
||||
6,
|
||||
0,
|
||||
8,
|
||||
"IMAGE"
|
||||
]
|
||||
],
|
||||
"external": []
|
||||
}
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { expect } from '@playwright/test'
|
||||
import { comfyPageFixture as test } from './ComfyPage'
|
||||
import { ComfyPage, NodeReference, comfyPageFixture as test } from './ComfyPage'
|
||||
|
||||
test.describe('Group Node', () => {
|
||||
test.afterEach(async ({ comfyPage }) => {
|
||||
@@ -148,4 +148,108 @@ test.describe('Group Node', () => {
|
||||
expect(await comfyPage.getGraphNodesCount()).toBe(1)
|
||||
expect(comfyPage.page.locator('.comfy-missing-nodes')).not.toBeVisible()
|
||||
})
|
||||
|
||||
test.describe('Copy and paste', () => {
|
||||
let groupNode: NodeReference | null
|
||||
const WORKFLOW_NAME = 'group_node_v1.3.3'
|
||||
const GROUP_NODE_CATEGORY = 'group nodes>workflow'
|
||||
const GROUP_NODE_PREFIX = 'workflow>'
|
||||
const GROUP_NODE_NAME = 'group_node' // Node name in given workflow
|
||||
const GROUP_NODE_TYPE = `${GROUP_NODE_PREFIX}${GROUP_NODE_NAME}`
|
||||
|
||||
const isRegisteredLitegraph = async (comfyPage: ComfyPage) => {
|
||||
return await comfyPage.page.evaluate((nodeType: string) => {
|
||||
return !!window['LiteGraph'].registered_node_types[nodeType]
|
||||
}, GROUP_NODE_TYPE)
|
||||
}
|
||||
|
||||
const isRegisteredNodeDefStore = async (comfyPage: ComfyPage) => {
|
||||
const groupNodesFolderCt = await comfyPage.menu.nodeLibraryTab
|
||||
.getFolder(GROUP_NODE_CATEGORY)
|
||||
.count()
|
||||
return groupNodesFolderCt === 1
|
||||
}
|
||||
|
||||
const verifyNodeLoaded = async (
|
||||
comfyPage: ComfyPage,
|
||||
expectedCount: number
|
||||
) => {
|
||||
expect(await comfyPage.getNodeRefsByType(GROUP_NODE_TYPE)).toHaveLength(
|
||||
expectedCount
|
||||
)
|
||||
expect(await isRegisteredLitegraph(comfyPage)).toBe(true)
|
||||
expect(await isRegisteredNodeDefStore(comfyPage)).toBe(true)
|
||||
}
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Floating')
|
||||
await comfyPage.loadWorkflow(WORKFLOW_NAME)
|
||||
await comfyPage.menu.nodeLibraryTab.open()
|
||||
|
||||
groupNode = await comfyPage.getFirstNodeRef()
|
||||
if (!groupNode)
|
||||
throw new Error(`Group node not found in workflow ${WORKFLOW_NAME}`)
|
||||
await groupNode.copy()
|
||||
})
|
||||
|
||||
test.afterEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
await comfyPage.page.evaluate((groupNodeName) => {
|
||||
window['LiteGraph'].unregisterNodeType(groupNodeName)
|
||||
}, GROUP_NODE_TYPE)
|
||||
})
|
||||
|
||||
test('Copies and pastes group node within the same workflow', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.ctrlV()
|
||||
await verifyNodeLoaded(comfyPage, 2)
|
||||
})
|
||||
|
||||
test('Copies and pastes group node after clearing workflow', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.menu.topbar.triggerTopbarCommand([
|
||||
'Edit',
|
||||
'Clear Workflow'
|
||||
])
|
||||
await comfyPage.ctrlV()
|
||||
await verifyNodeLoaded(comfyPage, 1)
|
||||
})
|
||||
|
||||
test('Copies and pastes group node into a newly created blank workflow', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.menu.topbar.triggerTopbarCommand(['Workflow', 'New'])
|
||||
await comfyPage.ctrlV()
|
||||
await verifyNodeLoaded(comfyPage, 1)
|
||||
})
|
||||
|
||||
test('Copies and pastes group node across different workflows', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.loadWorkflow('default')
|
||||
await comfyPage.ctrlV()
|
||||
await verifyNodeLoaded(comfyPage, 1)
|
||||
})
|
||||
|
||||
test('Serializes group node after copy and paste across workflows', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.menu.topbar.triggerTopbarCommand(['Workflow', 'New'])
|
||||
await comfyPage.ctrlV()
|
||||
const currentGraphState = await comfyPage.page.evaluate(() =>
|
||||
window['app'].graph.serialize()
|
||||
)
|
||||
|
||||
await test.step('Load workflow containing a group node pasted from a different workflow', async () => {
|
||||
await comfyPage.page.evaluate(
|
||||
(workflow) => window['app'].loadGraphData(workflow),
|
||||
currentGraphState
|
||||
)
|
||||
await comfyPage.nextFrame()
|
||||
await verifyNodeLoaded(comfyPage, 1)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -5,7 +5,13 @@ import { ManageGroupDialog } from './groupNodeManage'
|
||||
import type { LGraphNode } from '@comfyorg/litegraph'
|
||||
import { LGraphCanvas, LiteGraph } from '@comfyorg/litegraph'
|
||||
import { useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
import { ComfyNode, ComfyWorkflowJSON } from '@/types/comfyWorkflow'
|
||||
import { ComfyLink, ComfyNode, ComfyWorkflowJSON } from '@/types/comfyWorkflow'
|
||||
|
||||
type GroupNodeWorkflowData = {
|
||||
external: ComfyLink[]
|
||||
links: ComfyLink[]
|
||||
nodes: ComfyNode[]
|
||||
}
|
||||
|
||||
const GROUP = Symbol()
|
||||
|
||||
@@ -32,7 +38,7 @@ const Workflow = {
|
||||
}
|
||||
return Workflow.InUse.Free
|
||||
},
|
||||
storeGroupNode(name, data) {
|
||||
storeGroupNode(name: string, data: GroupNodeWorkflowData) {
|
||||
let extra = app.graph.extra
|
||||
if (!extra) app.graph.extra = extra = {}
|
||||
let groupNodes = extra.groupNodes
|
||||
@@ -649,16 +655,6 @@ export class GroupNodeConfig {
|
||||
}
|
||||
|
||||
static async registerFromWorkflow(groupNodes, missingNodeTypes) {
|
||||
const clean = app.clean
|
||||
app.clean = function () {
|
||||
for (const g in groupNodes) {
|
||||
try {
|
||||
LiteGraph.unregisterNodeType(`${PREFIX}${SEPARATOR}` + g)
|
||||
} catch (error) {}
|
||||
}
|
||||
app.clean = clean
|
||||
}
|
||||
|
||||
for (const g in groupNodes) {
|
||||
const groupData = groupNodes[g]
|
||||
|
||||
@@ -1482,6 +1478,11 @@ const ext = {
|
||||
nodeCreated(node) {
|
||||
if (GroupNodeHandler.isGroupNode(node)) {
|
||||
node[GROUP] = new GroupNodeHandler(node)
|
||||
|
||||
// Ensure group nodes pasted from other workflows are stored
|
||||
if (node.title && node[GROUP]?.groupData?.nodeData) {
|
||||
Workflow.storeGroupNode(node.title, node[GROUP].groupData.nodeData)
|
||||
}
|
||||
}
|
||||
},
|
||||
async refreshComboInNodes(defs) {
|
||||
|
||||
Reference in New Issue
Block a user