feat: add useWidget composable and WidgetItemByKey component

- useWidget composable at src/composables/graph/useWidget.ts

- Returns reactive widget, value, isHidden, isDisabled, isAdvanced, isPromoted, label

- WidgetItemByKey.vue wrapper at src/components/rightSidePanel/parameters/

- 7 new tests for useWidget composable

Amp-Thread-ID: https://ampcode.com/threads/T-019c2639-ebe0-7088-858d-853102e1873c
Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
Alexander Brown
2026-02-03 17:21:25 -08:00
parent f9db91dac5
commit 4a491c5734
3 changed files with 218 additions and 0 deletions

View File

@@ -0,0 +1,53 @@
<script setup lang="ts">
import { computed } from 'vue'
import type { NodeId } from '@/lib/litegraph/src/LGraphNode'
import type { SubgraphNode } from '@/lib/litegraph/src/subgraph/SubgraphNode'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import WidgetItem from './WidgetItem.vue'
const {
nodeId,
widgetName,
isDraggable = false,
hiddenFavoriteIndicator = false,
showNodeName = false,
parents = [],
isShownOnParents = false
} = defineProps<{
nodeId: NodeId
widgetName: string
isDraggable?: boolean
hiddenFavoriteIndicator?: boolean
showNodeName?: boolean
parents?: SubgraphNode[]
isShownOnParents?: boolean
}>()
const emit = defineEmits<{
'update:widgetValue': [value: string | number | boolean | object]
}>()
const canvasStore = useCanvasStore()
const node = computed(() => canvasStore.canvas?.graph?.getNodeById(nodeId))
const widget = computed(() =>
node.value?.widgets?.find((w) => w.name === widgetName)
)
</script>
<template>
<WidgetItem
v-if="node && widget"
:node="node"
:widget="widget"
:is-draggable="isDraggable"
:hidden-favorite-indicator="hiddenFavoriteIndicator"
:show-node-name="showNodeName"
:parents="parents"
:is-shown-on-parents="isShownOnParents"
@update:widget-value="emit('update:widgetValue', $event)"
/>
</template>

View File

@@ -0,0 +1,117 @@
import { createTestingPinia } from '@pinia/testing'
import { setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it } from 'vitest'
import { ref } from 'vue'
import type { NodeId } from '@/lib/litegraph/src/LGraphNode'
import { useWidgetValueStore } from '@/stores/widgetValueStore'
import { useWidget } from './useWidget'
describe('useWidget', () => {
beforeEach(() => {
setActivePinia(createTestingPinia({ stubActions: false }))
})
it('returns undefined widget when not registered', () => {
const { widget, value, isHidden, isDisabled, isAdvanced, isPromoted } =
useWidget(1, 'test_widget')
expect(widget.value).toBeUndefined()
expect(value.value).toBeUndefined()
expect(isHidden.value).toBe(false)
expect(isDisabled.value).toBe(false)
expect(isAdvanced.value).toBe(false)
expect(isPromoted.value).toBe(false)
})
it('returns widget state when registered', () => {
const store = useWidgetValueStore()
store.registerWidget(1, 'test_widget', 'text', 'initial_value', {
hidden: true,
advanced: true,
label: 'Custom Label'
})
const { widget, value, isHidden, isAdvanced, label } = useWidget(
1,
'test_widget'
)
expect(widget.value).toBeDefined()
expect(value.value).toBe('initial_value')
expect(isHidden.value).toBe(true)
expect(isAdvanced.value).toBe(true)
expect(label.value).toBe('Custom Label')
})
it('allows setting value through writable computed', () => {
const store = useWidgetValueStore()
store.registerWidget(1, 'test_widget', 'number', 42)
const { value } = useWidget(1, 'test_widget')
expect(value.value).toBe(42)
value.value = 100
expect(store.get(1, 'test_widget')).toBe(100)
expect(store.getWidget(1, 'test_widget')?.value).toBe(100)
})
it('reacts to nodeId ref changes', () => {
const store = useWidgetValueStore()
store.registerWidget(1, 'shared_widget', 'text', 'node1_value')
store.registerWidget(2, 'shared_widget', 'text', 'node2_value')
const nodeId = ref<NodeId>(1)
const { value } = useWidget(nodeId, 'shared_widget')
expect(value.value).toBe('node1_value')
nodeId.value = 2
expect(value.value).toBe('node2_value')
})
it('reacts to widgetName ref changes', () => {
const store = useWidgetValueStore()
store.registerWidget(1, 'widget_a', 'text', 'value_a')
store.registerWidget(1, 'widget_b', 'text', 'value_b')
const widgetName = ref('widget_a')
const { value } = useWidget(1, widgetName)
expect(value.value).toBe('value_a')
widgetName.value = 'widget_b'
expect(value.value).toBe('value_b')
})
it('reacts to store state changes', () => {
const store = useWidgetValueStore()
store.registerWidget(1, 'test_widget', 'toggle', false)
const { value, isHidden } = useWidget(1, 'test_widget')
expect(value.value).toBe(false)
expect(isHidden.value).toBe(false)
store.set(1, 'test_widget', true)
expect(value.value).toBe(true)
store.setHidden(1, 'test_widget', true)
expect(isHidden.value).toBe(true)
})
it('returns correct disabled and promoted states', () => {
const store = useWidgetValueStore()
store.registerWidget(1, 'test_widget', 'text', 'value', {
disabled: true,
promoted: true
})
const { isDisabled, isPromoted } = useWidget(1, 'test_widget')
expect(isDisabled.value).toBe(true)
expect(isPromoted.value).toBe(true)
})
})

View File

@@ -0,0 +1,48 @@
import type { MaybeRefOrGetter } from 'vue'
import { computed, toValue } from 'vue'
import type { NodeId } from '@/lib/litegraph/src/LGraphNode'
import type { WidgetState } from '@/stores/widgetValueStore'
import { useWidgetValueStore } from '@/stores/widgetValueStore'
/**
* Composable for reactive access to widget state by (nodeId, widgetName).
*
* Provides computed accessors for widget value and metadata flags.
* The value computed is writable, allowing direct assignment.
*
* @param nodeId - Node ID (can be ref or getter)
* @param widgetName - Widget name (can be ref or getter)
*/
export function useWidget(
nodeId: MaybeRefOrGetter<NodeId>,
widgetName: MaybeRefOrGetter<string>
) {
const store = useWidgetValueStore()
const widget = computed<WidgetState | undefined>(() =>
store.getWidget(toValue(nodeId), toValue(widgetName))
)
const value = computed({
get: () => widget.value?.value,
set: (v: unknown) => store.set(toValue(nodeId), toValue(widgetName), v)
})
const isHidden = computed(() => widget.value?.hidden ?? false)
const isDisabled = computed(() => widget.value?.disabled ?? false)
const isAdvanced = computed(() => widget.value?.advanced ?? false)
const isPromoted = computed(() => widget.value?.promoted ?? false)
const label = computed(() => widget.value?.label)
return {
widget,
value,
isHidden,
isDisabled,
isAdvanced,
isPromoted,
label
}
}