Migrate vue widget data to use refs

This commit is contained in:
Austin Mroz
2025-12-01 23:47:59 -08:00
parent 3eece91eb6
commit 47650bebf8
34 changed files with 148 additions and 262 deletions

View File

@@ -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>
@@ -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,
@@ -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
}
@@ -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)
@@ -196,7 +182,6 @@ const processedWidgets = computed((): ProcessedWidget[] => {
vueComponent,
simplified,
value: widget.value,
updateHandler,
tooltipConfig,
slotMetadata
})

View File

@@ -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(() => {
@@ -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(() => {

View File

@@ -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'
@@ -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, {

View File

@@ -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 {
@@ -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>

View File

@@ -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: [] })

View File

@@ -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'
@@ -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>,

View File

@@ -4,6 +4,7 @@ import Galleria from 'primevue/galleria'
import type { GalleriaProps } from 'primevue/galleria'
import { describe, expect, it } from 'vitest'
import { createI18n } from 'vue-i18n'
import { ref } from 'vue'
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
@@ -52,7 +53,7 @@ function createMockWidget(
return {
name: 'test_galleria',
type: 'array',
value,
value: () => ref(value),
options
}
}

View File

@@ -68,12 +68,12 @@ export interface GalleryImage {
export type GalleryValue = string[] | GalleryImage[]
const value = defineModel<GalleryValue>({ required: true })
const props = defineProps<{
widget: SimplifiedWidget<GalleryValue>
}>()
const value = props.widget.value()
const activeIndex = ref(0)
const { t } = useI18n()

View File

@@ -2,6 +2,7 @@ import { mount } from '@vue/test-utils'
import PrimeVue from 'primevue/config'
import ImageCompare from 'primevue/imagecompare'
import { describe, expect, it } from 'vitest'
import { ref } from 'vue'
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
@@ -15,7 +16,7 @@ describe('WidgetImageCompare Display', () => {
): SimplifiedWidget<ImageCompareValue | string> => ({
name: 'test_imagecompare',
type: 'object',
value,
value: () => ref(value),
options
})

View File

@@ -44,24 +44,24 @@ const props = defineProps<{
}>()
const beforeImage = computed(() => {
const value = props.widget.value
const value = props.widget.value().value
return typeof value === 'string' ? value : value?.before || ''
})
const afterImage = computed(() => {
const value = props.widget.value
const value = props.widget.value().value
return typeof value === 'string' ? '' : value?.after || ''
})
const beforeAlt = computed(() => {
const value = props.widget.value
const value = props.widget.value().value
return typeof value === 'object' && value?.beforeAlt
? value.beforeAlt
: 'Before image'
})
const afterAlt = computed(() => {
const value = props.widget.value
const value = props.widget.value().value
return typeof value === 'object' && value?.afterAlt
? value.afterAlt
: 'After image'

View File

@@ -14,7 +14,7 @@ const props = defineProps<{
widget: SimplifiedWidget<number>
}>()
const modelValue = defineModel<number>({ default: 0 })
const modelValue = props.widget.value()
const hasControlAfterGenerate = computed(() => {
return !!props.widget.controlWidget

View File

@@ -2,6 +2,7 @@ import { mount } from '@vue/test-utils'
import PrimeVue from 'primevue/config'
import InputNumber from 'primevue/inputnumber'
import { describe, expect, it } from 'vitest'
import { ref, watch } from 'vue'
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
@@ -13,12 +14,13 @@ function createMockWidget(
options: SimplifiedWidget['options'] = {},
callback?: (value: number) => void
): SimplifiedWidget<number> {
const valueRef = ref(value)
if (callback) watch(valueRef, callback)
return {
name: 'test_input_number',
type,
value,
options,
callback
value: () => valueRef,
options
}
}

View File

@@ -16,7 +16,7 @@ const props = defineProps<{
widget: SimplifiedWidget<number>
}>()
const modelValue = defineModel<number>({ default: 0 })
const modelValue = props.widget.value()
const filteredProps = computed(() =>
filterWidgetProps(props.widget.options, INPUT_EXCLUDED_PROPS)

View File

@@ -2,6 +2,7 @@ import { mount } from '@vue/test-utils'
import PrimeVue from 'primevue/config'
import InputNumber from 'primevue/inputnumber'
import { describe, expect, it } from 'vitest'
import { ref, watch } from 'vue'
import Slider from '@/components/ui/slider/Slider.vue'
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
@@ -13,12 +14,13 @@ function createMockWidget(
options: SimplifiedWidget['options'] = {},
callback?: (value: number) => void
): SimplifiedWidget<number> {
const valueRef = ref(value)
if (callback) watch(valueRef, callback)
return {
name: 'test_slider',
type: 'float',
value,
options: { min: 0, max: 100, step: 1, precision: 0, ...options },
callback
value: () => valueRef,
options: { min: 0, max: 100, step: 1, precision: 0, ...options }
}
}

View File

@@ -48,7 +48,7 @@ const { widget } = defineProps<{
widget: SimplifiedWidget<number>
}>()
const modelValue = defineModel<number>({ default: 0 })
const modelValue = widget.value()
const timesEmptied = ref(0)

View File

@@ -4,6 +4,7 @@ import InputText from 'primevue/inputtext'
import type { InputTextProps } from 'primevue/inputtext'
import Textarea from 'primevue/textarea'
import { describe, expect, it } from 'vitest'
import { ref, watch } from 'vue'
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
@@ -14,13 +15,16 @@ describe('WidgetInputText Value Binding', () => {
value: string = 'default',
options: Partial<InputTextProps> = {},
callback?: (value: string) => void
): SimplifiedWidget<string> => ({
name: 'test_input',
type: 'string',
value,
options,
callback
})
): SimplifiedWidget<string> => {
const valueRef = ref(value)
if (callback) watch(valueRef, callback)
return {
name: 'test_input',
type: 'string',
value: () => valueRef,
options
}
}
const mountComponent = (
widget: SimplifiedWidget<string>,

View File

@@ -29,7 +29,7 @@ const props = defineProps<{
widget: SimplifiedWidget<string>
}>()
const modelValue = defineModel<string>({ default: '' })
const modelValue = props.widget.value()
const filteredProps = computed(() =>
filterWidgetProps(props.widget.options, INPUT_EXCLUDED_PROPS)

View File

@@ -2,7 +2,7 @@ import { mount } from '@vue/test-utils'
import PrimeVue from 'primevue/config'
import Textarea from 'primevue/textarea'
import { describe, expect, it, vi } from 'vitest'
import { nextTick } from 'vue'
import { nextTick, ref, watch } from 'vue'
import { createI18n } from 'vue-i18n'
import enMessages from '@/locales/en/main.json'
@@ -28,13 +28,16 @@ describe('WidgetMarkdown Dual Mode Display', () => {
value: string = '# Default Heading\nSome **bold** text.',
options: Record<string, unknown> = {},
callback?: (value: string) => void
): SimplifiedWidget<string> => ({
name: 'test_markdown',
type: 'string',
value,
options,
callback
})
): SimplifiedWidget<string> => {
const valueRef = ref(value)
if (callback) watch(valueRef, callback)
return {
name: 'test_markdown',
type: 'string',
value: () => valueRef,
options
}
}
const mountComponent = (
widget: SimplifiedWidget<string>,

View File

@@ -38,7 +38,7 @@ const { widget } = defineProps<{
widget: SimplifiedWidget<string>
}>()
const modelValue = defineModel<string>({ default: '' })
const modelValue = widget.value()
// State
const isEditing = ref(false)

View File

@@ -92,6 +92,7 @@ import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
import { useToastStore } from '@/platform/updates/common/toastStore'
import { app } from '@/scripts/app'
import { useAudioService } from '@/services/audioService'
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
import { useAudioPlayback } from '../composables/audio/useAudioPlayback'
import { useAudioRecorder } from '../composables/audio/useAudioRecorder'
@@ -99,8 +100,9 @@ import { useAudioWaveform } from '../composables/audio/useAudioWaveform'
import { formatTime } from '../utils/audioUtils'
const props = defineProps<{
readonly?: boolean
nodeId: string
widget: SimplifiedWidget<string>
readonly?: boolean
}>()
// Audio element ref
@@ -152,7 +154,7 @@ const { isPlaying, audioElementKey } = playback
// Computed for waveform animation
const isWaveformActive = computed(() => isRecording.value || isPlaying.value)
const modelValue = defineModel<string>({ default: '' })
const modelValue = props.widget.value()
const litegraphNode = computed(() => {
if (!props.nodeId || !app.canvas.graph) return null

View File

@@ -32,7 +32,7 @@ const props = defineProps<{
nodeType?: string
}>()
const modelValue = defineModel<string | undefined>()
const modelValue = props.widget.value()
const comboSpec = computed<ComboInputSpec | undefined>(() => {
if (props.widget.spec && isComboInputSpec(props.widget.spec)) {

View File

@@ -42,11 +42,7 @@ interface Props {
const props = defineProps<Props>()
const modelValue = defineModel<string | undefined>({
default(props: Props) {
return props.widget.options?.values?.[0] || ''
}
})
const modelValue = props.widget.value()
// Transform compatibility props for overlay positioning
const transformCompatProps = useTransformCompatOverlayProps()

View File

@@ -43,11 +43,7 @@ provide(
computed(() => props.assetKind)
)
const modelValue = defineModel<string | undefined>({
default(props: Props) {
return props.widget.options?.values?.[0] || ''
}
})
const modelValue = props.widget.value()
const toastStore = useToastStore()
const queueStore = useQueueStore()
@@ -313,11 +309,6 @@ async function handleFilesUpdate(files: File[]) {
// 3. Update widget value to the first uploaded file
modelValue.value = uploadedPaths[0]
// 4. Trigger callback to notify underlying LiteGraph widget
if (props.widget.callback) {
props.widget.callback(uploadedPaths[0])
}
} catch (error) {
console.error('Upload error:', error)
toastStore.addAlert(`Upload failed: ${error}`)

View File

@@ -2,6 +2,7 @@ import { mount } from '@vue/test-utils'
import PrimeVue from 'primevue/config'
import Textarea from 'primevue/textarea'
import { describe, expect, it } from 'vitest'
import { ref, watch } from 'vue'
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
@@ -12,12 +13,13 @@ function createMockWidget(
options: SimplifiedWidget['options'] = {},
callback?: (value: string) => void
): SimplifiedWidget<string> {
const valueRef = ref(value)
if (callback) watch(valueRef, callback)
return {
name: 'test_textarea',
type: 'string',
value,
options,
callback
value: () => valueRef,
options
}
}

View File

@@ -46,7 +46,7 @@ const { widget, placeholder = '' } = defineProps<{
placeholder?: string
}>()
const modelValue = defineModel<string>({ default: '' })
const modelValue = widget.value()
const filteredProps = computed(() =>
filterWidgetProps(widget.options, INPUT_EXCLUDED_PROPS)

View File

@@ -3,6 +3,7 @@ import PrimeVue from 'primevue/config'
import ToggleSwitch from 'primevue/toggleswitch'
import type { ToggleSwitchProps } from 'primevue/toggleswitch'
import { describe, expect, it } from 'vitest'
import { ref, watch } from 'vue'
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
@@ -13,13 +14,16 @@ describe('WidgetToggleSwitch Value Binding', () => {
value: boolean = false,
options: Partial<ToggleSwitchProps> = {},
callback?: (value: boolean) => void
): SimplifiedWidget<boolean> => ({
name: 'test_toggle',
type: 'boolean',
value,
options,
callback
})
): SimplifiedWidget<boolean> => {
const valueRef = ref(value)
if (callback) watch(valueRef, callback)
return {
name: 'test_toggle',
type: 'boolean',
value: () => valueRef,
options
}
}
const mountComponent = (
widget: SimplifiedWidget<boolean>,

View File

@@ -25,7 +25,7 @@ const { widget } = defineProps<{
widget: SimplifiedWidget<boolean>
}>()
const modelValue = defineModel<boolean>()
const modelValue = widget.value()
const filteredProps = computed(() =>
filterWidgetProps(widget.options, STANDARD_EXCLUDED_PROPS)