Skip to content

Commit fac86e3

Browse files
authored
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)
1 parent 693fbbd commit fac86e3

File tree

6 files changed

+85
-26
lines changed

6 files changed

+85
-26
lines changed

src/composables/graph/useGraphNodeManager.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -269,10 +269,13 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager {
269269
const updatedWidgets = currentData.widgets.map((w) =>
270270
w.name === widgetName ? { ...w, value: validateWidgetValue(value) } : w
271271
)
272-
vueNodeData.set(nodeId, {
272+
// Create a completely new object to ensure Vue reactivity triggers
273+
const updatedData = {
273274
...currentData,
274275
widgets: updatedWidgets
275-
})
276+
}
277+
278+
vueNodeData.set(nodeId, updatedData)
276279
} catch (error) {
277280
// Ignore widget update errors to prevent cascade failures
278281
}

src/composables/graph/useWidgetValue.ts

Lines changed: 25 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,17 @@
22
* Composable for managing widget value synchronization between Vue and LiteGraph
33
* Provides consistent pattern for immediate UI updates and LiteGraph callbacks
44
*/
5-
import { ref, watch } from 'vue'
5+
import { computed, toValue, ref, watch } from 'vue'
66
import type { Ref } from 'vue'
77

88
import type { SimplifiedWidget, WidgetValue } from '@/types/simplifiedWidget'
9+
import type { MaybeRefOrGetter } from '@vueuse/core'
910

1011
interface UseWidgetValueOptions<T extends WidgetValue = WidgetValue, U = T> {
1112
/** The widget configuration from LiteGraph */
1213
widget: SimplifiedWidget<T>
13-
/** The current value from parent component */
14-
modelValue: T
14+
/** The current value from parent component (can be a value or a getter function) */
15+
modelValue: MaybeRefOrGetter<T>
1516
/** Default value if modelValue is null/undefined */
1617
defaultValue: T
1718
/** Emit function from component setup */
@@ -46,8 +47,21 @@ export function useWidgetValue<T extends WidgetValue = WidgetValue, U = T>({
4647
emit,
4748
transform
4849
}: UseWidgetValueOptions<T, U>): UseWidgetValueReturn<T, U> {
49-
// Local value for immediate UI updates
50-
const localValue = ref<T>(modelValue ?? defaultValue)
50+
// Ref for immediate UI feedback before value flows back through modelValue
51+
const newProcessedValue = ref<T | null>(null)
52+
53+
// Computed that prefers the immediately processed value, then falls back to modelValue
54+
const localValue = computed<T>(
55+
() => newProcessedValue.value ?? toValue(modelValue) ?? defaultValue
56+
)
57+
58+
// Clear newProcessedValue when modelValue updates (allowing external changes to flow through)
59+
watch(
60+
() => toValue(modelValue),
61+
() => {
62+
newProcessedValue.value = null
63+
}
64+
)
5165

5266
// Handle user changes
5367
const onChange = (newValue: U) => {
@@ -71,21 +85,13 @@ export function useWidgetValue<T extends WidgetValue = WidgetValue, U = T>({
7185
}
7286
}
7387

74-
// 1. Update local state for immediate UI feedback
75-
localValue.value = processedValue
88+
// Set for immediate UI feedback
89+
newProcessedValue.value = processedValue
7690

77-
// 2. Emit to parent component
91+
// Emit to parent component
7892
emit('update:modelValue', processedValue)
7993
}
8094

81-
// Watch for external updates from LiteGraph
82-
watch(
83-
() => modelValue,
84-
(newValue) => {
85-
localValue.value = newValue ?? defaultValue
86-
}
87-
)
88-
8995
return {
9096
localValue: localValue as Ref<T>,
9197
onChange
@@ -97,7 +103,7 @@ export function useWidgetValue<T extends WidgetValue = WidgetValue, U = T>({
97103
*/
98104
export function useStringWidgetValue(
99105
widget: SimplifiedWidget<string>,
100-
modelValue: string,
106+
modelValue: string | (() => string),
101107
emit: (event: 'update:modelValue', value: string) => void
102108
) {
103109
return useWidgetValue({
@@ -114,7 +120,7 @@ export function useStringWidgetValue(
114120
*/
115121
export function useNumberWidgetValue(
116122
widget: SimplifiedWidget<number>,
117-
modelValue: number,
123+
modelValue: number | (() => number),
118124
emit: (event: 'update:modelValue', value: number) => void
119125
) {
120126
return useWidgetValue({
@@ -137,7 +143,7 @@ export function useNumberWidgetValue(
137143
*/
138144
export function useBooleanWidgetValue(
139145
widget: SimplifiedWidget<boolean>,
140-
modelValue: boolean,
146+
modelValue: boolean | (() => boolean),
141147
emit: (event: 'update:modelValue', value: boolean) => void
142148
) {
143149
return useWidgetValue({

src/extensions/core/uploadAudio.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@ async function uploadFile(
5858
getResourceURL(...splitFilePath(path))
5959
)
6060
audioWidget.value = path
61+
62+
// Manually trigger the callback to update VueNodes
63+
audioWidget.callback?.(path)
6164
}
6265
} else {
6366
useToastStore().addAlert(resp.status + ' - ' + resp.statusText)

src/renderer/extensions/vueNodes/components/LGraphNode.vue

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@
2323
bypassed,
2424
'before:rounded-2xl before:pointer-events-none before:absolute before:inset-0':
2525
muted,
26-
'will-change-transform': isDragging
26+
'will-change-transform': isDragging,
27+
'ring-4 ring-primary-500 bg-primary-500/10': isDraggingOver
2728
},
2829
2930
shouldHandleNodePointerEvents
@@ -43,6 +44,9 @@
4344
v-bind="pointerHandlers"
4445
@wheel="handleWheel"
4546
@contextmenu="handleContextMenu"
47+
@dragover.prevent="handleDragOver"
48+
@dragleave="handleDragLeave"
49+
@drop="handleDrop"
4650
>
4751
<div class="flex flex-col justify-center items-center relative">
4852
<template v-if="isCollapsed">
@@ -132,7 +136,7 @@
132136
</template>
133137

134138
<script setup lang="ts">
135-
import { whenever } from '@vueuse/core'
139+
import { useMouseInElement, whenever } from '@vueuse/core'
136140
import { storeToRefs } from 'pinia'
137141
import { computed, inject, onErrorCaptured, onMounted, ref } from 'vue'
138142
import { useI18n } from 'vue-i18n'
@@ -481,4 +485,43 @@ const nodeMedia = computed(() => {
481485
})
482486
483487
const nodeContainerRef = ref<HTMLDivElement>()
488+
489+
// Track mouse position relative to node container for drag and drop
490+
const { isOutside } = useMouseInElement(nodeContainerRef)
491+
492+
// Drag and drop support
493+
const isDraggingOver = ref(false)
494+
495+
const handleDragOver = (event: DragEvent) => {
496+
const node = lgraphNode.value
497+
if (!node || !node.onDragOver) {
498+
isDraggingOver.value = false
499+
return
500+
}
501+
502+
// Call the litegraph node's onDragOver callback to check if files are valid
503+
const canDrop = node.onDragOver(event)
504+
isDraggingOver.value = canDrop
505+
}
506+
507+
const handleDragLeave = () => {
508+
if (isOutside.value) {
509+
isDraggingOver.value = false
510+
}
511+
}
512+
513+
const handleDrop = async (event: DragEvent) => {
514+
event.preventDefault()
515+
event.stopPropagation()
516+
517+
isDraggingOver.value = false
518+
519+
const node = lgraphNode.value
520+
if (!node || !node.onDragDrop) {
521+
return
522+
}
523+
524+
// Forward the drop event to the litegraph node's onDragDrop callback
525+
await node.onDragDrop(event)
526+
}
484527
</script>

src/renderer/extensions/vueNodes/widgets/components/WidgetSelectDropdown.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ const emit = defineEmits<{
4444
4545
const { localValue, onChange } = useWidgetValue({
4646
widget: props.widget,
47-
modelValue: props.modelValue,
47+
modelValue: () => props.modelValue,
4848
defaultValue: props.widget.options?.values?.[0] || '',
4949
emit
5050
})

src/renderer/extensions/vueNodes/widgets/composables/useImageUploadWidget.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,9 +78,13 @@ export const useImageUploadWidget = () => {
7878
folder,
7979
onUploadComplete: (output) => {
8080
output.forEach((path) => addToComboValues(fileComboWidget, path))
81+
82+
// Create a NEW array to ensure Vue reactivity detects the change
83+
const newValue = allow_batch ? [...output] : output[0]
84+
8185
// @ts-expect-error litegraph combo value type does not support arrays yet
82-
fileComboWidget.value = output
83-
fileComboWidget.callback?.(output)
86+
fileComboWidget.value = newValue
87+
fileComboWidget.callback?.(newValue)
8488
}
8589
})
8690

0 commit comments

Comments
 (0)