mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-26 01:34:07 +00:00
Migrate vue widget data to use refs
This commit is contained in:
@@ -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))
|
||||
|
||||
|
||||
@@ -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
|
||||
})
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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: [] })
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
})
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ const { widget } = defineProps<{
|
||||
widget: SimplifiedWidget<number>
|
||||
}>()
|
||||
|
||||
const modelValue = defineModel<number>({ default: 0 })
|
||||
const modelValue = widget.value()
|
||||
|
||||
const timesEmptied = ref(0)
|
||||
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -38,7 +38,7 @@ const { widget } = defineProps<{
|
||||
widget: SimplifiedWidget<string>
|
||||
}>()
|
||||
|
||||
const modelValue = defineModel<string>({ default: '' })
|
||||
const modelValue = widget.value()
|
||||
|
||||
// State
|
||||
const isEditing = ref(false)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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}`)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -45,7 +45,7 @@ export interface SimplifiedWidget<
|
||||
type: string
|
||||
|
||||
/** Current value of the widget */
|
||||
value: T
|
||||
value: () => Ref<T>
|
||||
|
||||
borderStyle?: string
|
||||
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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: []
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user