Compare commits

...

3 Commits

Author SHA1 Message Date
jaeone94
64c7a00f64 Merge branch 'main' into jaeone/extract-promoted-widget-error-clearing 2026-07-03 10:14:37 +09:00
jaeone94
1000fbec83 docs: clarify widget error clearing helper contract 2026-07-02 09:36:50 +09:00
jaeone94
6de41edd68 refactor: extract promoted widget error clearing helper 2026-07-02 09:36:50 +09:00
6 changed files with 201 additions and 58 deletions

View File

@@ -12,6 +12,7 @@ import {
import { useI18n } from 'vue-i18n'
import Button from '@/components/ui/button/Button.vue'
import { clearWidgetRelatedErrorScopes } from '@/composables/graph/widgetErrorClearing'
import { widgetPromotedSource } from '@/core/graph/subgraph/promotedInputWidget'
import { isWidgetPromotedOnSubgraphNode } from '@/core/graph/subgraph/promotionUtils'
import { resolvePromotedWidgetSource } from '@/core/graph/subgraph/resolvePromotedWidgetSource'
@@ -247,25 +248,20 @@ function clearWidgetErrors(
const executionId = getExecutionIdByNode(rootGraph, widgetNode)
if (!executionId) return
const options = { min: widget.options?.min, max: widget.options?.max }
const range = { min: widget.options?.min, max: widget.options?.max }
const source = resolvePromotedWidgetSource(rootGraph, widgetNode, widget)
if (source?.sourceExecutionId) {
executionErrorStore.clearWidgetRelatedErrors(
source.sourceExecutionId,
source.sourceWidgetName,
source.sourceWidgetName,
value,
options
)
}
executionErrorStore.clearWidgetRelatedErrors(
executionId,
widget.name,
widget.name,
clearWidgetRelatedErrorScopes({
clearWidgetRelatedErrors: executionErrorStore.clearWidgetRelatedErrors,
host: { executionId, widgetName: widget.name },
source: source?.sourceExecutionId
? {
executionId: source.sourceExecutionId,
widgetName: source.sourceWidgetName
}
: undefined,
value,
options
)
range
})
}
function setWidgetValue(

View File

@@ -1075,10 +1075,10 @@ describe('clearWidgetRelatedErrors parameter routing', () => {
vi.spyOn(app, 'isGraphReady', 'get').mockReturnValue(false)
})
it('passes widgetName (not errorInputName) for model lookup', () => {
it('routes validation and missing-asset widget names separately', () => {
const graph = new LGraph()
const node = new LGraphNode('test')
const widget = node.addWidget('number', 'steps', 42, () => undefined, {
const widget = node.addWidget('number', 'store_name', 42, () => undefined, {
min: 0,
max: 100
})
@@ -1089,12 +1089,12 @@ describe('clearWidgetRelatedErrors parameter routing', () => {
vi.spyOn(app, 'rootGraph', 'get').mockReturnValue(graph)
const clearSpy = vi.spyOn(store, 'clearWidgetRelatedErrors')
node.onWidgetChanged!.call(node, 'steps', 42, 0, widget)
node.onWidgetChanged!.call(node, 'display_name', 42, 0, widget)
expect(clearSpy).toHaveBeenCalledWith(
String(node.id),
'steps',
'steps',
'display_name',
'store_name',
42,
{ min: 0, max: 100 }
)

View File

@@ -6,6 +6,7 @@
* works in legacy canvas mode as well.
*/
import { useChainCallback } from '@/composables/functional/useChainCallback'
import { clearWidgetRelatedErrorScopes } from '@/composables/graph/widgetErrorClearing'
import { resolvePromotedWidgetSource } from '@/core/graph/subgraph/resolvePromotedWidgetSource'
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
import type { LGraph, LGraphNode } from '@/lib/litegraph/src/litegraph'
@@ -93,25 +94,25 @@ function installNodeHooks(node: LGraphNode): void {
const hostExecId = getExecutionIdByNode(app.rootGraph, node)
if (!hostExecId) return
const options = { min: widget.options?.min, max: widget.options?.max }
const executionErrorStore = useExecutionErrorStore()
const range = { min: widget.options?.min, max: widget.options?.max }
const source = resolvePromotedWidgetSource(app.rootGraph, node, widget)
if (source?.sourceExecutionId) {
useExecutionErrorStore().clearWidgetRelatedErrors(
source.sourceExecutionId,
source.sourceWidgetName,
source.sourceWidgetName,
newValue,
options
)
}
useExecutionErrorStore().clearWidgetRelatedErrors(
hostExecId,
name,
widget.name,
newValue,
options
)
clearWidgetRelatedErrorScopes({
clearWidgetRelatedErrors: executionErrorStore.clearWidgetRelatedErrors,
host: {
executionId: hostExecId,
errorInputName: name,
widgetName: widget.name
},
source: source?.sourceExecutionId
? {
executionId: source.sourceExecutionId,
widgetName: source.sourceWidgetName
}
: undefined,
value: newValue,
range
})
}
)
}

View File

@@ -0,0 +1,86 @@
import { describe, expect, it, vi } from 'vitest'
import { clearWidgetRelatedErrorScopes } from '@/composables/graph/widgetErrorClearing'
import { createNodeExecutionId } from '@/types/nodeIdentification'
describe('clearWidgetRelatedErrorScopes', () => {
it('clears only the host scope for normal widgets', () => {
const clearWidgetRelatedErrors = vi.fn()
clearWidgetRelatedErrorScopes({
clearWidgetRelatedErrors,
host: {
executionId: createNodeExecutionId([65]),
widgetName: 'ckpt_name'
},
value: 'real_model.safetensors',
range: { min: 0, max: 10 }
})
expect(clearWidgetRelatedErrors).toHaveBeenCalledOnce()
expect(clearWidgetRelatedErrors).toHaveBeenCalledWith(
createNodeExecutionId([65]),
'ckpt_name',
'ckpt_name',
'real_model.safetensors',
{ min: 0, max: 10 }
)
})
it('preserves distinct validation and missing-asset widget names', () => {
const clearWidgetRelatedErrors = vi.fn()
clearWidgetRelatedErrorScopes({
clearWidgetRelatedErrors,
host: {
executionId: createNodeExecutionId([65]),
errorInputName: 'display_name',
widgetName: 'store_name'
},
value: 'real_model.safetensors'
})
expect(clearWidgetRelatedErrors).toHaveBeenCalledWith(
createNodeExecutionId([65]),
'display_name',
'store_name',
'real_model.safetensors',
undefined
)
})
it('clears promoted source scope before host scope', () => {
const clearWidgetRelatedErrors = vi.fn()
clearWidgetRelatedErrorScopes({
clearWidgetRelatedErrors,
source: {
executionId: createNodeExecutionId([65, 42]),
widgetName: 'ckpt_name'
},
host: {
executionId: createNodeExecutionId([65]),
widgetName: 'promoted_ckpt'
},
value: 'real_model.safetensors',
range: { min: undefined, max: undefined }
})
expect(clearWidgetRelatedErrors).toHaveBeenNthCalledWith(
1,
createNodeExecutionId([65, 42]),
'ckpt_name',
'ckpt_name',
'real_model.safetensors',
{ min: undefined, max: undefined }
)
expect(clearWidgetRelatedErrors).toHaveBeenNthCalledWith(
2,
createNodeExecutionId([65]),
'promoted_ckpt',
'promoted_ckpt',
'real_model.safetensors',
{ min: undefined, max: undefined }
)
})
})

View File

@@ -0,0 +1,64 @@
import type { NodeExecutionId } from '@/types/nodeIdentification'
interface WidgetErrorRangeOptions {
min?: number
max?: number
}
interface WidgetErrorScope {
executionId: NodeExecutionId
/** Validation error key matched against `error.extra_info.input_name`. */
errorInputName?: string
/** Missing model/media store lookup key. */
widgetName: string
}
// Keep this signature aligned with executionErrorStore.clearWidgetRelatedErrors.
type ClearWidgetRelatedErrors = (
executionId: NodeExecutionId,
errorInputName: string,
widgetName: string,
newValue: unknown,
range?: WidgetErrorRangeOptions
) => void
interface ClearWidgetRelatedErrorScopesOptions {
clearWidgetRelatedErrors: ClearWidgetRelatedErrors
host: WidgetErrorScope
source?: WidgetErrorScope
value: unknown
range?: WidgetErrorRangeOptions
}
/**
* Clear the interior promoted-widget source first, then the host widget.
* The host widget's range is applied to both scopes, matching prior behavior.
*/
export function clearWidgetRelatedErrorScopes({
clearWidgetRelatedErrors,
host,
source,
value,
range
}: ClearWidgetRelatedErrorScopesOptions): void {
if (source) {
applyScopeClear(clearWidgetRelatedErrors, source, value, range)
}
applyScopeClear(clearWidgetRelatedErrors, host, value, range)
}
function applyScopeClear(
clearWidgetRelatedErrors: ClearWidgetRelatedErrors,
scope: WidgetErrorScope,
value: unknown,
range?: WidgetErrorRangeOptions
): void {
clearWidgetRelatedErrors(
scope.executionId,
scope.errorInputName ?? scope.widgetName,
scope.widgetName,
value,
range
)
}

View File

@@ -7,6 +7,7 @@ import type {
VueNodeData,
WidgetSlotMetadata
} from '@/composables/graph/useGraphNodeManager'
import { clearWidgetRelatedErrorScopes } from '@/composables/graph/widgetErrorClearing'
import { useAppMode } from '@/composables/useAppMode'
import { showNodeOptions } from '@/composables/graph/useMoreOptionsMenu'
import type { IWidgetOptions } from '@/lib/litegraph/src/types/widgets'
@@ -97,24 +98,19 @@ function createWidgetUpdateHandler(
return (newValue: WidgetValue) => {
if (widgetState) widgetState.value = newValue
widget.callback?.(newValue)
const options = { min: widgetOptions?.min, max: widgetOptions?.max }
if (widget.sourceExecutionId) {
const sourceWidgetName = widget.sourceWidgetName ?? widget.name
executionErrorStore.clearWidgetRelatedErrors(
widget.sourceExecutionId,
sourceWidgetName,
sourceWidgetName,
newValue,
options
)
}
executionErrorStore.clearWidgetRelatedErrors(
nodeExecId,
widget.name,
widget.name,
newValue,
options
)
const range = { min: widgetOptions?.min, max: widgetOptions?.max }
clearWidgetRelatedErrorScopes({
clearWidgetRelatedErrors: executionErrorStore.clearWidgetRelatedErrors,
host: { executionId: nodeExecId, widgetName: widget.name },
source: widget.sourceExecutionId
? {
executionId: widget.sourceExecutionId,
widgetName: widget.sourceWidgetName ?? widget.name
}
: undefined,
value: newValue,
range
})
}
}