Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Migrate vue widget data to use refs
  • Loading branch information
AustinMroz committed Dec 13, 2025
commit 47650bebf82e40f98896807d7b6514ecbd9a1468
158 changes: 21 additions & 137 deletions src/composables/graph/useGraphNodeManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export interface WidgetSlotMetadata {
export interface SafeWidgetData {
name: string
type: string
value: WidgetValue
value: () => Ref<WidgetValue>
borderStyle?: string
callback?: ((value: unknown) => void) | undefined
controlWidget?: () => Ref<ControlOptions>
Expand Down Expand Up @@ -109,18 +109,30 @@ export function safeWidgetMapper(
const nodeDefStore = useNodeDefStore()
return function (widget) {
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.value === undefined &&
widget.type === 'combo' &&
widget.options?.values &&
Array.isArray(widget.options.values) &&
widget.options.values.length > 0
) {
value = widget.options.values[0]
widget.value = widget.options.values[0]
}
//@ts-expect-error duck violence
if (!widget.valueRef) {
const valueRef = ref(widget.value)
watch(valueRef, (newValue) => {
widget.value = newValue
widget.callback?.(newValue)
})
widget.callback = useChainCallback(widget.callback, () => {
if (valueRef.value !== widget.value)
//@ts-expect-error duck violence
valueRef.value = validateWidgetValue(widget.value)
})
//@ts-expect-error duck violence
widget.valueRef = () => valueRef
}
const spec = nodeDefStore.getInputSpecForWidget(node, widget.name)
const slotInfo = slotMetadata.get(widget.name)
Expand All @@ -133,9 +145,9 @@ export function safeWidgetMapper(
return {
name: widget.name,
type: widget.type,
value: value,
//@ts-expect-error duck violence
value: widget.valueRef,
borderStyle,
callback: widget.callback,
isDOMWidget: isDOMWidget(widget),
label: widget.label,
options: widget.options,
Expand All @@ -147,7 +159,7 @@ export function safeWidgetMapper(
return {
name: widget.name || 'unknown',
type: widget.type || 'text',
value: undefined
value: () => ref()
}
}
}
Expand Down Expand Up @@ -271,128 +283,6 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager {
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: { value?: unknown; name: string }, // 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

// 2. Call the original callback if it exists
if (originalCallback) {
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 = () => {
if (!graph?._nodes) return

Expand All @@ -413,9 +303,6 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager {
// Store non-reactive reference
nodeRefs.set(id, node)

// Set up widget callbacks BEFORE extracting data (critical order)
setupNodeWidgetCallbacks(node)

// Extract and store safe data for Vue
vueNodeData.set(id, extractVueNodeData(node))
})
Expand All @@ -434,9 +321,6 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager {
// Store non-reactive reference to original 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)
vueNodeData.set(id, extractVueNodeData(node))

Expand Down
19 changes: 2 additions & 17 deletions src/renderer/extensions/vueNodes/components/NodeWidgets.vue
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,6 @@
:node-id="nodeData?.id != null ? String(nodeData.id) : ''"
:node-type="nodeType"
class="col-span-2"
@update:model-value="widget.updateHandler"
/>
</div>
</template>
Expand All @@ -69,7 +68,7 @@
<script setup lang="ts">
import type { TooltipOptions } from 'primevue'
import { computed, onErrorCaptured, ref, toValue } from 'vue'
import type { Component } from 'vue'
import type { Component, Ref } from 'vue'

import type {
VueNodeData,
Expand Down Expand Up @@ -136,8 +135,7 @@ interface ProcessedWidget {
type: string
vueComponent: Component
simplified: SimplifiedWidget
value: WidgetValue
updateHandler: (value: WidgetValue) => void
value: () => Ref<WidgetValue>
tooltipConfig: TooltipOptions
slotMetadata?: WidgetSlotMetadata
}
Expand Down Expand Up @@ -170,23 +168,11 @@ const processedWidgets = computed((): ProcessedWidget[] => {
value: widget.value,
label: widget.label,
options: widgetOptions,
callback: widget.callback,
spec: widget.spec,
borderStyle: widget.borderStyle,
controlWidget: widget.controlWidget
}

function updateHandler(value: WidgetValue) {
// Update the widget value directly
widget.value = value

// Skip callback for asset widgets - their callback opens the modal,
// but Vue asset mode handles selection through the dropdown
if (widget.type !== 'asset') {
widget.callback?.(value)
}
}

const tooltipText = getWidgetTooltip(widget)
const tooltipConfig = createTooltipConfig(tooltipText)

Expand All @@ -196,7 +182,6 @@ const processedWidgets = computed((): ProcessedWidget[] => {
vueComponent,
simplified,
value: widget.value,
updateHandler,
tooltipConfig,
slotMetadata
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,7 @@ const props = defineProps<{
nodeId: string
}>()

const modelValue = defineModel<string>('modelValue')

defineEmits<{
'update:modelValue': [value: string]
}>()
const modelValue = props.widget.value()

// Get litegraph node
const litegraphNode = computed(() => {
Expand All @@ -50,7 +46,7 @@ const isOutputNodeRef = computed(() => {
return isOutputNode(node)
})

const audioFilePath = computed(() => props.widget.value as string)
const audioFilePath = props.widget.value()

// Computed audio URL from widget value (for input files)
const audioUrlFromWidget = computed(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import Button from 'primevue/button'
import type { ButtonProps } from 'primevue/button'
import PrimeVue from 'primevue/config'
import { describe, expect, it, vi } from 'vitest'
import { ref, watch } from 'vue'

import WidgetButton from '@/renderer/extensions/vueNodes/widgets/components/WidgetButton.vue'
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
Expand All @@ -12,13 +13,16 @@ describe('WidgetButton Interactions', () => {
options: Partial<ButtonProps> = {},
callback?: () => void,
name: string = 'test_button'
): SimplifiedWidget<void> => ({
name,
type: 'button',
value: undefined,
options,
callback
})
): SimplifiedWidget<void> => {
const valueRef = ref()
if (callback) watch(valueRef, callback)
return {
name,
type: 'button',
value: () => valueRef,
options
}
}

const mountComponent = (widget: SimplifiedWidget<void>, readonly = false) => {
return mount(WidgetButton, {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

<script setup lang="ts">
import Button from 'primevue/button'
import { computed } from 'vue'
import { computed, triggerRef } from 'vue'

import type { SimplifiedWidget } from '@/types/simplifiedWidget'
import {
Expand All @@ -37,8 +37,7 @@ const filteredProps = computed(() =>
)

const handleClick = () => {
if (props.widget.callback) {
props.widget.callback()
}
//FIXME: Will do nothing since backing value is unchanged
triggerRef(props.widget.value())
}
</script>
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,12 @@ import type { SimplifiedWidget } from '@/types/simplifiedWidget'

type ChartWidgetOptions = NonNullable<ChartInputSpec['options']>

const value = defineModel<ChartData>({ required: true })

const props = defineProps<{
widget: SimplifiedWidget<ChartData, ChartWidgetOptions>
}>()

const value = props.widget.value()

const chartType = computed(() => props.widget.options?.type ?? 'line')

const chartData = computed(() => value.value || { labels: [], datasets: [] })
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import ColorPicker from 'primevue/colorpicker'
import type { ColorPickerProps } from 'primevue/colorpicker'
import PrimeVue from 'primevue/config'
import { describe, expect, it } from 'vitest'
import { ref, watch } from 'vue'

import type { SimplifiedWidget } from '@/types/simplifiedWidget'

Expand All @@ -14,13 +15,16 @@ describe('WidgetColorPicker Value Binding', () => {
value: string = '#000000',
options: Partial<ColorPickerProps> = {},
callback?: (value: string) => void
): SimplifiedWidget<string> => ({
name: 'test_color_picker',
type: 'color',
value,
options,
callback
})
): SimplifiedWidget<string> => {
const valueRef = ref(value)
if (callback) watch(valueRef, callback)
return {
name: 'test_color_picker',
type: 'color',
value: () => valueRef,
options
}
}

const mountComponent = (
widget: SimplifiedWidget<string>,
Expand Down
Loading