Compare commits

..

1 Commits

Author SHA1 Message Date
Terry Jia
e154bba37e feat: add proxyWidgetSelector support to SubgraphNode 2026-04-03 18:51:54 -04:00
5 changed files with 90 additions and 30 deletions

View File

@@ -164,7 +164,11 @@ import {
} from '@/platform/workflow/management/stores/workflowStore'
import { useWorkspaceStore } from '@/stores/workspaceStore'
import type { TreeExplorerNode, TreeNode } from '@/types/treeExplorerTypes'
import { ensureWorkflowSuffix, getWorkflowSuffix } from '@/utils/formatUtil'
import {
ensureWorkflowSuffix,
getFilenameDetails,
getWorkflowSuffix
} from '@/utils/formatUtil'
import { buildTree, sortedTree } from '@/utils/treeUtil'
const { title, filter, searchSubject, dataTestid, hideLeafIcon } = defineProps<{
@@ -324,9 +328,7 @@ const renderTreeNode = (
}
: { handleClick }
const label = node.leaf
? (node.key.split('/').pop() ?? node.label)
: node.label
const label = node.leaf ? getFilenameDetails(node.label).filename : node.label
return {
key: node.key,

View File

@@ -16,7 +16,10 @@ import type {
INodeInputSlot,
INodeOutputSlot
} from '@/lib/litegraph/src/interfaces'
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
import type {
IBaseWidget,
IWidgetOptions
} from '@/lib/litegraph/src/types/widgets'
import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations'
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
import { LayoutSource } from '@/renderer/core/layout/types'
@@ -74,6 +77,7 @@ export interface SafeWidgetData {
advanced?: boolean
hidden?: boolean
read_only?: boolean
values?: IWidgetOptions['values']
}
/** Input specification from node definition */
spec?: InputSpec
@@ -222,7 +226,8 @@ function safeWidgetMapper(
canvasOnly: widget.options.canvasOnly,
advanced: widget.options?.advanced ?? widget.advanced,
hidden: widget.options.hidden,
read_only: widget.options.read_only
read_only: widget.options.read_only,
values: widget.options.values
}
}

View File

@@ -10,6 +10,15 @@ const proxyWidgetTupleSchema = z.union([
const proxyWidgetsPropertySchema = z.array(proxyWidgetTupleSchema)
type ProxyWidgetsProperty = z.infer<typeof proxyWidgetsPropertySchema>
export interface ProxyWidgetSelector {
name?: string
selected: string
options: {
label: string
widgets?: string[][]
}[]
}
export function parseProxyWidgets(
property: NodeProperty | undefined
): ProxyWidgetsProperty {

View File

@@ -45,6 +45,7 @@ import {
supportsVirtualCanvasImagePreview
} from '@/composables/node/canvasImagePreviewTypes'
import { parseProxyWidgets } from '@/core/schemas/promotionSchema'
import type { ProxyWidgetSelector } from '@/core/schemas/promotionSchema'
import { useDomWidgetStore } from '@/stores/domWidgetStore'
import {
makePromotionEntryKey,
@@ -116,6 +117,7 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
hasMissingBoundSourceWidget: boolean
views: PromotedWidgetView[]
}
private _selectorWidget: IBaseWidget | null = null
// Declared as accessor via Object.defineProperty in constructor.
// TypeScript doesn't allow overriding a property with get/set syntax,
@@ -297,6 +299,69 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
return views
}
private _getWidgetsWithSelector(): IBaseWidget[] {
const views = this._getPromotedViews()
const selector = this.properties.proxyWidgetSelector as
| ProxyWidgetSelector
| undefined
if (!selector?.options?.length || !this._selectorWidget) return views
const selectedOption =
selector.options.find((opt) => opt.label === selector.selected) ??
selector.options[0]
if (!selectedOption?.widgets) return [this._selectorWidget, ...views]
const allGroupedKeys = new Set(
selector.options.flatMap((opt) =>
(opt.widgets ?? []).map(([nid, wn]) => `${nid}:${wn}`)
)
)
const selectedKeys = new Set(
selectedOption.widgets.map(([nid, wn]) => `${nid}:${wn}`)
)
const filteredViews = views.filter((v) => {
const key = `${v.sourceNodeId}:${v.sourceWidgetName}`
return !allGroupedKeys.has(key) || selectedKeys.has(key)
})
return [this._selectorWidget, ...filteredViews]
}
private _initSelectorWidget(): void {
const selector = this.properties.proxyWidgetSelector as
| ProxyWidgetSelector
| undefined
if (!selector?.options?.length) {
this._selectorWidget = null
return
}
const validLabels = selector.options.map((o) => o.label)
if (!validLabels.includes(selector.selected)) {
selector.selected = validLabels[0]
}
this._selectorWidget = {
name: selector.name ?? 'selector',
type: 'combo',
value: selector.selected,
y: 0,
serialize: false,
options: {
values: validLabels
},
callback: (value: unknown) => {
selector.selected = String(value)
this._selectorWidget!.value = String(value)
this._invalidatePromotedViewsCache()
const minSize = this.computeSize()
this.setSize([this.size[0], minSize[1]])
this.graph?.setDirtyCanvas(true, true)
}
}
}
private _invalidatePromotedViewsCache(): void {
this._cacheVersion++
}
@@ -695,7 +760,7 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
// Synthetic widgets getter — SubgraphNodes have no native widgets.
Object.defineProperty(this, 'widgets', {
get: () => this._getPromotedViews(),
get: () => this._getWidgetsWithSelector(),
set: () => {
if (import.meta.env.DEV)
console.warn(
@@ -1097,6 +1162,8 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
this.properties.proxyWidgets = serialized
}
this._initSelectorWidget()
// Check all inputs for connected widgets
for (const input of this.inputs) {
const subgraphInput = input._subgraphSlot

View File

@@ -65,29 +65,6 @@ describe('buildTree', () => {
})
})
describe('buildTree workflow keys preserve file extensions', () => {
it('should set leaf label to full filename including extension', () => {
const workflows = [
{ key: 'Workflow-A.json' },
{ key: 'subfolder/Workflow-B.json' },
{ key: 'my-app.app.json' }
]
const tree = buildTree(workflows, (w) => w.key.split('/'))
const leafLabels = (node: TreeNode): string[] =>
node.leaf
? [node.key.split('/').pop()!]
: (node.children ?? []).flatMap(leafLabels)
expect(leafLabels(tree)).toEqual([
'Workflow-A.json',
'Workflow-B.json',
'my-app.app.json'
])
})
})
describe('sortedTree', () => {
const createNode = (label: string, leaf = false): TreeNode => ({
key: label,