fix: App mode - renaming widgets on subgraphs (#10245)

## Summary

Fixes renaming of widgets from subgraph nodes in app builder/app mode.

## Changes

- **What**: If the widget is from a subgraph node and no parents passed,
use the node as the subgraph parent.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10245-fix-App-mode-renaming-widgets-on-subgraphs-3276d73d3650815bb131c840df43cdf2)
by [Unito](https://www.unito.io)
This commit is contained in:
pythongosssss
2026-03-19 11:00:31 +00:00
committed by GitHub
parent 3591579141
commit 77ddda9d3c
8 changed files with 403 additions and 5 deletions

View File

@@ -1,8 +1,19 @@
import { describe, expect, it } from 'vitest'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
import type { INodeInputSlot } from '@/lib/litegraph/src/interfaces'
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
import { getWidgetDefaultValue } from '@/utils/widgetUtil'
import { getWidgetDefaultValue, renameWidget } from '@/utils/widgetUtil'
vi.mock('@/core/graph/subgraph/resolvePromotedWidgetSource', () => ({
resolvePromotedWidgetSource: vi.fn()
}))
import { resolvePromotedWidgetSource } from '@/core/graph/subgraph/resolvePromotedWidgetSource'
const mockedResolve = vi.mocked(resolvePromotedWidgetSource)
describe('getWidgetDefaultValue', () => {
it('returns undefined for undefined spec', () => {
@@ -37,3 +48,98 @@ describe('getWidgetDefaultValue', () => {
expect(getWidgetDefaultValue(spec)).toBeUndefined()
})
})
function makeWidget(overrides: Record<string, unknown> = {}): IBaseWidget {
return {
name: 'myWidget',
type: 'number',
value: 0,
label: undefined,
options: {},
...overrides
} as unknown as IBaseWidget
}
function makeNode({
isSubgraph = false,
inputs = [] as INodeInputSlot[]
}: {
isSubgraph?: boolean
inputs?: INodeInputSlot[]
} = {}): LGraphNode {
return {
id: 1,
inputs,
isSubgraphNode: () => isSubgraph
} as unknown as LGraphNode
}
describe('renameWidget', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('renames a regular widget and its matching input', () => {
const widget = makeWidget({ name: 'seed' })
const input = { name: 'seed', widget: { name: 'seed' } } as INodeInputSlot
const node = makeNode({ inputs: [input] })
const result = renameWidget(widget, node, 'My Seed')
expect(result).toBe(true)
expect(widget.label).toBe('My Seed')
expect(input.label).toBe('My Seed')
})
it('clears label when given empty string', () => {
const widget = makeWidget({ name: 'seed', label: 'Old Label' })
const node = makeNode()
renameWidget(widget, node, '')
expect(widget.label).toBeUndefined()
})
it('renames promoted widget source when node is a subgraph without explicit parents', () => {
const sourceWidget = makeWidget({ name: 'innerSeed' })
const interiorInput = {
name: 'innerSeed',
widget: { name: 'innerSeed' }
} as INodeInputSlot
const interiorNode = makeNode({ inputs: [interiorInput] })
mockedResolve.mockReturnValue({
widget: sourceWidget,
node: interiorNode
})
const promotedWidget = makeWidget({
name: 'seed',
sourceNodeId: '5',
sourceWidgetName: 'innerSeed'
})
const subgraphNode = makeNode({ isSubgraph: true })
const result = renameWidget(promotedWidget, subgraphNode, 'Renamed')
expect(result).toBe(true)
expect(sourceWidget.label).toBe('Renamed')
expect(interiorInput.label).toBe('Renamed')
expect(promotedWidget.label).toBe('Renamed')
})
it('does not resolve promoted widget source for non-subgraph node without parents', () => {
const promotedWidget = makeWidget({
name: 'seed',
sourceNodeId: '5',
sourceWidgetName: 'innerSeed'
})
const node = makeNode({ isSubgraph: false })
const result = renameWidget(promotedWidget, node, 'Renamed')
expect(result).toBe(true)
expect(mockedResolve).not.toHaveBeenCalled()
expect(promotedWidget.label).toBe('Renamed')
})
})

View File

@@ -48,7 +48,10 @@ export function renameWidget(
newLabel: string,
parents?: SubgraphNode[]
): boolean {
if (isPromotedWidgetView(widget) && parents?.length) {
if (
isPromotedWidgetView(widget) &&
(parents?.length || node.isSubgraphNode())
) {
const sourceWidget = resolvePromotedWidgetSource(node, widget)
if (!sourceWidget) {
console.error('Could not resolve source widget for promoted widget')