mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-28 02:02:08 +00:00
Ensure widgets always get a single callback (#7579)
The other side of reactivity. Ensure that vue mode always registers a callback on litegraph nodes and never registers more than one. ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-7579-Ensure-widgets-always-get-a-single-callback-2cc6d73d365081e2a488c38ae394efc0) by [Unito](https://www.unito.io)
This commit is contained in:
@@ -2,6 +2,7 @@
|
|||||||
import { computed, provide } from 'vue'
|
import { computed, provide } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
import { useReactiveWidgetValue } from '@/composables/graph/useGraphNodeManager'
|
||||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||||
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
|
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
|
||||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||||
@@ -71,7 +72,7 @@ const displayLabel = computed(
|
|||||||
<component
|
<component
|
||||||
:is="getWidgetComponent(widget)"
|
:is="getWidgetComponent(widget)"
|
||||||
:widget="widget"
|
:widget="widget"
|
||||||
:model-value="widget.value"
|
:model-value="useReactiveWidgetValue(widget)"
|
||||||
:node-id="String(node.id)"
|
:node-id="String(node.id)"
|
||||||
:node-type="node.type"
|
:node-type="node.type"
|
||||||
:class="cn('col-span-1', shouldExpand(widget.type) && 'min-h-36')"
|
:class="cn('col-span-1', shouldExpand(widget.type) && 'min-h-36')"
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* Provides event-driven reactivity with performance optimizations
|
* Provides event-driven reactivity with performance optimizations
|
||||||
*/
|
*/
|
||||||
import { reactiveComputed } from '@vueuse/core'
|
import { reactiveComputed } from '@vueuse/core'
|
||||||
import { reactive, shallowReactive } from 'vue'
|
import { customRef, reactive, shallowReactive } from 'vue'
|
||||||
|
|
||||||
import { useChainCallback } from '@/composables/functional/useChainCallback'
|
import { useChainCallback } from '@/composables/functional/useChainCallback'
|
||||||
import { isProxyWidget } from '@/core/graph/subgraph/proxyWidget'
|
import { isProxyWidget } from '@/core/graph/subgraph/proxyWidget'
|
||||||
@@ -90,6 +90,23 @@ export interface GraphNodeManager {
|
|||||||
cleanup(): void
|
cleanup(): void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function widgetWithVueTrack(
|
||||||
|
widget: IBaseWidget
|
||||||
|
): asserts widget is IBaseWidget & { vueTrack: () => void } {
|
||||||
|
if (widget.vueTrack) return
|
||||||
|
|
||||||
|
customRef((track, trigger) => {
|
||||||
|
widget.callback = useChainCallback(widget.callback, trigger)
|
||||||
|
widget.vueTrack = track
|
||||||
|
return { get() {}, set() {} }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
export function useReactiveWidgetValue(widget: IBaseWidget) {
|
||||||
|
widgetWithVueTrack(widget)
|
||||||
|
widget.vueTrack()
|
||||||
|
return widget.value
|
||||||
|
}
|
||||||
|
|
||||||
function getControlWidget(widget: IBaseWidget): SafeControlWidget | undefined {
|
function getControlWidget(widget: IBaseWidget): SafeControlWidget | undefined {
|
||||||
const cagWidget = widget.linkedWidgets?.find(
|
const cagWidget = widget.linkedWidgets?.find(
|
||||||
(w) => w.name == 'control_after_generate'
|
(w) => w.name == 'control_after_generate'
|
||||||
@@ -106,6 +123,37 @@ function getNodeType(node: LGraphNode, widget: IBaseWidget) {
|
|||||||
return subNode?.type
|
return subNode?.type
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates that a value is a valid WidgetValue type
|
||||||
|
*/
|
||||||
|
const normalizeWidgetValue = (value: unknown): WidgetValue => {
|
||||||
|
if (value === null || value === undefined || value === void 0) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
typeof value === 'string' ||
|
||||||
|
typeof value === 'number' ||
|
||||||
|
typeof value === 'boolean'
|
||||||
|
) {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
if (typeof value === 'object') {
|
||||||
|
// Check if it's a File array
|
||||||
|
if (
|
||||||
|
Array.isArray(value) &&
|
||||||
|
value.length > 0 &&
|
||||||
|
value.every((item): item is File => item instanceof File)
|
||||||
|
) {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
// Otherwise it's a generic object
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
// If none of the above, return undefined
|
||||||
|
console.warn(`Invalid widget value type: ${typeof value}`, value)
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
export function safeWidgetMapper(
|
export function safeWidgetMapper(
|
||||||
node: LGraphNode,
|
node: LGraphNode,
|
||||||
slotMetadata: Map<string, WidgetSlotMetadata>
|
slotMetadata: Map<string, WidgetSlotMetadata>
|
||||||
@@ -113,19 +161,6 @@ export function safeWidgetMapper(
|
|||||||
const nodeDefStore = useNodeDefStore()
|
const nodeDefStore = useNodeDefStore()
|
||||||
return function (widget) {
|
return function (widget) {
|
||||||
try {
|
try {
|
||||||
// TODO: Use widget.getReactiveData() once TypeScript types are updated
|
|
||||||
let value = widget.value
|
|
||||||
|
|
||||||
// For combo widgets, if value is undefined, use the first option as default
|
|
||||||
if (
|
|
||||||
value === undefined &&
|
|
||||||
widget.type === 'combo' &&
|
|
||||||
widget.options?.values &&
|
|
||||||
Array.isArray(widget.options.values) &&
|
|
||||||
widget.options.values.length > 0
|
|
||||||
) {
|
|
||||||
value = widget.options.values[0]
|
|
||||||
}
|
|
||||||
const spec = nodeDefStore.getInputSpecForWidget(node, widget.name)
|
const spec = nodeDefStore.getInputSpecForWidget(node, widget.name)
|
||||||
const slotInfo = slotMetadata.get(widget.name)
|
const slotInfo = slotMetadata.get(widget.name)
|
||||||
const borderStyle = widget.promoted
|
const borderStyle = widget.promoted
|
||||||
@@ -133,13 +168,18 @@ export function safeWidgetMapper(
|
|||||||
: widget.advanced
|
: widget.advanced
|
||||||
? 'ring ring-component-node-widget-advanced'
|
? 'ring ring-component-node-widget-advanced'
|
||||||
: undefined
|
: undefined
|
||||||
|
const callback = (v: unknown) => {
|
||||||
|
const value = normalizeWidgetValue(v)
|
||||||
|
widget.value = value ?? undefined
|
||||||
|
widget.callback?.(value)
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: widget.name,
|
name: widget.name,
|
||||||
type: widget.type,
|
type: widget.type,
|
||||||
value: value,
|
value: useReactiveWidgetValue(widget),
|
||||||
borderStyle,
|
borderStyle,
|
||||||
callback: widget.callback,
|
callback,
|
||||||
controlWidget: getControlWidget(widget),
|
controlWidget: getControlWidget(widget),
|
||||||
isDOMWidget: isDOMWidget(widget),
|
isDOMWidget: isDOMWidget(widget),
|
||||||
label: widget.label,
|
label: widget.label,
|
||||||
@@ -286,128 +326,6 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager {
|
|||||||
return nodeRefs.get(id)
|
return nodeRefs.get(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates that a value is a valid WidgetValue type
|
|
||||||
*/
|
|
||||||
const validateWidgetValue = (value: unknown): WidgetValue => {
|
|
||||||
if (value === null || value === undefined || value === void 0) {
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
typeof value === 'string' ||
|
|
||||||
typeof value === 'number' ||
|
|
||||||
typeof value === 'boolean'
|
|
||||||
) {
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
if (typeof value === 'object') {
|
|
||||||
// Check if it's a File array
|
|
||||||
if (
|
|
||||||
Array.isArray(value) &&
|
|
||||||
value.length > 0 &&
|
|
||||||
value.every((item): item is File => item instanceof File)
|
|
||||||
) {
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
// Otherwise it's a generic object
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
// If none of the above, return undefined
|
|
||||||
console.warn(`Invalid widget value type: ${typeof value}`, value)
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates Vue state when widget values change
|
|
||||||
*/
|
|
||||||
const updateVueWidgetState = (
|
|
||||||
nodeId: string,
|
|
||||||
widgetName: string,
|
|
||||||
value: unknown
|
|
||||||
): void => {
|
|
||||||
try {
|
|
||||||
const currentData = vueNodeData.get(nodeId)
|
|
||||||
if (!currentData?.widgets) return
|
|
||||||
|
|
||||||
const updatedWidgets = currentData.widgets.map((w) =>
|
|
||||||
w.name === widgetName ? { ...w, value: validateWidgetValue(value) } : w
|
|
||||||
)
|
|
||||||
// Create a completely new object to ensure Vue reactivity triggers
|
|
||||||
const updatedData = {
|
|
||||||
...currentData,
|
|
||||||
widgets: updatedWidgets
|
|
||||||
}
|
|
||||||
|
|
||||||
vueNodeData.set(nodeId, updatedData)
|
|
||||||
} catch (error) {
|
|
||||||
// Ignore widget update errors to prevent cascade failures
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a wrapped callback for a widget that maintains LiteGraph/Vue sync
|
|
||||||
*/
|
|
||||||
const createWrappedWidgetCallback = (
|
|
||||||
widget: IBaseWidget, // LiteGraph widget with minimal typing
|
|
||||||
originalCallback: ((value: unknown) => void) | undefined,
|
|
||||||
nodeId: string
|
|
||||||
) => {
|
|
||||||
let updateInProgress = false
|
|
||||||
|
|
||||||
return (value: unknown) => {
|
|
||||||
if (updateInProgress) return
|
|
||||||
updateInProgress = true
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 1. Update the widget value in LiteGraph (critical for LiteGraph state)
|
|
||||||
// Validate that the value is of an acceptable type
|
|
||||||
if (
|
|
||||||
value !== null &&
|
|
||||||
value !== undefined &&
|
|
||||||
typeof value !== 'string' &&
|
|
||||||
typeof value !== 'number' &&
|
|
||||||
typeof value !== 'boolean' &&
|
|
||||||
typeof value !== 'object'
|
|
||||||
) {
|
|
||||||
console.warn(`Invalid widget value type: ${typeof value}`)
|
|
||||||
updateInProgress = false
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Always update widget.value to ensure sync
|
|
||||||
widget.value = value ?? undefined
|
|
||||||
|
|
||||||
// 2. Call the original callback if it exists
|
|
||||||
if (originalCallback && widget.type !== 'asset') {
|
|
||||||
originalCallback.call(widget, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Update Vue state to maintain synchronization
|
|
||||||
updateVueWidgetState(nodeId, widget.name, value)
|
|
||||||
} finally {
|
|
||||||
updateInProgress = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets up widget callbacks for a node
|
|
||||||
*/
|
|
||||||
const setupNodeWidgetCallbacks = (node: LGraphNode) => {
|
|
||||||
if (!node.widgets) return
|
|
||||||
|
|
||||||
const nodeId = String(node.id)
|
|
||||||
|
|
||||||
node.widgets.forEach((widget) => {
|
|
||||||
const originalCallback = widget.callback
|
|
||||||
widget.callback = createWrappedWidgetCallback(
|
|
||||||
widget,
|
|
||||||
originalCallback,
|
|
||||||
nodeId
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const syncWithGraph = () => {
|
const syncWithGraph = () => {
|
||||||
if (!graph?._nodes) return
|
if (!graph?._nodes) return
|
||||||
|
|
||||||
@@ -428,9 +346,6 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager {
|
|||||||
// Store non-reactive reference
|
// Store non-reactive reference
|
||||||
nodeRefs.set(id, node)
|
nodeRefs.set(id, node)
|
||||||
|
|
||||||
// Set up widget callbacks BEFORE extracting data (critical order)
|
|
||||||
setupNodeWidgetCallbacks(node)
|
|
||||||
|
|
||||||
// Extract and store safe data for Vue
|
// Extract and store safe data for Vue
|
||||||
vueNodeData.set(id, extractVueNodeData(node))
|
vueNodeData.set(id, extractVueNodeData(node))
|
||||||
})
|
})
|
||||||
@@ -449,9 +364,6 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager {
|
|||||||
// Store non-reactive reference to original node
|
// Store non-reactive reference to original node
|
||||||
nodeRefs.set(id, node)
|
nodeRefs.set(id, node)
|
||||||
|
|
||||||
// Set up widget callbacks BEFORE extracting data (critical order)
|
|
||||||
setupNodeWidgetCallbacks(node)
|
|
||||||
|
|
||||||
// Extract initial data for Vue (may be incomplete during graph configure)
|
// Extract initial data for Vue (may be incomplete during graph configure)
|
||||||
vueNodeData.set(id, extractVueNodeData(node))
|
vueNodeData.set(id, extractVueNodeData(node))
|
||||||
|
|
||||||
|
|||||||
@@ -211,11 +211,9 @@ function newProxyFromOverlay(subgraphNode: SubgraphNode, overlay: Overlay) {
|
|||||||
}
|
}
|
||||||
return Reflect.get(redirectedTarget, property, redirectedReceiver)
|
return Reflect.get(redirectedTarget, property, redirectedReceiver)
|
||||||
},
|
},
|
||||||
set(_t: IBaseWidget, property: string, value: unknown, receiver: object) {
|
set(_t: IBaseWidget, property: string, value: unknown) {
|
||||||
let redirectedTarget: object = backingWidget
|
let redirectedTarget: object = backingWidget
|
||||||
let redirectedReceiver = receiver
|
if (property == 'computedHeight') {
|
||||||
if (property == 'value') redirectedReceiver = backingWidget
|
|
||||||
else if (property == 'computedHeight') {
|
|
||||||
if (overlay.widgetName.startsWith('$$') && linkedNode) {
|
if (overlay.widgetName.startsWith('$$') && linkedNode) {
|
||||||
updatePreviews(linkedNode)
|
updatePreviews(linkedNode)
|
||||||
}
|
}
|
||||||
@@ -228,9 +226,8 @@ function newProxyFromOverlay(subgraphNode: SubgraphNode, overlay: Overlay) {
|
|||||||
}
|
}
|
||||||
if (Object.prototype.hasOwnProperty.call(overlay, property)) {
|
if (Object.prototype.hasOwnProperty.call(overlay, property)) {
|
||||||
redirectedTarget = overlay
|
redirectedTarget = overlay
|
||||||
redirectedReceiver = overlay
|
|
||||||
}
|
}
|
||||||
return Reflect.set(redirectedTarget, property, value, redirectedReceiver)
|
return Reflect.set(redirectedTarget, property, value, redirectedTarget)
|
||||||
},
|
},
|
||||||
getPrototypeOf() {
|
getPrototypeOf() {
|
||||||
return Reflect.getPrototypeOf(backingWidget)
|
return Reflect.getPrototypeOf(backingWidget)
|
||||||
|
|||||||
@@ -284,6 +284,7 @@ export interface IBaseWidget<
|
|||||||
/** Widget type (see {@link TWidgetType}) */
|
/** Widget type (see {@link TWidgetType}) */
|
||||||
type: TType
|
type: TType
|
||||||
value?: TValue
|
value?: TValue
|
||||||
|
vueTrack?: () => void
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether the widget value should be serialized on node serialization.
|
* Whether the widget value should be serialized on node serialization.
|
||||||
|
|||||||
Reference in New Issue
Block a user