mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-14 17:37:46 +00:00
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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user