mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-20 14:30:41 +00:00
## Summary Automatically clears transient validation errors (`value_bigger_than_max`, `value_smaller_than_min`, `value_not_in_list`, `required_input_missing`) when the user modifies a widget value or connects an input slot, so resolved errors don't linger in the error panel. Also clears missing model state when the user changes a combo widget value. ## Changes - **`useNodeErrorAutoResolve` composable**: watches widget changes and slot connections, clears matching errors via `executionErrorStore` - **`executionErrorStore`**: adds `clearSimpleNodeErrors` and `clearSimpleWidgetErrorIfValid` with granular per-slot error removal - **`executionErrorUtil`**: adds `isValueStillOutOfRange` to prevent premature clearing when a new value still violates the constraint - **`graphTraversalUtil`**: adds `getExecutionIdFromNodeData` for subgraph-aware execution ID resolution - **`GraphCanvas.vue`**: fixes subgraph error key lookup by using `getExecutionIdByNode` instead of raw `node.id` - **`NodeWidgets.vue`**: wires up the new composable to the widget layer - **`missingModelStore`**: adds `removeMissingModelByWidget` to clear missing model state on widget value change - **`useGraphNodeManager`**: registers composable per node - **Tests**: 126 new unit tests covering error clearing, range validation, and graph traversal edge cases ## Screenshots https://github.com/user-attachments/assets/515ea811-ff84-482a-a866-a17e5c779c39 https://github.com/user-attachments/assets/a2b30f02-4929-4537-952c-a0febe20f02e ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-9464-feat-auto-resolve-simple-validation-errors-on-widget-change-and-slot-connection-31b6d73d3650816b8afdc34f4b40295a) by [Unito](https://www.unito.io) --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
109 lines
3.4 KiB
TypeScript
109 lines
3.4 KiB
TypeScript
/**
|
|
* Installs per-node error-clearing callbacks (onConnectionsChange,
|
|
* onWidgetChanged) on all current and future nodes in a graph.
|
|
*
|
|
* Decoupled from the Vue rendering lifecycle so that error auto-clearing
|
|
* works in legacy canvas mode as well.
|
|
*/
|
|
import { useChainCallback } from '@/composables/functional/useChainCallback'
|
|
import { isPromotedWidgetView } from '@/core/graph/subgraph/promotedWidgetTypes'
|
|
import { resolveConcretePromotedWidget } from '@/core/graph/subgraph/resolveConcretePromotedWidget'
|
|
import type { LGraph, LGraphNode } from '@/lib/litegraph/src/litegraph'
|
|
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
|
|
import { NodeSlotType } from '@/lib/litegraph/src/types/globalEnums'
|
|
import { app } from '@/scripts/app'
|
|
import { useExecutionErrorStore } from '@/stores/executionErrorStore'
|
|
import { getExecutionIdByNode } from '@/utils/graphTraversalUtil'
|
|
|
|
function resolvePromotedExecId(
|
|
rootGraph: LGraph,
|
|
node: LGraphNode,
|
|
widget: IBaseWidget,
|
|
hostExecId: string
|
|
): string {
|
|
if (!isPromotedWidgetView(widget)) return hostExecId
|
|
const result = resolveConcretePromotedWidget(
|
|
node,
|
|
widget.sourceNodeId,
|
|
widget.sourceWidgetName
|
|
)
|
|
if (result.status === 'resolved' && result.resolved.node) {
|
|
return getExecutionIdByNode(rootGraph, result.resolved.node) ?? hostExecId
|
|
}
|
|
return hostExecId
|
|
}
|
|
|
|
const hookedNodes = new WeakSet<LGraphNode>()
|
|
|
|
function installNodeHooks(node: LGraphNode): void {
|
|
if (hookedNodes.has(node)) return
|
|
hookedNodes.add(node)
|
|
|
|
node.onConnectionsChange = useChainCallback(
|
|
node.onConnectionsChange,
|
|
function (type, slotIndex, isConnected) {
|
|
if (type !== NodeSlotType.INPUT || !isConnected) return
|
|
if (!app.rootGraph) return
|
|
const slotName = node.inputs?.[slotIndex]?.name
|
|
if (!slotName) return
|
|
const execId = getExecutionIdByNode(app.rootGraph, node)
|
|
if (!execId) return
|
|
useExecutionErrorStore().clearSimpleNodeErrors(execId, slotName)
|
|
}
|
|
)
|
|
|
|
node.onWidgetChanged = useChainCallback(
|
|
node.onWidgetChanged,
|
|
// _name is the LiteGraph callback arg; re-derive from the widget
|
|
// object to handle promoted widgets where sourceWidgetName differs.
|
|
function (_name, newValue, _oldValue, widget) {
|
|
if (!app.rootGraph) return
|
|
const hostExecId = getExecutionIdByNode(app.rootGraph, node)
|
|
if (!hostExecId) return
|
|
|
|
const execId = resolvePromotedExecId(
|
|
app.rootGraph,
|
|
node,
|
|
widget,
|
|
hostExecId
|
|
)
|
|
const widgetName = isPromotedWidgetView(widget)
|
|
? widget.sourceWidgetName
|
|
: widget.name
|
|
|
|
useExecutionErrorStore().clearWidgetRelatedErrors(
|
|
execId,
|
|
widget.name,
|
|
widgetName,
|
|
newValue,
|
|
{ min: widget.options?.min, max: widget.options?.max }
|
|
)
|
|
}
|
|
)
|
|
}
|
|
|
|
function installNodeHooksRecursive(node: LGraphNode): void {
|
|
installNodeHooks(node)
|
|
if (node.isSubgraphNode?.()) {
|
|
for (const innerNode of node.subgraph._nodes ?? []) {
|
|
installNodeHooksRecursive(innerNode)
|
|
}
|
|
}
|
|
}
|
|
|
|
export function installErrorClearingHooks(graph: LGraph): () => void {
|
|
for (const node of graph._nodes ?? []) {
|
|
installNodeHooksRecursive(node)
|
|
}
|
|
|
|
const originalOnNodeAdded = graph.onNodeAdded
|
|
graph.onNodeAdded = function (node: LGraphNode) {
|
|
installNodeHooksRecursive(node)
|
|
originalOnNodeAdded?.call(this, node)
|
|
}
|
|
|
|
return () => {
|
|
graph.onNodeAdded = originalOnNodeAdded || undefined
|
|
}
|
|
}
|