From a737be7e167b62a3ea73f02e35497cec224a0fba Mon Sep 17 00:00:00 2001 From: bymyself Date: Wed, 2 Oct 2024 12:51:33 -0700 Subject: [PATCH] Fix group node copy paste (#1069) * Fix group node copy paste * nit --------- Co-authored-by: huchenlei --- browser_tests/ComfyPage.ts | 7 +- browser_tests/assets/group_node_v1.3.3.json | 404 ++++++++++++++++++++ browser_tests/groupNode.spec.ts | 106 ++++- src/extensions/core/groupNode.ts | 25 +- 4 files changed, 528 insertions(+), 14 deletions(-) create mode 100644 browser_tests/assets/group_node_v1.3.3.json diff --git a/browser_tests/ComfyPage.ts b/browser_tests/ComfyPage.ts index f687d9786..a35e76470 100644 --- a/browser_tests/ComfyPage.ts +++ b/browser_tests/ComfyPage.ts @@ -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, diff --git a/browser_tests/assets/group_node_v1.3.3.json b/browser_tests/assets/group_node_v1.3.3.json new file mode 100644 index 000000000..7afd9d7c8 --- /dev/null +++ b/browser_tests/assets/group_node_v1.3.3.json @@ -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 +} \ No newline at end of file diff --git a/browser_tests/groupNode.spec.ts b/browser_tests/groupNode.spec.ts index 56f76f1aa..0c86272eb 100644 --- a/browser_tests/groupNode.spec.ts +++ b/browser_tests/groupNode.spec.ts @@ -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) + }) + }) + }) }) diff --git a/src/extensions/core/groupNode.ts b/src/extensions/core/groupNode.ts index 694081e98..f1100b5aa 100644 --- a/src/extensions/core/groupNode.ts +++ b/src/extensions/core/groupNode.ts @@ -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) {