Drag vuenodes input (#6514)

This pull request introduces several improvements to Vue reactivity and
user experience in the graph node and widget system. The main focus is
on ensuring that changes to node and widget data reliably trigger
updates in Vue components, improving drag-and-drop support for nodes,
and enhancing widget value handling for better compatibility and
reactivity.

**Vue Reactivity Improvements:**

* In `useGraphNodeManager.ts`, node data updates now create a completely
new object and add a timestamp (`_updateTs`) to force Vue's reactivity
system to detect changes. Additionally, node data is re-set on the next
tick to guarantee component updates.
[[1]](diffhunk://#diff-f980db6f42cef913c3fe92669783b255d617e40b9ccef9a1ab9cc8e326ff1790L272-R280)
[[2]](diffhunk://#diff-f980db6f42cef913c3fe92669783b255d617e40b9ccef9a1ab9cc8e326ff1790R326-R335)
* Widget value composables (`useWidgetValue` and related helpers) now
accept either a direct value or a getter function for `modelValue`, and
always normalize it to a getter. Watches are updated to use this getter
for more reliable reactivity.
[[1]](diffhunk://#diff-92dc3c8b09ab57105e400e115196aae645214f305685044f62edc3338afa0911L13-R14)
[[2]](diffhunk://#diff-92dc3c8b09ab57105e400e115196aae645214f305685044f62edc3338afa0911R49-R57)
[[3]](diffhunk://#diff-92dc3c8b09ab57105e400e115196aae645214f305685044f62edc3338afa0911L82-R91)
[[4]](diffhunk://#diff-92dc3c8b09ab57105e400e115196aae645214f305685044f62edc3338afa0911L100-R104)
[[5]](diffhunk://#diff-92dc3c8b09ab57105e400e115196aae645214f305685044f62edc3338afa0911L117-R121)
[[6]](diffhunk://#diff-92dc3c8b09ab57105e400e115196aae645214f305685044f62edc3338afa0911L140-R144)
[[7]](diffhunk://#diff-0c43cefa9fb524ae86541c7ca851e97a22b3fd01f95795c83273c977be77468fL47-R47)
* In `useImageUploadWidget.ts`, widget value updates now use a new
array/object to ensure Vue detects the change, especially for batch
uploads.

**Drag-and-Drop Support for Nodes:**

* The `LGraphNode.vue` component adds drag-and-drop event handlers
(`dragover`, `dragleave`, `drop`) and visual feedback (`isDraggingOver`
state and highlight ring) for improved user experience when dragging
files onto nodes. Node callbacks (`onDragOver`, `onDragDrop`) are used
for custom validation and handling.
[[1]](diffhunk://#diff-a7744614cf842e54416047326db79ad81f7c7ab7bfb66ae2b46f5c73ac7d47f2L26-R27)
[[2]](diffhunk://#diff-a7744614cf842e54416047326db79ad81f7c7ab7bfb66ae2b46f5c73ac7d47f2R47-R49)
[[3]](diffhunk://#diff-a7744614cf842e54416047326db79ad81f7c7ab7bfb66ae2b46f5c73ac7d47f2R482-R521)

**Widget and Audio Upload Handling:**

* In `uploadAudio.ts`, after uploading an audio file, the widget's
callback is manually triggered to ensure Vue nodes update. There is also
a commented-out call to mark the canvas as dirty for potential future
refresh logic.
[[1]](diffhunk://#diff-796b36f2cafb906a5e95b5750ca5ddc1bf57a304d4a022e0bdaee04b4ee5bbc4R61-R65)
[[2]](diffhunk://#diff-796b36f2cafb906a5e95b5750ca5ddc1bf57a304d4a022e0bdaee04b4ee5bbc4R190-R191)

These changes collectively improve the reliability and responsiveness of
UI updates in the graph node system, especially in scenarios involving
external updates, drag-and-drop interactions, and batch widget value
changes.



https://github.com/user-attachments/assets/8e3194c9-196c-4e13-ad0b-a32177f2d062



┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6514-Drag-vuenodes-input-29e6d73d3650817da1b7ef96b61b752d)
by [Unito](https://www.unito.io)
This commit is contained in:
Johnpaul Chiwetelu
2025-11-05 09:11:56 +01:00
committed by GitHub
parent 693fbbd3e4
commit fac86e35bf
6 changed files with 85 additions and 26 deletions

View File

@@ -269,10 +269,13 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager {
const updatedWidgets = currentData.widgets.map((w) =>
w.name === widgetName ? { ...w, value: validateWidgetValue(value) } : w
)
vueNodeData.set(nodeId, {
// 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
}

View File

@@ -2,16 +2,17 @@
* Composable for managing widget value synchronization between Vue and LiteGraph
* Provides consistent pattern for immediate UI updates and LiteGraph callbacks
*/
import { ref, watch } from 'vue'
import { computed, toValue, ref, watch } from 'vue'
import type { Ref } from 'vue'
import type { SimplifiedWidget, WidgetValue } from '@/types/simplifiedWidget'
import type { MaybeRefOrGetter } from '@vueuse/core'
interface UseWidgetValueOptions<T extends WidgetValue = WidgetValue, U = T> {
/** The widget configuration from LiteGraph */
widget: SimplifiedWidget<T>
/** The current value from parent component */
modelValue: T
/** The current value from parent component (can be a value or a getter function) */
modelValue: MaybeRefOrGetter<T>
/** Default value if modelValue is null/undefined */
defaultValue: T
/** Emit function from component setup */
@@ -46,8 +47,21 @@ export function useWidgetValue<T extends WidgetValue = WidgetValue, U = T>({
emit,
transform
}: UseWidgetValueOptions<T, U>): UseWidgetValueReturn<T, U> {
// Local value for immediate UI updates
const localValue = ref<T>(modelValue ?? defaultValue)
// Ref for immediate UI feedback before value flows back through modelValue
const newProcessedValue = ref<T | null>(null)
// Computed that prefers the immediately processed value, then falls back to modelValue
const localValue = computed<T>(
() => newProcessedValue.value ?? toValue(modelValue) ?? defaultValue
)
// Clear newProcessedValue when modelValue updates (allowing external changes to flow through)
watch(
() => toValue(modelValue),
() => {
newProcessedValue.value = null
}
)
// Handle user changes
const onChange = (newValue: U) => {
@@ -71,21 +85,13 @@ export function useWidgetValue<T extends WidgetValue = WidgetValue, U = T>({
}
}
// 1. Update local state for immediate UI feedback
localValue.value = processedValue
// Set for immediate UI feedback
newProcessedValue.value = processedValue
// 2. Emit to parent component
// Emit to parent component
emit('update:modelValue', processedValue)
}
// Watch for external updates from LiteGraph
watch(
() => modelValue,
(newValue) => {
localValue.value = newValue ?? defaultValue
}
)
return {
localValue: localValue as Ref<T>,
onChange
@@ -97,7 +103,7 @@ export function useWidgetValue<T extends WidgetValue = WidgetValue, U = T>({
*/
export function useStringWidgetValue(
widget: SimplifiedWidget<string>,
modelValue: string,
modelValue: string | (() => string),
emit: (event: 'update:modelValue', value: string) => void
) {
return useWidgetValue({
@@ -114,7 +120,7 @@ export function useStringWidgetValue(
*/
export function useNumberWidgetValue(
widget: SimplifiedWidget<number>,
modelValue: number,
modelValue: number | (() => number),
emit: (event: 'update:modelValue', value: number) => void
) {
return useWidgetValue({
@@ -137,7 +143,7 @@ export function useNumberWidgetValue(
*/
export function useBooleanWidgetValue(
widget: SimplifiedWidget<boolean>,
modelValue: boolean,
modelValue: boolean | (() => boolean),
emit: (event: 'update:modelValue', value: boolean) => void
) {
return useWidgetValue({

View File

@@ -58,6 +58,9 @@ async function uploadFile(
getResourceURL(...splitFilePath(path))
)
audioWidget.value = path
// Manually trigger the callback to update VueNodes
audioWidget.callback?.(path)
}
} else {
useToastStore().addAlert(resp.status + ' - ' + resp.statusText)

View File

@@ -23,7 +23,8 @@
bypassed,
'before:rounded-2xl before:pointer-events-none before:absolute before:inset-0':
muted,
'will-change-transform': isDragging
'will-change-transform': isDragging,
'ring-4 ring-primary-500 bg-primary-500/10': isDraggingOver
},
shouldHandleNodePointerEvents
@@ -43,6 +44,9 @@
v-bind="pointerHandlers"
@wheel="handleWheel"
@contextmenu="handleContextMenu"
@dragover.prevent="handleDragOver"
@dragleave="handleDragLeave"
@drop="handleDrop"
>
<div class="flex flex-col justify-center items-center relative">
<template v-if="isCollapsed">
@@ -132,7 +136,7 @@
</template>
<script setup lang="ts">
import { whenever } from '@vueuse/core'
import { useMouseInElement, whenever } from '@vueuse/core'
import { storeToRefs } from 'pinia'
import { computed, inject, onErrorCaptured, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
@@ -481,4 +485,43 @@ const nodeMedia = computed(() => {
})
const nodeContainerRef = ref<HTMLDivElement>()
// Track mouse position relative to node container for drag and drop
const { isOutside } = useMouseInElement(nodeContainerRef)
// Drag and drop support
const isDraggingOver = ref(false)
const handleDragOver = (event: DragEvent) => {
const node = lgraphNode.value
if (!node || !node.onDragOver) {
isDraggingOver.value = false
return
}
// Call the litegraph node's onDragOver callback to check if files are valid
const canDrop = node.onDragOver(event)
isDraggingOver.value = canDrop
}
const handleDragLeave = () => {
if (isOutside.value) {
isDraggingOver.value = false
}
}
const handleDrop = async (event: DragEvent) => {
event.preventDefault()
event.stopPropagation()
isDraggingOver.value = false
const node = lgraphNode.value
if (!node || !node.onDragDrop) {
return
}
// Forward the drop event to the litegraph node's onDragDrop callback
await node.onDragDrop(event)
}
</script>

View File

@@ -44,7 +44,7 @@ const emit = defineEmits<{
const { localValue, onChange } = useWidgetValue({
widget: props.widget,
modelValue: props.modelValue,
modelValue: () => props.modelValue,
defaultValue: props.widget.options?.values?.[0] || '',
emit
})

View File

@@ -78,9 +78,13 @@ export const useImageUploadWidget = () => {
folder,
onUploadComplete: (output) => {
output.forEach((path) => addToComboValues(fileComboWidget, path))
// Create a NEW array to ensure Vue reactivity detects the change
const newValue = allow_batch ? [...output] : output[0]
// @ts-expect-error litegraph combo value type does not support arrays yet
fileComboWidget.value = output
fileComboWidget.callback?.(output)
fileComboWidget.value = newValue
fileComboWidget.callback?.(newValue)
}
})