From fac86e35bf0bd4bfd4c486931d5a8a33d5c566c3 Mon Sep 17 00:00:00 2001 From: Johnpaul Chiwetelu <49923152+Myestery@users.noreply.github.com> Date: Wed, 5 Nov 2025 09:11:56 +0100 Subject: [PATCH] Drag vuenodes input (#6514) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- src/composables/graph/useGraphNodeManager.ts | 7 ++- src/composables/graph/useWidgetValue.ts | 44 +++++++++-------- src/extensions/core/uploadAudio.ts | 3 ++ .../vueNodes/components/LGraphNode.vue | 47 ++++++++++++++++++- .../components/WidgetSelectDropdown.vue | 2 +- .../composables/useImageUploadWidget.ts | 8 +++- 6 files changed, 85 insertions(+), 26 deletions(-) diff --git a/src/composables/graph/useGraphNodeManager.ts b/src/composables/graph/useGraphNodeManager.ts index 2e9c54a2c..24b2b1a64 100644 --- a/src/composables/graph/useGraphNodeManager.ts +++ b/src/composables/graph/useGraphNodeManager.ts @@ -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 } diff --git a/src/composables/graph/useWidgetValue.ts b/src/composables/graph/useWidgetValue.ts index 87658f83f..20e018881 100644 --- a/src/composables/graph/useWidgetValue.ts +++ b/src/composables/graph/useWidgetValue.ts @@ -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 { /** The widget configuration from LiteGraph */ widget: SimplifiedWidget - /** The current value from parent component */ - modelValue: T + /** The current value from parent component (can be a value or a getter function) */ + modelValue: MaybeRefOrGetter /** Default value if modelValue is null/undefined */ defaultValue: T /** Emit function from component setup */ @@ -46,8 +47,21 @@ export function useWidgetValue({ emit, transform }: UseWidgetValueOptions): UseWidgetValueReturn { - // Local value for immediate UI updates - const localValue = ref(modelValue ?? defaultValue) + // Ref for immediate UI feedback before value flows back through modelValue + const newProcessedValue = ref(null) + + // Computed that prefers the immediately processed value, then falls back to modelValue + const localValue = computed( + () => 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({ } } - // 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, onChange @@ -97,7 +103,7 @@ export function useWidgetValue({ */ export function useStringWidgetValue( widget: SimplifiedWidget, - 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, - 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, - modelValue: boolean, + modelValue: boolean | (() => boolean), emit: (event: 'update:modelValue', value: boolean) => void ) { return useWidgetValue({ diff --git a/src/extensions/core/uploadAudio.ts b/src/extensions/core/uploadAudio.ts index cd2ff97e1..dfefc88a0 100644 --- a/src/extensions/core/uploadAudio.ts +++ b/src/extensions/core/uploadAudio.ts @@ -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) diff --git a/src/renderer/extensions/vueNodes/components/LGraphNode.vue b/src/renderer/extensions/vueNodes/components/LGraphNode.vue index ead65d2cc..e63289610 100644 --- a/src/renderer/extensions/vueNodes/components/LGraphNode.vue +++ b/src/renderer/extensions/vueNodes/components/LGraphNode.vue @@ -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" >
diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetSelectDropdown.vue b/src/renderer/extensions/vueNodes/widgets/components/WidgetSelectDropdown.vue index bb5accb81..1d0b139ee 100644 --- a/src/renderer/extensions/vueNodes/widgets/components/WidgetSelectDropdown.vue +++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetSelectDropdown.vue @@ -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 }) diff --git a/src/renderer/extensions/vueNodes/widgets/composables/useImageUploadWidget.ts b/src/renderer/extensions/vueNodes/widgets/composables/useImageUploadWidget.ts index c8c4f575f..78f9d0c97 100644 --- a/src/renderer/extensions/vueNodes/widgets/composables/useImageUploadWidget.ts +++ b/src/renderer/extensions/vueNodes/widgets/composables/useImageUploadWidget.ts @@ -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) } })