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
This commit is contained in:
Glary-Bot
2026-05-19 23:26:30 +00:00
parent 64c75bfce5
commit f460e105e9
12 changed files with 35 additions and 378 deletions

View File

@@ -216,16 +216,6 @@ export class NodeOperationsHelper {
}
}
async convertAllNodesToGroupNode(groupNodeName: string): Promise<void> {
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<void> {
await this.promptDialogInput.fill(value)
await this.page.keyboard.press('Enter')

View File

@@ -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()

View File

@@ -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)
})
})
})

View File

@@ -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'
)
})
}
)

View File

@@ -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
}) => {

View File

@@ -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)'
]
/**

View File

@@ -45,8 +45,7 @@ export interface SubMenuOption {
}
export enum BadgeVariant {
NEW = 'new',
DEPRECATED = 'deprecated'
NEW = 'new'
}
// Global singleton for NodeOptions component reference

View File

@@ -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()
})
})

View File

@@ -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[] => [
{

View File

@@ -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<string, ComfyNodeDef>
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()
}
]
},

View File

@@ -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"
},

View File

@@ -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",