feat: auto-resolve simple validation errors on widget change and slot connection (#9464)

## 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>
This commit is contained in:
jaeone94
2026-03-13 23:49:44 +09:00
committed by GitHub
parent d73f8e1beb
commit 31a33a0ba2
16 changed files with 1790 additions and 105 deletions

View File

@@ -86,6 +86,7 @@ import { computed, onErrorCaptured, ref, toValue } from 'vue'
import type { Component } from 'vue'
import type {
SafeWidgetData,
VueNodeData,
WidgetSlotMetadata
} from '@/composables/graph/useGraphNodeManager'
@@ -93,6 +94,7 @@ import { useAppMode } from '@/composables/useAppMode'
import { showNodeOptions } from '@/composables/graph/useMoreOptionsMenu'
import { useErrorHandling } from '@/composables/useErrorHandling'
import { st } from '@/i18n'
import type { IWidgetOptions } from '@/lib/litegraph/src/types/widgets'
import { LGraphEventMode } from '@/lib/litegraph/src/types/globalEnums'
import { useSettingStore } from '@/platform/settings/settingStore'
import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions'
@@ -110,6 +112,7 @@ import {
shouldRenderAsVue
} from '@/renderer/extensions/vueNodes/widgets/registry/widgetRegistry'
import { nodeTypeValidForApp } from '@/stores/appModeStore'
import type { WidgetState } from '@/stores/widgetValueStore'
import {
stripGraphPrefix,
useWidgetValueStore
@@ -119,6 +122,8 @@ import { useMissingModelStore } from '@/platform/missingModel/missingModelStore'
import { useExecutionErrorStore } from '@/stores/executionErrorStore'
import type { SimplifiedWidget, WidgetValue } from '@/types/simplifiedWidget'
import { cn } from '@/utils/tailwindUtil'
import { getExecutionIdFromNodeData } from '@/utils/graphTraversalUtil'
import { app } from '@/scripts/app'
import InputSlot from './InputSlot.vue'
@@ -181,6 +186,26 @@ const { getWidgetTooltip, createTooltipConfig } = useNodeTooltips(
)
const widgetValueStore = useWidgetValueStore()
function createWidgetUpdateHandler(
widgetState: WidgetState | undefined,
widget: SafeWidgetData,
nodeExecId: string,
widgetOptions: IWidgetOptions | Record<string, never>
): (newValue: WidgetValue) => void {
return (newValue: WidgetValue) => {
if (widgetState) widgetState.value = newValue
widget.callback?.(newValue)
const effectiveExecId = widget.sourceExecutionId ?? nodeExecId
executionErrorStore.clearWidgetRelatedErrors(
effectiveExecId,
widget.slotName ?? widget.name,
widget.name,
newValue,
{ min: widgetOptions?.min, max: widgetOptions?.max }
)
}
}
interface ProcessedWidget {
advanced: boolean
handleContextMenu: (e: PointerEvent) => void
@@ -198,13 +223,37 @@ interface ProcessedWidget {
slotMetadata?: WidgetSlotMetadata
}
function hasWidgetError(
widget: SafeWidgetData,
nodeExecId: string,
nodeErrors: { errors: { extra_info?: { input_name?: string } }[] } | undefined
): boolean {
const errors = widget.sourceExecutionId
? executionErrorStore.lastNodeErrors?.[widget.sourceExecutionId]?.errors
: nodeErrors?.errors
const inputName = widget.slotName ?? widget.name
return (
!!errors?.some((e) => e.extra_info?.input_name === inputName) ||
missingModelStore.isWidgetMissingModel(
widget.sourceExecutionId ?? nodeExecId,
widget.name
)
)
}
const processedWidgets = computed((): ProcessedWidget[] => {
if (!nodeData?.widgets) return []
const nodeErrors = executionErrorStore.lastNodeErrors?.[nodeData.id ?? '']
// nodeData.id is the local node ID; subgraph nodes need the full execution
// path (e.g. "65:63") to match keys in lastNodeErrors.
const nodeExecId = app.rootGraph
? getExecutionIdFromNodeData(app.rootGraph, nodeData)
: String(nodeData.id ?? '')
const nodeErrors = executionErrorStore.lastNodeErrors?.[nodeExecId]
const graphId = canvasStore.canvas?.graph?.rootGraph.id
const nodeId = nodeData.id
const nodeIdStr = String(nodeId)
const { widgets } = nodeData
const result: ProcessedWidget[] = []
@@ -260,12 +309,12 @@ const processedWidgets = computed((): ProcessedWidget[] => {
spec: widget.spec
}
function updateHandler(newValue: WidgetValue) {
// Update value in store
if (widgetState) widgetState.value = newValue
// Invoke LiteGraph callback wrapper (handles triggerDraw, etc.)
widget.callback?.(newValue)
}
const updateHandler = createWidgetUpdateHandler(
widgetState,
widget,
nodeExecId,
widgetOptions
)
const tooltipText = getWidgetTooltip(widget)
const tooltipConfig = createTooltipConfig(tooltipText)
@@ -286,12 +335,7 @@ const processedWidgets = computed((): ProcessedWidget[] => {
advanced: widget.options?.advanced ?? false,
handleContextMenu,
hasLayoutSize: widget.hasLayoutSize ?? false,
hasError:
(nodeErrors?.errors?.some(
(error) => error.extra_info?.input_name === widget.name
) ??
false) ||
missingModelStore.isWidgetMissingModel(nodeIdStr, widget.name),
hasError: hasWidgetError(widget, nodeExecId, nodeErrors),
hidden: widget.options?.hidden ?? false,
id: String(bareWidgetId),
name: widget.name,