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

@@ -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>
@@ -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)
@@ -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,
@@ -147,7 +159,7 @@ export function safeWidgetMapper(
return {
name: widget.name || 'unknown',
type: widget.type || 'text',
value: undefined
value: () => ref()
}
}
}
@@ -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
@@ -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))
})
@@ -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))

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)

View File

@@ -45,7 +45,7 @@ export interface SimplifiedWidget<
type: string
/** Current value of the widget */
value: T
value: () => Ref<T>
borderStyle?: string

View File

@@ -3,7 +3,7 @@ import { storeToRefs } from 'pinia'
import Button from 'primevue/button'
import Splitter from 'primevue/splitter'
import SplitterPanel from 'primevue/splitterpanel'
import { computed } from 'vue'
import { computed, ref } from 'vue'
import ExtensionSlot from '@/components/common/ExtensionSlot.vue'
import CurrentUserButton from '@/components/topbar/CurrentUserButton.vue'
@@ -64,7 +64,7 @@ const isDesktop = isElectron()
const batchCountWidget = {
options: { step2: 1, precision: 1, min: 1, max: 100 },
value: 1,
value: () => ref(1),
name: t('Number of generations'),
type: 'number'
}

View File

@@ -1,6 +1,7 @@
import { createTestingPinia } from '@pinia/testing'
import { mount } from '@vue/test-utils'
import { describe, expect, it } from 'vitest'
import { ref } from 'vue'
import type {
SafeWidgetData,
@@ -15,11 +16,10 @@ describe('NodeWidgets', () => {
): SafeWidgetData => ({
name: 'test_widget',
type: 'combo',
value: 'test_value',
value: () => ref('test_value'),
options: {
values: ['option1', 'option2']
},
callback: undefined,
spec: undefined,
label: undefined,
isDOMWidget: false,

View File

@@ -4,6 +4,7 @@ import PrimeVue from 'primevue/config'
import Select from 'primevue/select'
import type { SelectProps } from 'primevue/select'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { ref, watch } from 'vue'
import type { ComboInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
@@ -51,17 +52,20 @@ describe('WidgetSelect Value Binding', () => {
> = {},
callback?: (value: string | undefined) => void,
spec?: ComboInputSpec
): SimplifiedWidget<string | undefined> => ({
name: 'test_select',
type: 'combo',
value,
options: {
values: ['option1', 'option2', 'option3'],
...options
},
callback,
spec
})
): SimplifiedWidget<string | undefined> => {
const valueRef = ref(value)
if (callback) watch(valueRef, callback)
return {
name: 'test_select',
type: 'combo',
value: () => valueRef,
options: {
values: ['option1', 'option2', 'option3'],
...options
},
spec
}
}
const mountComponent = (
widget: SimplifiedWidget<string | undefined>,

View File

@@ -2,6 +2,7 @@ import { createTestingPinia } from '@pinia/testing'
import { flushPromises, mount } from '@vue/test-utils'
import PrimeVue from 'primevue/config'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { ref } from 'vue'
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
@@ -34,7 +35,7 @@ describe('WidgetSelect asset mode', () => {
const createWidget = (): SimplifiedWidget<string | undefined> => ({
name: 'ckpt_name',
type: 'combo',
value: undefined,
value: () => ref(),
options: {
values: []
}

View File

@@ -3,6 +3,7 @@ import { mount } from '@vue/test-utils'
import type { VueWrapper } from '@vue/test-utils'
import PrimeVue from 'primevue/config'
import type { ComponentPublicInstance } from 'vue'
import { ref } from 'vue'
import { describe, expect, it, vi } from 'vitest'
import type { ComboInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
@@ -28,7 +29,7 @@ describe('WidgetSelectDropdown custom label mapping', () => {
): SimplifiedWidget<string | undefined> => ({
name: 'test_image_select',
type: 'combo',
value,
value: () => ref(value),
options: {
values: ['img_001.png', 'photo_abc.jpg', 'hash789.png'],
...options