diff --git a/src/composables/graph/useGraphNodeManager.ts b/src/composables/graph/useGraphNodeManager.ts index c134f53bd9..c99d768416 100644 --- a/src/composables/graph/useGraphNodeManager.ts +++ b/src/composables/graph/useGraphNodeManager.ts @@ -42,7 +42,7 @@ export interface WidgetSlotMetadata { export interface SafeWidgetData { name: string type: string - value: WidgetValue + value: () => Ref borderStyle?: string callback?: ((value: unknown) => void) | undefined controlWidget?: () => Ref @@ -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)) diff --git a/src/renderer/extensions/vueNodes/components/NodeWidgets.vue b/src/renderer/extensions/vueNodes/components/NodeWidgets.vue index 35057df965..912fc2cc01 100644 --- a/src/renderer/extensions/vueNodes/components/NodeWidgets.vue +++ b/src/renderer/extensions/vueNodes/components/NodeWidgets.vue @@ -59,7 +59,6 @@ :node-id="nodeData?.id != null ? String(nodeData.id) : ''" :node-type="nodeType" class="col-span-2" - @update:model-value="widget.updateHandler" /> @@ -69,7 +68,7 @@ diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetChart.vue b/src/renderer/extensions/vueNodes/widgets/components/WidgetChart.vue index cadd4754bf..46df05778b 100644 --- a/src/renderer/extensions/vueNodes/widgets/components/WidgetChart.vue +++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetChart.vue @@ -21,12 +21,12 @@ import type { SimplifiedWidget } from '@/types/simplifiedWidget' type ChartWidgetOptions = NonNullable -const value = defineModel({ required: true }) - const props = defineProps<{ widget: SimplifiedWidget }>() +const value = props.widget.value() + const chartType = computed(() => props.widget.options?.type ?? 'line') const chartData = computed(() => value.value || { labels: [], datasets: [] }) diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetColorPicker.test.ts b/src/renderer/extensions/vueNodes/widgets/components/WidgetColorPicker.test.ts index 32a937f89a..dacc21f082 100644 --- a/src/renderer/extensions/vueNodes/widgets/components/WidgetColorPicker.test.ts +++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetColorPicker.test.ts @@ -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 = {}, callback?: (value: string) => void - ): SimplifiedWidget => ({ - name: 'test_color_picker', - type: 'color', - value, - options, - callback - }) + ): SimplifiedWidget => { + const valueRef = ref(value) + if (callback) watch(valueRef, callback) + return { + name: 'test_color_picker', + type: 'color', + value: () => valueRef, + options + } + } const mountComponent = ( widget: SimplifiedWidget, diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetGalleria.test.ts b/src/renderer/extensions/vueNodes/widgets/components/WidgetGalleria.test.ts index 399edb5144..fcdbf214ea 100644 --- a/src/renderer/extensions/vueNodes/widgets/components/WidgetGalleria.test.ts +++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetGalleria.test.ts @@ -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 } } diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetGalleria.vue b/src/renderer/extensions/vueNodes/widgets/components/WidgetGalleria.vue index e32acb7817..5408728f34 100644 --- a/src/renderer/extensions/vueNodes/widgets/components/WidgetGalleria.vue +++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetGalleria.vue @@ -68,12 +68,12 @@ export interface GalleryImage { export type GalleryValue = string[] | GalleryImage[] -const value = defineModel({ required: true }) - const props = defineProps<{ widget: SimplifiedWidget }>() +const value = props.widget.value() + const activeIndex = ref(0) const { t } = useI18n() diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetImageCompare.test.ts b/src/renderer/extensions/vueNodes/widgets/components/WidgetImageCompare.test.ts index 847ffc6574..0addbe09ff 100644 --- a/src/renderer/extensions/vueNodes/widgets/components/WidgetImageCompare.test.ts +++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetImageCompare.test.ts @@ -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 => ({ name: 'test_imagecompare', type: 'object', - value, + value: () => ref(value), options }) diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetImageCompare.vue b/src/renderer/extensions/vueNodes/widgets/components/WidgetImageCompare.vue index 34516a120e..15205ffbad 100644 --- a/src/renderer/extensions/vueNodes/widgets/components/WidgetImageCompare.vue +++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetImageCompare.vue @@ -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' diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetInputNumber.vue b/src/renderer/extensions/vueNodes/widgets/components/WidgetInputNumber.vue index 06b1c14645..57a4b00c57 100644 --- a/src/renderer/extensions/vueNodes/widgets/components/WidgetInputNumber.vue +++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetInputNumber.vue @@ -14,7 +14,7 @@ const props = defineProps<{ widget: SimplifiedWidget }>() -const modelValue = defineModel({ default: 0 }) +const modelValue = props.widget.value() const hasControlAfterGenerate = computed(() => { return !!props.widget.controlWidget diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetInputNumberInput.test.ts b/src/renderer/extensions/vueNodes/widgets/components/WidgetInputNumberInput.test.ts index 6fbadb5172..a6c343d5a6 100644 --- a/src/renderer/extensions/vueNodes/widgets/components/WidgetInputNumberInput.test.ts +++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetInputNumberInput.test.ts @@ -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 { + const valueRef = ref(value) + if (callback) watch(valueRef, callback) return { name: 'test_input_number', type, - value, - options, - callback + value: () => valueRef, + options } } diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetInputNumberInput.vue b/src/renderer/extensions/vueNodes/widgets/components/WidgetInputNumberInput.vue index 2bd16cb3b1..6a00817ddb 100644 --- a/src/renderer/extensions/vueNodes/widgets/components/WidgetInputNumberInput.vue +++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetInputNumberInput.vue @@ -16,7 +16,7 @@ const props = defineProps<{ widget: SimplifiedWidget }>() -const modelValue = defineModel({ default: 0 }) +const modelValue = props.widget.value() const filteredProps = computed(() => filterWidgetProps(props.widget.options, INPUT_EXCLUDED_PROPS) diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetInputNumberSlider.test.ts b/src/renderer/extensions/vueNodes/widgets/components/WidgetInputNumberSlider.test.ts index 324ecda61a..8fbf563aab 100644 --- a/src/renderer/extensions/vueNodes/widgets/components/WidgetInputNumberSlider.test.ts +++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetInputNumberSlider.test.ts @@ -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 { + 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 } } } diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetInputNumberSlider.vue b/src/renderer/extensions/vueNodes/widgets/components/WidgetInputNumberSlider.vue index a7f1463552..f0c51b9f56 100644 --- a/src/renderer/extensions/vueNodes/widgets/components/WidgetInputNumberSlider.vue +++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetInputNumberSlider.vue @@ -48,7 +48,7 @@ const { widget } = defineProps<{ widget: SimplifiedWidget }>() -const modelValue = defineModel({ default: 0 }) +const modelValue = widget.value() const timesEmptied = ref(0) diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetInputText.test.ts b/src/renderer/extensions/vueNodes/widgets/components/WidgetInputText.test.ts index 599f85306f..1be2ceb684 100644 --- a/src/renderer/extensions/vueNodes/widgets/components/WidgetInputText.test.ts +++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetInputText.test.ts @@ -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 = {}, callback?: (value: string) => void - ): SimplifiedWidget => ({ - name: 'test_input', - type: 'string', - value, - options, - callback - }) + ): SimplifiedWidget => { + const valueRef = ref(value) + if (callback) watch(valueRef, callback) + return { + name: 'test_input', + type: 'string', + value: () => valueRef, + options + } + } const mountComponent = ( widget: SimplifiedWidget, diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetInputText.vue b/src/renderer/extensions/vueNodes/widgets/components/WidgetInputText.vue index eff22f1efc..b7ad6561cf 100644 --- a/src/renderer/extensions/vueNodes/widgets/components/WidgetInputText.vue +++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetInputText.vue @@ -29,7 +29,7 @@ const props = defineProps<{ widget: SimplifiedWidget }>() -const modelValue = defineModel({ default: '' }) +const modelValue = props.widget.value() const filteredProps = computed(() => filterWidgetProps(props.widget.options, INPUT_EXCLUDED_PROPS) diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetMarkdown.test.ts b/src/renderer/extensions/vueNodes/widgets/components/WidgetMarkdown.test.ts index ee788cc4db..f3e6c9c533 100644 --- a/src/renderer/extensions/vueNodes/widgets/components/WidgetMarkdown.test.ts +++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetMarkdown.test.ts @@ -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 = {}, callback?: (value: string) => void - ): SimplifiedWidget => ({ - name: 'test_markdown', - type: 'string', - value, - options, - callback - }) + ): SimplifiedWidget => { + const valueRef = ref(value) + if (callback) watch(valueRef, callback) + return { + name: 'test_markdown', + type: 'string', + value: () => valueRef, + options + } + } const mountComponent = ( widget: SimplifiedWidget, diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetMarkdown.vue b/src/renderer/extensions/vueNodes/widgets/components/WidgetMarkdown.vue index 28247859ed..2cf2fcd4f0 100644 --- a/src/renderer/extensions/vueNodes/widgets/components/WidgetMarkdown.vue +++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetMarkdown.vue @@ -38,7 +38,7 @@ const { widget } = defineProps<{ widget: SimplifiedWidget }>() -const modelValue = defineModel({ default: '' }) +const modelValue = widget.value() // State const isEditing = ref(false) diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetRecordAudio.vue b/src/renderer/extensions/vueNodes/widgets/components/WidgetRecordAudio.vue index aa0fc4df32..5c106c10cd 100644 --- a/src/renderer/extensions/vueNodes/widgets/components/WidgetRecordAudio.vue +++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetRecordAudio.vue @@ -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 + 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({ default: '' }) +const modelValue = props.widget.value() const litegraphNode = computed(() => { if (!props.nodeId || !app.canvas.graph) return null diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetSelect.vue b/src/renderer/extensions/vueNodes/widgets/components/WidgetSelect.vue index 7cf365076f..3883fcd0d8 100644 --- a/src/renderer/extensions/vueNodes/widgets/components/WidgetSelect.vue +++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetSelect.vue @@ -32,7 +32,7 @@ const props = defineProps<{ nodeType?: string }>() -const modelValue = defineModel() +const modelValue = props.widget.value() const comboSpec = computed(() => { if (props.widget.spec && isComboInputSpec(props.widget.spec)) { diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetSelectDefault.vue b/src/renderer/extensions/vueNodes/widgets/components/WidgetSelectDefault.vue index f029f0fcd7..e0b5d1a013 100644 --- a/src/renderer/extensions/vueNodes/widgets/components/WidgetSelectDefault.vue +++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetSelectDefault.vue @@ -42,11 +42,7 @@ interface Props { const props = defineProps() -const modelValue = defineModel({ - default(props: Props) { - return props.widget.options?.values?.[0] || '' - } -}) +const modelValue = props.widget.value() // Transform compatibility props for overlay positioning const transformCompatProps = useTransformCompatOverlayProps() diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetSelectDropdown.vue b/src/renderer/extensions/vueNodes/widgets/components/WidgetSelectDropdown.vue index 78702bafa5..8183ddfc1c 100644 --- a/src/renderer/extensions/vueNodes/widgets/components/WidgetSelectDropdown.vue +++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetSelectDropdown.vue @@ -43,11 +43,7 @@ provide( computed(() => props.assetKind) ) -const modelValue = defineModel({ - 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}`) diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetTextarea.test.ts b/src/renderer/extensions/vueNodes/widgets/components/WidgetTextarea.test.ts index e15f329449..f89932cd60 100644 --- a/src/renderer/extensions/vueNodes/widgets/components/WidgetTextarea.test.ts +++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetTextarea.test.ts @@ -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 { + const valueRef = ref(value) + if (callback) watch(valueRef, callback) return { name: 'test_textarea', type: 'string', - value, - options, - callback + value: () => valueRef, + options } } diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetTextarea.vue b/src/renderer/extensions/vueNodes/widgets/components/WidgetTextarea.vue index 25876f5395..b63422f0df 100644 --- a/src/renderer/extensions/vueNodes/widgets/components/WidgetTextarea.vue +++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetTextarea.vue @@ -46,7 +46,7 @@ const { widget, placeholder = '' } = defineProps<{ placeholder?: string }>() -const modelValue = defineModel({ default: '' }) +const modelValue = widget.value() const filteredProps = computed(() => filterWidgetProps(widget.options, INPUT_EXCLUDED_PROPS) diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetToggleSwitch.test.ts b/src/renderer/extensions/vueNodes/widgets/components/WidgetToggleSwitch.test.ts index 7e7a876484..bb702f312d 100644 --- a/src/renderer/extensions/vueNodes/widgets/components/WidgetToggleSwitch.test.ts +++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetToggleSwitch.test.ts @@ -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 = {}, callback?: (value: boolean) => void - ): SimplifiedWidget => ({ - name: 'test_toggle', - type: 'boolean', - value, - options, - callback - }) + ): SimplifiedWidget => { + const valueRef = ref(value) + if (callback) watch(valueRef, callback) + return { + name: 'test_toggle', + type: 'boolean', + value: () => valueRef, + options + } + } const mountComponent = ( widget: SimplifiedWidget, diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetToggleSwitch.vue b/src/renderer/extensions/vueNodes/widgets/components/WidgetToggleSwitch.vue index b902b6655e..f5c00ff32a 100644 --- a/src/renderer/extensions/vueNodes/widgets/components/WidgetToggleSwitch.vue +++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetToggleSwitch.vue @@ -25,7 +25,7 @@ const { widget } = defineProps<{ widget: SimplifiedWidget }>() -const modelValue = defineModel() +const modelValue = widget.value() const filteredProps = computed(() => filterWidgetProps(widget.options, STANDARD_EXCLUDED_PROPS) diff --git a/src/types/simplifiedWidget.ts b/src/types/simplifiedWidget.ts index f96eaa8590..c979a47e13 100644 --- a/src/types/simplifiedWidget.ts +++ b/src/types/simplifiedWidget.ts @@ -45,7 +45,7 @@ export interface SimplifiedWidget< type: string /** Current value of the widget */ - value: T + value: () => Ref borderStyle?: string diff --git a/src/views/LinearView.vue b/src/views/LinearView.vue index 1a02d37dc1..2585b47632 100644 --- a/src/views/LinearView.vue +++ b/src/views/LinearView.vue @@ -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' } diff --git a/tests-ui/renderer/extensions/vueNodes/components/NodeWidgets.test.ts b/tests-ui/renderer/extensions/vueNodes/components/NodeWidgets.test.ts index 78f6c370a6..ece9889d5e 100644 --- a/tests-ui/renderer/extensions/vueNodes/components/NodeWidgets.test.ts +++ b/tests-ui/renderer/extensions/vueNodes/components/NodeWidgets.test.ts @@ -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, diff --git a/tests-ui/renderer/extensions/vueNodes/widgets/components/WidgetSelect.test.ts b/tests-ui/renderer/extensions/vueNodes/widgets/components/WidgetSelect.test.ts index 4d52d47e26..7358604d74 100644 --- a/tests-ui/renderer/extensions/vueNodes/widgets/components/WidgetSelect.test.ts +++ b/tests-ui/renderer/extensions/vueNodes/widgets/components/WidgetSelect.test.ts @@ -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 => ({ - name: 'test_select', - type: 'combo', - value, - options: { - values: ['option1', 'option2', 'option3'], - ...options - }, - callback, - spec - }) + ): SimplifiedWidget => { + 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, diff --git a/tests-ui/tests/renderer/extensions/vueNodes/widgets/components/WidgetSelect.asset-mode.test.ts b/tests-ui/tests/renderer/extensions/vueNodes/widgets/components/WidgetSelect.asset-mode.test.ts index 82224c7104..a68bd19904 100644 --- a/tests-ui/tests/renderer/extensions/vueNodes/widgets/components/WidgetSelect.asset-mode.test.ts +++ b/tests-ui/tests/renderer/extensions/vueNodes/widgets/components/WidgetSelect.asset-mode.test.ts @@ -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 => ({ name: 'ckpt_name', type: 'combo', - value: undefined, + value: () => ref(), options: { values: [] } diff --git a/tests-ui/tests/renderer/extensions/vueNodes/widgets/components/WidgetSelectDropdown.test.ts b/tests-ui/tests/renderer/extensions/vueNodes/widgets/components/WidgetSelectDropdown.test.ts index 730ceb0bf8..0bc25974b0 100644 --- a/tests-ui/tests/renderer/extensions/vueNodes/widgets/components/WidgetSelectDropdown.test.ts +++ b/tests-ui/tests/renderer/extensions/vueNodes/widgets/components/WidgetSelectDropdown.test.ts @@ -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 => ({ name: 'test_image_select', type: 'combo', - value, + value: () => ref(value), options: { values: ['img_001.png', 'photo_abc.jpg', 'hash789.png'], ...options