From f460e105e9510c2690cd9def04e2c90157c98cd5 Mon Sep 17 00:00:00 2001 From: Glary-Bot Date: Tue, 19 May 2026 23:26:30 +0000 Subject: [PATCH] feat: remove ability to create Group Nodes Group Nodes are a legacy feature superseded by Subgraphs. This removes all UI entry points for creating new Group Nodes, while keeping the loading, ungrouping, and management code intact so existing workflows that contain Group Nodes continue to load and can still be unpacked or managed. Removed entry points: - 'Convert selected nodes to group node' command - Alt+G keybinding - 'Convert to Group Node (Deprecated)' canvas and node context menu items - 'Convert to Group Node' option in the Vue selection menu - Associated en locale strings - Browser tests that exercised the creation flow --- .../fixtures/helpers/NodeOperationsHelper.ts | 10 - .../fixtures/utils/litegraphUtils.ts | 11 - browser_tests/tests/groupNode.spec.ts | 230 ++---------------- browser_tests/tests/rightClickMenu.spec.ts | 17 -- .../interactions/node/contextMenu.spec.ts | 19 -- src/composables/graph/contextMenuConverter.ts | 4 +- src/composables/graph/useMoreOptionsMenu.ts | 3 +- .../graph/useSelectionMenuOptions.test.ts | 4 +- .../graph/useSelectionMenuOptions.ts | 29 +-- src/extensions/core/groupNode.ts | 81 +----- src/locales/en/commands.json | 3 - src/locales/en/main.json | 2 - 12 files changed, 35 insertions(+), 378 deletions(-) diff --git a/browser_tests/fixtures/helpers/NodeOperationsHelper.ts b/browser_tests/fixtures/helpers/NodeOperationsHelper.ts index a0fadd2f74..5f21069516 100644 --- a/browser_tests/fixtures/helpers/NodeOperationsHelper.ts +++ b/browser_tests/fixtures/helpers/NodeOperationsHelper.ts @@ -216,16 +216,6 @@ export class NodeOperationsHelper { } } - async convertAllNodesToGroupNode(groupNodeName: string): Promise { - await this.comfyPage.canvas.press('Control+a') - const node = await this.getFirstNodeRef() - if (!node) { - throw new Error('No nodes found to convert') - } - await node.clickContextMenuOption('Convert to Group Node') - await this.fillPromptDialog(groupNodeName) - } - async fillPromptDialog(value: string): Promise { await this.promptDialogInput.fill(value) await this.page.keyboard.press('Enter') diff --git a/browser_tests/fixtures/utils/litegraphUtils.ts b/browser_tests/fixtures/utils/litegraphUtils.ts index 0036526a65..1a5d02de9d 100644 --- a/browser_tests/fixtures/utils/litegraphUtils.ts +++ b/browser_tests/fixtures/utils/litegraphUtils.ts @@ -514,17 +514,6 @@ export class NodeReference { const ctx = this.comfyPage.page.locator('.litecontextmenu') await ctx.getByText(optionText).click() } - async convertToGroupNode(groupNodeName: string = 'GroupNode') { - await this.clickContextMenuOption('Convert to Group Node') - await this.comfyPage.nodeOps.fillPromptDialog(groupNodeName) - const nodes = await this.comfyPage.nodeOps.getNodeRefsByType( - `workflow>${groupNodeName}` - ) - if (nodes.length !== 1) { - throw new Error(`Did not find single group node (found=${nodes.length})`) - } - return nodes[0] - } async convertToSubgraph() { await this.clickContextMenuOption('Convert to Subgraph') await this.comfyPage.nextFrame() diff --git a/browser_tests/tests/groupNode.spec.ts b/browser_tests/tests/groupNode.spec.ts index d002d83681..0a5c3edcb3 100644 --- a/browser_tests/tests/groupNode.spec.ts +++ b/browser_tests/tests/groupNode.spec.ts @@ -5,11 +5,14 @@ import { comfyExpect as expect, comfyPageFixture as test } from '@e2e/fixtures/ComfyPage' -import type { NodeLibrarySidebarTab } from '@e2e/fixtures/components/SidebarTab' import { TestIds } from '@e2e/fixtures/selectors' -import { DefaultGraphPositions } from '@e2e/fixtures/constants/defaultGraphPositions' import type { NodeReference } from '@e2e/fixtures/utils/litegraphUtils' +// Group Node creation has been removed from the UI. These tests cover the +// surviving behaviors: loading legacy workflows that contain group nodes, +// copy/paste of already-loaded group nodes, and opening the Manage Group +// Node dialog from an existing group node. + test.beforeEach(async ({ comfyPage }) => { await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled') await comfyPage.settings.setSetting('Comfy.NodeLibrary.NewDesign', false) @@ -17,151 +20,14 @@ test.beforeEach(async ({ comfyPage }) => { }) test.describe('Group Node', { tag: '@node' }, () => { - test.describe('Node library sidebar', () => { - const groupNodeName = 'DefautWorkflowGroupNode' - const groupNodeCategory = 'group nodes>workflow' - const groupNodeBookmarkName = `workflow>${groupNodeName}` - let libraryTab: NodeLibrarySidebarTab - - test.beforeEach(async ({ comfyPage }) => { - await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top') - libraryTab = comfyPage.menu.nodeLibraryTab - await comfyPage.nodeOps.convertAllNodesToGroupNode(groupNodeName) - await libraryTab.open() - }) - - test('Is added to node library sidebar', async ({ - comfyPage: _comfyPage - }) => { - await expect(libraryTab.getFolder(groupNodeCategory)).toHaveCount(1) - }) - - test('Can be added to canvas using node library sidebar', async ({ - comfyPage - }) => { - const initialNodeCount = await comfyPage.nodeOps.getGraphNodesCount() - - // Add group node from node library sidebar - await libraryTab.getFolder(groupNodeCategory).click() - await libraryTab.getNode(groupNodeName).click() - - // Verify the node is added to the canvas - await expect - .poll(() => comfyPage.nodeOps.getGraphNodesCount()) - .toBe(initialNodeCount + 1) - }) - - test('Can be bookmarked and unbookmarked', async ({ comfyPage }) => { - await libraryTab.getFolder(groupNodeCategory).click() - await libraryTab - .getNode(groupNodeName) - .locator('.bookmark-button') - .click() - - // Verify the node is added to the bookmarks tab - await expect - .poll(() => - comfyPage.settings.getSetting('Comfy.NodeLibrary.Bookmarks.V2') - ) - .toEqual([groupNodeBookmarkName]) - // Verify the bookmark node with the same name is added to the tree - await expect(libraryTab.getNode(groupNodeName)).not.toHaveCount(0) - - // Unbookmark the node - await libraryTab - .getNode(groupNodeName) - .locator('.bookmark-button') - .first() - .click() - - // Verify the node is removed from the bookmarks tab - await expect - .poll(() => - comfyPage.settings.getSetting('Comfy.NodeLibrary.Bookmarks.V2') - ) - .toHaveLength(0) - }) - - test('Displays preview on bookmark hover', async ({ comfyPage }) => { - await libraryTab.getFolder(groupNodeCategory).click() - await libraryTab - .getNode(groupNodeName) - .locator('.bookmark-button') - .click() - await comfyPage.page - .locator('.p-tree-node-label.tree-explorer-node-label') - .first() - .hover() - await expect( - comfyPage.page.locator('.node-lib-node-preview') - ).toBeVisible() - await libraryTab - .getNode(groupNodeName) - .locator('.bookmark-button') - .first() - .click() - }) - }) - - test( - 'Can be added to canvas using search', - { tag: '@screenshot' }, - async ({ comfyPage }) => { - const groupNodeName = 'DefautWorkflowGroupNode' - await comfyPage.nodeOps.convertAllNodesToGroupNode(groupNodeName) - await comfyPage.canvasOps.doubleClick() - await comfyPage.nextFrame() - await comfyPage.searchBox.input.waitFor({ state: 'visible' }) - await comfyPage.searchBox.input.fill(groupNodeName) - await comfyPage.searchBox.dropdown.waitFor({ state: 'visible' }) - - const exactGroupNodeResult = comfyPage.searchBox.dropdown - .locator(`li[aria-label="${groupNodeName}"]`) - .first() - await expect(exactGroupNodeResult).toBeVisible() - await exactGroupNodeResult.click() - - await expect(comfyPage.canvas).toHaveScreenshot( - 'group-node-copy-added-from-search.png' - ) - } - ) - - test('Displays tooltip on title hover', async ({ comfyPage }) => { - await comfyPage.settings.setSetting('Comfy.EnableTooltips', true) - await comfyPage.nodeOps.convertAllNodesToGroupNode('Group Node') - await comfyPage.page.mouse.move(47, 173) - await expect(comfyPage.page.locator('.node-tooltip')).toBeVisible() - }) - - test('Manage group opens with the correct group selected', async ({ + test('Loads from a workflow using the legacy path separator ("/")', async ({ comfyPage }) => { - const makeGroup = async (name: string, type1: string, type2: string) => { - const node1 = (await comfyPage.nodeOps.getNodeRefsByType(type1))[0] - const node2 = (await comfyPage.nodeOps.getNodeRefsByType(type2))[0] - await node1.click('title') - await node2.click('title', { - modifiers: ['Shift'] - }) - return await node2.convertToGroupNode(name) - } - - const group1 = await makeGroup( - 'g1', - 'CLIPTextEncode', - 'CheckpointLoaderSimple' - ) - const group2 = await makeGroup('g2', 'EmptyLatentImage', 'KSampler') - - const manage1 = await group1.manageGroupNode() - await comfyPage.nextFrame() - await expect(manage1.selectedNodeTypeSelect).toHaveValue('g1') - await manage1.close() - await expect(manage1.root).toBeHidden() - - const manage2 = await group2.manageGroupNode() - await expect(manage2.selectedNodeTypeSelect).toHaveValue('g2') + await comfyPage.workflow.loadWorkflow('groupnodes/legacy_group_node') + await expect.poll(() => comfyPage.nodeOps.getGraphNodesCount()).toBe(1) + await expect( + comfyPage.page.getByTestId(TestIds.dialogs.errorOverlay) + ).toBeHidden() }) test('Preserves hidden input configuration when containing duplicate node types', async ({ @@ -201,50 +67,20 @@ test.describe('Group Node', { tag: '@node' }, () => { .toBe(2) }) - test('Reconnects inputs after configuration changed via manage dialog save', async ({ + test('Manage Group Node dialog opens for an existing group node', async ({ comfyPage }) => { - const expectSingleNode = async (type: string) => { - const nodes = await comfyPage.nodeOps.getNodeRefsByType(type) - expect(nodes).toHaveLength(1) - return nodes[0] - } - const latent = await expectSingleNode('EmptyLatentImage') - const sampler = await expectSingleNode('KSampler') - // Remove existing link - const samplerInput = await sampler.getInput(0) - await samplerInput.removeLinks() - // Group latent + sampler - await latent.click('title', { - modifiers: ['Shift'] - }) - await sampler.click('title', { - modifiers: ['Shift'] - }) - const groupNode = await sampler.convertToGroupNode() - // Connect node to group - const ckpt = await expectSingleNode('CheckpointLoaderSimple') - const input = await ckpt.connectOutput(0, groupNode, 0) - await expect.poll(() => input.getLinkCount()).toBe(1) - // Modify the group node via manage dialog - const manage = await groupNode.manageGroupNode() - await manage.selectNode('KSampler') - await manage.changeTab('Inputs') - await manage.setLabel('model', 'test') - await manage.save() - await manage.close() - // Ensure the link is still present - await expect.poll(() => input.getLinkCount()).toBe(1) - }) + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top') + await comfyPage.workflow.loadWorkflow('groupnodes/group_node_v1.3.3') - test('Loads from a workflow using the legacy path separator ("/")', async ({ - comfyPage - }) => { - await comfyPage.workflow.loadWorkflow('groupnodes/legacy_group_node') - await expect.poll(() => comfyPage.nodeOps.getGraphNodesCount()).toBe(1) - await expect( - comfyPage.page.getByTestId(TestIds.dialogs.errorOverlay) - ).toBeHidden() + const groupNode = await comfyPage.nodeOps.getFirstNodeRef() + if (!groupNode) throw new Error('Group node not found in workflow') + + const manage = await groupNode.manageGroupNode() + await comfyPage.nextFrame() + await expect(manage.selectedNodeTypeSelect).toHaveValue('group_node') + await manage.close() + await expect(manage.root).toBeHidden() }) test.describe('Copy and paste', () => { @@ -299,12 +135,8 @@ test.describe('Group Node', { tag: '@node' }, () => { test('Copies and pastes group node after clearing workflow', async ({ comfyPage }) => { - // Set setting await comfyPage.settings.setSetting('Comfy.ConfirmClear', false) - - // Clear workflow await comfyPage.command.executeCommand('Comfy.ClearWorkflow') - await comfyPage.clipboard.paste() await verifyNodeLoaded(comfyPage, 1) }) @@ -342,22 +174,4 @@ test.describe('Group Node', { tag: '@node' }, () => { }) }) }) - - test.describe('Keybindings', () => { - test('Convert to group node, no selection', async ({ comfyPage }) => { - await expect(comfyPage.toast.visibleToasts).toHaveCount(0) - await comfyPage.page.keyboard.press('Alt+g') - await expect(comfyPage.toast.visibleToasts).toHaveCount(1) - }) - - test('Convert to group node, selected 1 node', async ({ comfyPage }) => { - await expect(comfyPage.toast.visibleToasts).toHaveCount(0) - await comfyPage.canvas.click({ - position: DefaultGraphPositions.textEncodeNode1 - }) - await comfyPage.nextFrame() - await comfyPage.page.keyboard.press('Alt+g') - await expect(comfyPage.toast.visibleToasts).toHaveCount(1) - }) - }) }) diff --git a/browser_tests/tests/rightClickMenu.spec.ts b/browser_tests/tests/rightClickMenu.spec.ts index 49f39860e1..d6d65cb905 100644 --- a/browser_tests/tests/rightClickMenu.spec.ts +++ b/browser_tests/tests/rightClickMenu.spec.ts @@ -35,23 +35,6 @@ test.describe( 'add-group-group-added.png' ) }) - - test('Can convert to group node', async ({ comfyPage }) => { - await comfyPage.nodeOps.selectNodes(['CLIP Text Encode (Prompt)']) - await expect(comfyPage.canvas).toHaveScreenshot('selected-2-nodes.png') - await comfyPage.canvasOps.rightClick() - await comfyPage.contextMenu.clickMenuItem( - 'Convert to Group Node (Deprecated)' - ) - await comfyPage.contextMenu.waitForHidden() - await comfyPage.nodeOps.promptDialogInput.fill('GroupNode2CLIP') - await comfyPage.page.keyboard.press('Enter') - await comfyPage.nodeOps.promptDialogInput.waitFor({ state: 'hidden' }) - await comfyPage.expectScreenshot( - comfyPage.canvas, - 'right-click-node-group-node.png' - ) - }) } ) diff --git a/browser_tests/tests/vueNodes/interactions/node/contextMenu.spec.ts b/browser_tests/tests/vueNodes/interactions/node/contextMenu.spec.ts index 91d90f3481..65be2f9ab4 100644 --- a/browser_tests/tests/vueNodes/interactions/node/contextMenu.spec.ts +++ b/browser_tests/tests/vueNodes/interactions/node/contextMenu.spec.ts @@ -507,25 +507,6 @@ test.describe('Vue Node Context Menu', { tag: '@vue-nodes' }, () => { .toBe(initialGroupCount + 1) }) - test('should convert to group node via context menu', async ({ - comfyPage - }) => { - await openMultiNodeContextMenu(comfyPage, nodeTitles) - await clickExactMenuItem(comfyPage, 'Convert to Group Node') - - await comfyPage.nodeOps.promptDialogInput.waitFor({ state: 'visible' }) - await comfyPage.nodeOps.fillPromptDialog('TestGroupNode') - - await expect - .poll(async () => { - const groupNodes = await comfyPage.nodeOps.getNodeRefsByType( - 'workflow>TestGroupNode' - ) - return groupNodes.length - }) - .toBe(1) - }) - test('should convert selected nodes to subgraph via context menu', async ({ comfyPage }) => { diff --git a/src/composables/graph/contextMenuConverter.ts b/src/composables/graph/contextMenuConverter.ts index cf866a096f..6fbee00b81 100644 --- a/src/composables/graph/contextMenuConverter.ts +++ b/src/composables/graph/contextMenuConverter.ts @@ -245,9 +245,7 @@ const MENU_ORDER: string[] = [ 'Paste Image', 'Save Image', 'Copy (Clipspace)', - 'Paste (Clipspace)', - // Fallback for other core items - 'Convert to Group Node (Deprecated)' + 'Paste (Clipspace)' ] /** diff --git a/src/composables/graph/useMoreOptionsMenu.ts b/src/composables/graph/useMoreOptionsMenu.ts index 0ce59386de..f2a1556c64 100644 --- a/src/composables/graph/useMoreOptionsMenu.ts +++ b/src/composables/graph/useMoreOptionsMenu.ts @@ -45,8 +45,7 @@ export interface SubMenuOption { } export enum BadgeVariant { - NEW = 'new', - DEPRECATED = 'deprecated' + NEW = 'new' } // Global singleton for NodeOptions component reference diff --git a/src/composables/graph/useSelectionMenuOptions.test.ts b/src/composables/graph/useSelectionMenuOptions.test.ts index b35544e87d..57c83d48b5 100644 --- a/src/composables/graph/useSelectionMenuOptions.test.ts +++ b/src/composables/graph/useSelectionMenuOptions.test.ts @@ -72,14 +72,14 @@ describe('useSelectionMenuOptions - multiple nodes options', () => { expect(mocks.frameNodes).toHaveBeenCalledOnce() }) - it('returns Convert to Group Node option from getMultipleNodesOptions', () => { + it('does not include a Convert to Group Node option', () => { const { getMultipleNodesOptions } = useSelectionMenuOptions() const options = getMultipleNodesOptions() const groupNodeOption = options.find( (opt) => opt.label === 'contextMenu.Convert to Group Node' ) - expect(groupNodeOption).toBeDefined() + expect(groupNodeOption).toBeUndefined() }) }) diff --git a/src/composables/graph/useSelectionMenuOptions.ts b/src/composables/graph/useSelectionMenuOptions.ts index b571f9ab5d..a9fa8c63c2 100644 --- a/src/composables/graph/useSelectionMenuOptions.ts +++ b/src/composables/graph/useSelectionMenuOptions.ts @@ -1,8 +1,6 @@ import { computed } from 'vue' import { useI18n } from 'vue-i18n' -import { useCommandStore } from '@/stores/commandStore' - import { useFrameNodes } from './useFrameNodes' import { BadgeVariant } from './useMoreOptionsMenu' import type { MenuOption } from './useMoreOptionsMenu' @@ -102,28 +100,13 @@ export function useSelectionMenuOptions() { return options } - const getMultipleNodesOptions = (): MenuOption[] => { - const convertToGroupNodes = () => { - const commandStore = useCommandStore() - void commandStore.execute( - 'Comfy.GroupNode.ConvertSelectedNodesToGroupNode' - ) + const getMultipleNodesOptions = (): MenuOption[] => [ + { + label: t('g.frameNodes'), + icon: 'icon-[lucide--frame]', + action: frameNodes } - - return [ - { - label: t('contextMenu.Convert to Group Node'), - icon: 'icon-[lucide--group]', - action: convertToGroupNodes, - badge: BadgeVariant.DEPRECATED - }, - { - label: t('g.frameNodes'), - icon: 'icon-[lucide--frame]', - action: frameNodes - } - ] - } + ] const getAlignmentOptions = (): MenuOption[] => [ { diff --git a/src/extensions/core/groupNode.ts b/src/extensions/core/groupNode.ts index aec52616c8..b12dadc649 100644 --- a/src/extensions/core/groupNode.ts +++ b/src/extensions/core/groupNode.ts @@ -1833,38 +1833,6 @@ const replaceLegacySeparators = (nodes: ComfyNode[]): void => { } } -/** - * Convert selected nodes to a group node - * @throws {Error} if no nodes are selected - * @throws {Error} if a group node is already selected - * @throws {Error} if a group node is selected - * - * The context menu item should not be available if any of the above conditions are met. - * The error is automatically handled by the commandStore when the command is executed. - */ -async function convertSelectedNodesToGroupNode() { - const nodes = Object.values(app.canvas.selected_nodes ?? {}) - if (nodes.length === 0) { - throw new Error('No nodes selected') - } - if (nodes.length === 1) { - throw new Error('Please select multiple nodes to convert to group node') - } - - for (const node of nodes) { - if (node instanceof SubgraphNode) { - throw new Error('Selected nodes contain a subgraph node') - } - if (GroupNodeHandler.isGroupNode(node)) { - throw new Error('Selected nodes contain a group node') - } - } - return await GroupNodeHandler.fromNodes(nodes) -} - -const convertDisabled = (selected: LGraphNode[]) => - selected.length < 2 || !!selected.find((n) => GroupNodeHandler.isGroupNode(n)) - function ungroupSelectedGroupNodes() { const nodes = Object.values(app.canvas.selected_nodes ?? {}) for (const node of nodes) { @@ -1900,13 +1868,6 @@ let globalDefs: Record const ext: ComfyExtension = { name: id, commands: [ - { - id: 'Comfy.GroupNode.ConvertSelectedNodesToGroupNode', - label: 'Convert selected nodes to group node', - icon: 'pi pi-sitemap', - versionAdded: '1.3.17', - function: () => convertSelectedNodesToGroupNode() - }, { id: 'Comfy.GroupNode.UngroupSelectedGroupNodes', label: 'Ungroup selected group nodes', @@ -1924,13 +1885,6 @@ const ext: ComfyExtension = { } ], keybindings: [ - { - commandId: 'Comfy.GroupNode.ConvertSelectedNodesToGroupNode', - combo: { - alt: true, - key: 'g' - } - }, { commandId: 'Comfy.GroupNode.UngroupSelectedGroupNodes', combo: { @@ -1942,42 +1896,13 @@ const ext: ComfyExtension = { ], getCanvasMenuItems(canvas): IContextMenuValue[] { - const items: IContextMenuValue[] = [] - const selected = Object.values(canvas.selected_nodes ?? {}) - const convertEnabled = !convertDisabled(selected) - - items.push({ - content: `Convert to Group Node (Deprecated)`, - disabled: !convertEnabled, - // @ts-expect-error async callback - legacy menu API doesn't expect Promise - callback: async () => convertSelectedNodesToGroupNode() - }) - const groups = canvas.graph?.extra?.groupNodes const manageDisabled = !groups || !Object.keys(groups).length - items.push({ - content: `Manage Group Nodes`, - disabled: manageDisabled, - callback: () => manageGroupNodes() - }) - - return items - }, - - getNodeMenuItems(node): IContextMenuValue[] { - if (GroupNodeHandler.isGroupNode(node)) { - return [] - } - - const selected = Object.values(app.canvas.selected_nodes ?? {}) - const convertEnabled = !convertDisabled(selected) - return [ { - content: `Convert to Group Node (Deprecated)`, - disabled: !convertEnabled, - // @ts-expect-error async callback - legacy menu API doesn't expect Promise - callback: async () => convertSelectedNodesToGroupNode() + content: `Manage Group Nodes`, + disabled: manageDisabled, + callback: () => manageGroupNodes() } ] }, diff --git a/src/locales/en/commands.json b/src/locales/en/commands.json index cda6d1e78c..a452a4bf4d 100644 --- a/src/locales/en/commands.json +++ b/src/locales/en/commands.json @@ -158,9 +158,6 @@ "Comfy_Graph_UnpackSubgraph": { "label": "Unpack the selected Subgraph" }, - "Comfy_GroupNode_ConvertSelectedNodesToGroupNode": { - "label": "Convert selected nodes to group node" - }, "Comfy_GroupNode_ManageGroupNodes": { "label": "Manage group nodes" }, diff --git a/src/locales/en/main.json b/src/locales/en/main.json index 3d4cc8dd96..5cff98880c 100644 --- a/src/locales/en/main.json +++ b/src/locales/en/main.json @@ -584,7 +584,6 @@ "Copy (Clipspace)": "Copy (Clipspace)", "Add Node": "Add Node", "Add Group": "Add Group", - "Convert to Group Node": "Convert to Group Node", "Manage Group Nodes": "Manage Group Nodes", "Add Group For Selected Nodes": "Add Group For Selected Nodes", "Save Selected as Template": "Save Selected as Template", @@ -1381,7 +1380,6 @@ "Group Selected Nodes": "Group Selected Nodes", "Toggle promotion of hovered widget": "Toggle promotion of hovered widget", "Unpack the selected Subgraph": "Unpack the selected Subgraph", - "Convert selected nodes to group node": "Convert selected nodes to group node", "Manage group nodes": "Manage group nodes", "Ungroup selected group nodes": "Ungroup selected group nodes", "About ComfyUI": "About ComfyUI",