mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-06-07 08:14:42 +00:00
Compare commits
4 Commits
jaeone/fe-
...
drjkl/widg
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
96657158b7 | ||
|
|
4307d4cdb7 | ||
|
|
e7f4578a4a | ||
|
|
c1afcead2c |
@@ -77,14 +77,42 @@ test.describe('Optional input', { tag: ['@screenshot', '@node'] }, () => {
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('simple_slider.png')
|
||||
})
|
||||
|
||||
test('unknown converted widget', async ({ comfyPage }) => {
|
||||
await comfyPage.workflow.loadWorkflow(
|
||||
'missing/missing_nodes_converted_widget'
|
||||
)
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'missing_nodes_converted_widget.png'
|
||||
)
|
||||
})
|
||||
test(
|
||||
'unknown converted widget',
|
||||
{ tag: ['@vue-nodes'] },
|
||||
async ({ comfyPage }) => {
|
||||
await comfyPage.workflow.loadWorkflow(
|
||||
'missing/missing_nodes_converted_widget'
|
||||
)
|
||||
|
||||
const node = comfyPage.vueNodes.getNodeLocator('1')
|
||||
await expect(node).toBeVisible()
|
||||
|
||||
// Title reflects the unknown node type
|
||||
await expect(node).toContainText('UNKNOWN NODE')
|
||||
|
||||
// Inputs include the regular IMAGE input and the converted-widget "foo"
|
||||
// (which is what differentiates this fixture from a plain missing node).
|
||||
const nodeRef = await comfyPage.nodeOps.getNodeRefById('1')
|
||||
const inputs = (await nodeRef.getProperty('inputs')) as {
|
||||
name: string
|
||||
type: string
|
||||
widget?: { name: string }
|
||||
}[]
|
||||
expect(inputs.map((i) => i.name)).toEqual(['image', 'foo'])
|
||||
|
||||
const fooInput = inputs.find((i) => i.name === 'foo')
|
||||
expect(fooInput?.type).toBe('STRING')
|
||||
expect(fooInput?.widget?.name).toBe('foo')
|
||||
|
||||
// Per-node DOM screenshot localizes any visual regression to this node.
|
||||
// A canvas-wide screenshot turns a 1-row label drift into a "763 px diff"
|
||||
// with no signal about which widget is responsible.
|
||||
await expect(node).toHaveScreenshot(
|
||||
'missing_nodes_converted_widget_node.png'
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
test('dynamically added input', async ({ comfyPage }) => {
|
||||
await comfyPage.workflow.loadWorkflow('inputs/dynamically_added_input')
|
||||
|
||||
@@ -6,7 +6,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useAppModeStore } from '@/stores/appModeStore'
|
||||
import type { WidgetEntityId } from '@/world/entityIds'
|
||||
import { widgetEntityId } from '@/world/entityIds'
|
||||
|
||||
import { useResolvedSelectedInputs } from './useResolvedSelectedInputs'
|
||||
|
||||
@@ -22,14 +22,14 @@ vi.mock('@/scripts/app', () => ({
|
||||
}))
|
||||
|
||||
const rootGraphId = '11111111-1111-4111-8111-111111111111'
|
||||
const entitySeed = `${rootGraphId}:1:seed` as WidgetEntityId
|
||||
const entitySeed = widgetEntityId(rootGraphId, 1, 'seed')
|
||||
|
||||
function makeNode(id: number, widgetNames: string[]): LGraphNode {
|
||||
return fromAny<LGraphNode, unknown>({
|
||||
id,
|
||||
widgets: widgetNames.map((name) => ({
|
||||
name,
|
||||
entityId: `${rootGraphId}:${id}:${name}` as WidgetEntityId
|
||||
entityId: widgetEntityId(rootGraphId, id, name)
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -16,10 +16,7 @@ import {
|
||||
shouldExpand
|
||||
} from '@/renderer/extensions/vueNodes/widgets/registry/widgetRegistry'
|
||||
import { useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
import {
|
||||
useWidgetValueStore,
|
||||
stripGraphPrefix
|
||||
} from '@/stores/widgetValueStore'
|
||||
import { useWidgetValueStore } from '@/stores/widgetValueStore'
|
||||
import { useFavoritedWidgetsStore } from '@/stores/workspace/favoritedWidgetsStore'
|
||||
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
||||
import { resolveNodeDisplayName } from '@/utils/nodeTitleUtil'
|
||||
@@ -58,8 +55,8 @@ const { t } = useI18n()
|
||||
|
||||
const canvasStore = useCanvasStore()
|
||||
const nodeDefStore = useNodeDefStore()
|
||||
const widgetValueStore = useWidgetValueStore()
|
||||
const favoritedWidgetsStore = useFavoritedWidgetsStore()
|
||||
const widgetValueStore = useWidgetValueStore()
|
||||
const isEditing = ref(false)
|
||||
|
||||
const widgetComponent = computed(() => {
|
||||
@@ -74,10 +71,8 @@ function resolveSourceWidget(): { node: LGraphNode; widget: IBaseWidget } {
|
||||
|
||||
const simplifiedWidget = computed((): SimplifiedWidget => {
|
||||
const { node: sourceNode, widget: sourceWidget } = resolveSourceWidget()
|
||||
const graphId = node.graph?.rootGraph?.id
|
||||
const bareNodeId = stripGraphPrefix(String(sourceNode.id))
|
||||
const widgetState = graphId
|
||||
? widgetValueStore.getWidget(graphId, bareNodeId, sourceWidget.name)
|
||||
const widgetState = sourceWidget.entityId
|
||||
? widgetValueStore.getWidget(sourceWidget.entityId)
|
||||
: undefined
|
||||
|
||||
return {
|
||||
|
||||
@@ -7,7 +7,7 @@ import { computed, nextTick, watch } from 'vue'
|
||||
import { useGraphNodeManager } from '@/composables/graph/useGraphNodeManager'
|
||||
import { createPromotedWidgetView } from '@/core/graph/subgraph/promotedWidgetView'
|
||||
import { BaseWidget, LGraph, LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { widgetEntityId } from '@/world/entityIds'
|
||||
import { asGraphId, widgetEntityId } from '@/world/entityIds'
|
||||
import {
|
||||
createTestSubgraph,
|
||||
createTestSubgraphNode
|
||||
@@ -47,9 +47,10 @@ describe('Node Reactivity', () => {
|
||||
expect((widget as BaseWidget).node.id).toBe(node.id)
|
||||
|
||||
// Initial value should be in store after setNodeId was called
|
||||
expect(store.getWidget(graph.id, node.id, 'testnum')?.value).toBe(2)
|
||||
const testnumId = widgetEntityId(asGraphId(graph.id), node.id, 'testnum')
|
||||
expect(store.getWidget(testnumId)?.value).toBe(2)
|
||||
|
||||
const state = store.getWidget(graph.id, node.id, 'testnum')
|
||||
const state = store.getWidget(testnumId)
|
||||
if (!state) throw new Error('Expected widget state to exist')
|
||||
|
||||
const onValueChange = vi.fn()
|
||||
@@ -74,7 +75,9 @@ describe('Node Reactivity', () => {
|
||||
})
|
||||
await nextTick()
|
||||
|
||||
const state = store.getWidget(graph.id, node.id, 'testnum')
|
||||
const state = store.getWidget(
|
||||
widgetEntityId(asGraphId(graph.id), node.id, 'testnum')
|
||||
)
|
||||
if (!state) throw new Error('Expected widget state to exist')
|
||||
|
||||
const widgetValue = computed(() => state.value)
|
||||
|
||||
@@ -66,7 +66,8 @@ vi.mock('@/renderer/core/canvas/canvasStore', () => ({
|
||||
|
||||
vi.mock('@/stores/widgetValueStore', () => ({
|
||||
useWidgetValueStore: () => ({
|
||||
getNodeWidgets: vi.fn(() => [])
|
||||
getNodeWidgets: vi.fn(() => []),
|
||||
getNodeWidgetsByName: vi.fn(() => new Map())
|
||||
})
|
||||
}))
|
||||
|
||||
|
||||
@@ -1,12 +1,42 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { createTestingPinia } from '@pinia/testing'
|
||||
import { setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import type { NodeId } from '@/lib/litegraph/src/LGraphNode'
|
||||
import type { UUID } from '@/utils/uuid'
|
||||
import { useWidgetValueStore } from '@/stores/widgetValueStore'
|
||||
import type { WidgetState } from '@/stores/widgetValueStore'
|
||||
import type { NodeId } from '@/platform/workflow/validation/schemas/workflowSchema'
|
||||
import { asGraphId, widgetEntityId } from '@/world/entityIds'
|
||||
|
||||
import { boundsExtractor, singleValueExtractor } from './useUpstreamValue'
|
||||
import {
|
||||
boundsExtractor,
|
||||
singleValueExtractor,
|
||||
useUpstreamValue
|
||||
} from './useUpstreamValue'
|
||||
|
||||
function widget(name: string, value: unknown): WidgetState {
|
||||
return { name, type: 'INPUT', value, nodeId: '1' as NodeId, options: {} }
|
||||
vi.mock('@/renderer/core/canvas/canvasStore', () => ({
|
||||
useCanvasStore: () => ({
|
||||
canvas: {
|
||||
graph: { rootGraph: { id: '00000000-0000-0000-0000-000000000001' } }
|
||||
}
|
||||
})
|
||||
}))
|
||||
|
||||
function widgetState(value: unknown): WidgetState {
|
||||
return {
|
||||
type: 'INPUT',
|
||||
value,
|
||||
options: {},
|
||||
label: undefined,
|
||||
serialize: undefined,
|
||||
disabled: undefined
|
||||
}
|
||||
}
|
||||
|
||||
function widgetMap(
|
||||
...entries: Array<[string, unknown]>
|
||||
): Map<string, WidgetState> {
|
||||
return new Map(entries.map(([name, value]) => [name, widgetState(value)]))
|
||||
}
|
||||
|
||||
const isNumber = (v: unknown): v is number => typeof v === 'number'
|
||||
@@ -15,37 +45,37 @@ describe('singleValueExtractor', () => {
|
||||
const extract = singleValueExtractor(isNumber)
|
||||
|
||||
it('matches widget by outputName', () => {
|
||||
const widgets = [widget('a', 'text'), widget('b', 42)]
|
||||
const widgets = widgetMap(['a', 'text'], ['b', 42])
|
||||
expect(extract(widgets, 'b')).toBe(42)
|
||||
})
|
||||
|
||||
it('returns undefined when outputName widget has invalid value', () => {
|
||||
const widgets = [widget('a', 'text'), widget('b', 'not a number')]
|
||||
const widgets = widgetMap(['a', 'text'], ['b', 'not a number'])
|
||||
expect(extract(widgets, 'b')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('falls back to unique valid widget when outputName has no match', () => {
|
||||
const widgets = [widget('a', 'text'), widget('b', 42)]
|
||||
const widgets = widgetMap(['a', 'text'], ['b', 42])
|
||||
expect(extract(widgets, 'missing')).toBe(42)
|
||||
})
|
||||
|
||||
it('falls back to unique valid widget when no outputName provided', () => {
|
||||
const widgets = [widget('a', 'text'), widget('b', 42)]
|
||||
const widgets = widgetMap(['a', 'text'], ['b', 42])
|
||||
expect(extract(widgets, undefined)).toBe(42)
|
||||
})
|
||||
|
||||
it('returns undefined when multiple widgets have valid values', () => {
|
||||
const widgets = [widget('a', 1), widget('b', 2)]
|
||||
const widgets = widgetMap(['a', 1], ['b', 2])
|
||||
expect(extract(widgets, undefined)).toBeUndefined()
|
||||
})
|
||||
|
||||
it('returns undefined when no widgets have valid values', () => {
|
||||
const widgets = [widget('a', 'text')]
|
||||
const widgets = widgetMap(['a', 'text'])
|
||||
expect(extract(widgets, undefined)).toBeUndefined()
|
||||
})
|
||||
|
||||
it('returns undefined for empty widgets', () => {
|
||||
expect(extract([], undefined)).toBeUndefined()
|
||||
expect(extract(new Map(), undefined)).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -54,23 +84,23 @@ describe('boundsExtractor', () => {
|
||||
|
||||
it('extracts a single bounds object widget', () => {
|
||||
const bounds = { x: 10, y: 20, width: 100, height: 200 }
|
||||
const widgets = [widget('crop', bounds)]
|
||||
const widgets = widgetMap(['crop', bounds])
|
||||
expect(extract(widgets, undefined)).toEqual(bounds)
|
||||
})
|
||||
|
||||
it('matches bounds widget by outputName', () => {
|
||||
const bounds = { x: 1, y: 2, width: 3, height: 4 }
|
||||
const widgets = [widget('other', 'text'), widget('crop', bounds)]
|
||||
const widgets = widgetMap(['other', 'text'], ['crop', bounds])
|
||||
expect(extract(widgets, 'crop')).toEqual(bounds)
|
||||
})
|
||||
|
||||
it('assembles bounds from individual x/y/width/height widgets', () => {
|
||||
const widgets = [
|
||||
widget('x', 10),
|
||||
widget('y', 20),
|
||||
widget('width', 100),
|
||||
widget('height', 200)
|
||||
]
|
||||
const widgets = widgetMap(
|
||||
['x', 10],
|
||||
['y', 20],
|
||||
['width', 100],
|
||||
['height', 200]
|
||||
)
|
||||
expect(extract(widgets, undefined)).toEqual({
|
||||
x: 10,
|
||||
y: 20,
|
||||
@@ -80,39 +110,76 @@ describe('boundsExtractor', () => {
|
||||
})
|
||||
|
||||
it('returns undefined when some bound components are missing', () => {
|
||||
const widgets = [widget('x', 10), widget('y', 20), widget('width', 100)]
|
||||
const widgets = widgetMap(['x', 10], ['y', 20], ['width', 100])
|
||||
expect(extract(widgets, undefined)).toBeUndefined()
|
||||
})
|
||||
|
||||
it('returns undefined when bound components have wrong types', () => {
|
||||
const widgets = [
|
||||
widget('x', '10'),
|
||||
widget('y', 20),
|
||||
widget('width', 100),
|
||||
widget('height', 200)
|
||||
]
|
||||
const widgets = widgetMap(
|
||||
['x', '10'],
|
||||
['y', 20],
|
||||
['width', 100],
|
||||
['height', 200]
|
||||
)
|
||||
expect(extract(widgets, undefined)).toBeUndefined()
|
||||
})
|
||||
|
||||
it('returns undefined for empty widgets', () => {
|
||||
expect(extract([], undefined)).toBeUndefined()
|
||||
expect(extract(new Map(), undefined)).toBeUndefined()
|
||||
})
|
||||
|
||||
it('rejects partial bounds objects', () => {
|
||||
const partial = { x: 10, y: 20 }
|
||||
const widgets = [widget('crop', partial)]
|
||||
const widgets = widgetMap(['crop', partial])
|
||||
expect(extract(widgets, undefined)).toBeUndefined()
|
||||
})
|
||||
|
||||
it('prefers single bounds object over individual widgets', () => {
|
||||
const bounds = { x: 1, y: 2, width: 3, height: 4 }
|
||||
const widgets = [
|
||||
widget('crop', bounds),
|
||||
widget('x', 99),
|
||||
widget('y', 99),
|
||||
widget('width', 99),
|
||||
widget('height', 99)
|
||||
]
|
||||
const widgets = widgetMap(
|
||||
['crop', bounds],
|
||||
['x', 99],
|
||||
['y', 99],
|
||||
['width', 99],
|
||||
['height', 99]
|
||||
)
|
||||
expect(extract(widgets, undefined)).toEqual(bounds)
|
||||
})
|
||||
})
|
||||
|
||||
describe('useUpstreamValue (store-backed read path)', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createTestingPinia({ stubActions: false }))
|
||||
})
|
||||
|
||||
it('reads upstream node widgets via the widget value store', () => {
|
||||
const graphId = '00000000-0000-0000-0000-000000000001' as UUID
|
||||
const widgetId = widgetEntityId(
|
||||
asGraphId(graphId),
|
||||
'upstream-1' as NodeId,
|
||||
'value'
|
||||
)
|
||||
const state = useWidgetValueStore().registerWidget(widgetId, {
|
||||
type: 'number',
|
||||
value: 7,
|
||||
options: {}
|
||||
})
|
||||
|
||||
const upstreamValue = useUpstreamValue<number>(
|
||||
() => ({ nodeId: 'upstream-1', outputName: 'value' }),
|
||||
singleValueExtractor((v): v is number => typeof v === 'number')
|
||||
)
|
||||
|
||||
expect(upstreamValue.value).toBe(7)
|
||||
state.value = 11
|
||||
expect(upstreamValue.value).toBe(11)
|
||||
})
|
||||
|
||||
it('returns undefined when no upstream linkage is provided', () => {
|
||||
const upstreamValue = useUpstreamValue(
|
||||
() => undefined,
|
||||
singleValueExtractor((v): v is number => typeof v === 'number')
|
||||
)
|
||||
expect(upstreamValue.value).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { useWidgetValueStore } from '@/stores/widgetValueStore'
|
||||
import type { WidgetState } from '@/stores/widgetValueStore'
|
||||
import type { Bounds } from '@/renderer/core/layout/types'
|
||||
import type { WidgetState } from '@/stores/widgetValueStore'
|
||||
import { useWidgetValueStore } from '@/stores/widgetValueStore'
|
||||
import type { LinkedUpstreamInfo } from '@/types/simplifiedWidget'
|
||||
import { asGraphId, nodeEntityId } from '@/world/entityIds'
|
||||
|
||||
type ValueExtractor<T = unknown> = (
|
||||
widgets: WidgetState[],
|
||||
widgets: Map<string, WidgetState>,
|
||||
outputName: string | undefined
|
||||
) => T | undefined
|
||||
|
||||
@@ -23,7 +24,9 @@ export function useUpstreamValue<T>(
|
||||
if (!upstream) return undefined
|
||||
const graphId = canvasStore.canvas?.graph?.rootGraph.id
|
||||
if (!graphId) return undefined
|
||||
const widgets = widgetValueStore.getNodeWidgets(graphId, upstream.nodeId)
|
||||
const widgets = widgetValueStore.getNodeWidgetsByName(
|
||||
nodeEntityId(asGraphId(graphId), upstream.nodeId)
|
||||
)
|
||||
return extractValue(widgets, upstream.outputName)
|
||||
})
|
||||
}
|
||||
@@ -33,10 +36,12 @@ export function singleValueExtractor<T>(
|
||||
): ValueExtractor<T> {
|
||||
return (widgets, outputName) => {
|
||||
if (outputName) {
|
||||
const matched = widgets.find((w) => w.name === outputName)
|
||||
const matched = widgets.get(outputName)
|
||||
if (matched && isValid(matched.value)) return matched.value
|
||||
}
|
||||
const validValues = widgets.map((w) => w.value).filter(isValid)
|
||||
const validValues = [...widgets.values()]
|
||||
.map((w) => w.value)
|
||||
.filter(isValid)
|
||||
return validValues.length === 1 ? validValues[0] : undefined
|
||||
}
|
||||
}
|
||||
@@ -60,7 +65,7 @@ export function boundsExtractor(): ValueExtractor<Bounds> {
|
||||
|
||||
// Fallback: assemble from individual widgets matching BoundingBoxInputSpec field names
|
||||
const getNum = (name: string): number | undefined => {
|
||||
const w = widgets.find((w) => w.name === name)
|
||||
const w = widgets.get(name)
|
||||
return typeof w?.value === 'number' ? w.value : undefined
|
||||
}
|
||||
const x = getNum('x')
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
resetSubgraphFixtureState
|
||||
} from '@/lib/litegraph/src/subgraph/__fixtures__/subgraphHelpers'
|
||||
import { useWidgetValueStore } from '@/stores/widgetValueStore'
|
||||
import { asGraphId, widgetEntityId } from '@/world/entityIds'
|
||||
|
||||
import { isPromotedWidgetView } from './promotedWidgetTypes'
|
||||
|
||||
@@ -73,9 +74,12 @@ describe('PromotedWidgetView — host-wins semantics', () => {
|
||||
subgraph.inputNode.slots[0].connect(interior.inputs[0], interior)
|
||||
|
||||
const widgetStore = useWidgetValueStore()
|
||||
widgetStore.registerWidget(subgraph.rootGraph.id, {
|
||||
nodeId: String(interior.id),
|
||||
name: 'widget',
|
||||
const interiorWidgetId = widgetEntityId(
|
||||
asGraphId(subgraph.rootGraph.id),
|
||||
String(interior.id),
|
||||
'widget'
|
||||
)
|
||||
widgetStore.registerWidget(interiorWidgetId, {
|
||||
type: 'number',
|
||||
value: 42,
|
||||
options: {},
|
||||
@@ -90,11 +94,7 @@ describe('PromotedWidgetView — host-wins semantics', () => {
|
||||
|
||||
view.value = 99
|
||||
|
||||
const interiorState = widgetStore.getWidget(
|
||||
subgraph.rootGraph.id,
|
||||
String(interior.id),
|
||||
'widget'
|
||||
)
|
||||
const interiorState = widgetStore.getWidget(interiorWidgetId)
|
||||
expect(interiorState?.value).toBe(42)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { LGraphNode, NodeId } from '@/lib/litegraph/src/LGraphNode'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
||||
import type { LGraphCanvas } from '@/lib/litegraph/src/LGraphCanvas'
|
||||
import type { CanvasPointer } from '@/lib/litegraph/src/CanvasPointer'
|
||||
import type { Point } from '@/lib/litegraph/src/interfaces'
|
||||
@@ -11,11 +11,8 @@ import { toConcreteWidget } from '@/lib/litegraph/src/widgets/widgetMap'
|
||||
import { t } from '@/i18n'
|
||||
import { nextValueForLinkedTarget } from '@/scripts/valueControl'
|
||||
import { useDomWidgetStore } from '@/stores/domWidgetStore'
|
||||
import {
|
||||
stripGraphPrefix,
|
||||
useWidgetValueStore
|
||||
} from '@/stores/widgetValueStore'
|
||||
import type { WidgetState } from '@/stores/widgetValueStore'
|
||||
import { useWidgetValueStore } from '@/stores/widgetValueStore'
|
||||
import {
|
||||
resolveConcretePromotedWidget,
|
||||
resolvePromotedWidgetAtHost
|
||||
@@ -24,7 +21,6 @@ import { matchPromotedInput } from '@/core/graph/subgraph/matchPromotedInput'
|
||||
import { hasWidgetNode } from '@/core/graph/subgraph/widgetNodeTypeGuard'
|
||||
import type { WidgetEntityId } from '@/world/entityIds'
|
||||
import { widgetEntityId } from '@/world/entityIds'
|
||||
import { ensureWidgetState, getWidgetState } from '@/world/widgetValueIO'
|
||||
|
||||
import { isPromotedWidgetView } from './promotedWidgetTypes'
|
||||
import type { PromotedWidgetView as IPromotedWidgetView } from './promotedWidgetTypes'
|
||||
@@ -162,7 +158,7 @@ class PromotedWidgetView implements IPromotedWidgetView {
|
||||
}
|
||||
|
||||
private getHostWidgetState(): WidgetState | undefined {
|
||||
return getWidgetState(this.entityId)
|
||||
return useWidgetValueStore().getWidget(this.entityId)
|
||||
}
|
||||
|
||||
private setHostWidgetState(value: IBaseWidget['value']): void {
|
||||
@@ -207,8 +203,12 @@ class PromotedWidgetView implements IPromotedWidgetView {
|
||||
}
|
||||
|
||||
private registerHostWidgetState(value: IBaseWidget['value']): void {
|
||||
const widgetValueStore = useWidgetValueStore()
|
||||
const existing = widgetValueStore.getWidget(this.entityId)
|
||||
if (existing) return
|
||||
|
||||
const resolved = this.resolveDeepest()
|
||||
ensureWidgetState(this.entityId, {
|
||||
widgetValueStore.registerWidget(this.entityId, {
|
||||
type: resolved?.widget.type ?? 'button',
|
||||
value,
|
||||
options: { ...(resolved?.widget.options ?? {}) },
|
||||
@@ -418,19 +418,11 @@ class PromotedWidgetView implements IPromotedWidgetView {
|
||||
if (linkedState) return linkedState
|
||||
|
||||
const resolved = this.resolveDeepest()
|
||||
if (!resolved) return undefined
|
||||
return useWidgetValueStore().getWidget(
|
||||
this.graphId,
|
||||
stripGraphPrefix(String(resolved.node.id)),
|
||||
resolved.widget.name
|
||||
)
|
||||
if (!resolved?.widget.entityId) return undefined
|
||||
return useWidgetValueStore().getWidget(resolved.widget.entityId)
|
||||
}
|
||||
|
||||
private getLinkedInputWidgets(): Array<{
|
||||
nodeId: NodeId
|
||||
widgetName: string
|
||||
widget: IBaseWidget
|
||||
}> {
|
||||
private getLinkedInputWidgets(): IBaseWidget[] {
|
||||
const linkedInputSlot = this.subgraphNode.inputs.find((input) => {
|
||||
if (!input._subgraphSlot) return false
|
||||
if (matchPromotedInput([input], this) !== input) return false
|
||||
@@ -457,23 +449,15 @@ class PromotedWidgetView implements IPromotedWidgetView {
|
||||
const linkedInput = linkedInputSlot?._subgraphSlot
|
||||
if (!linkedInput) return []
|
||||
|
||||
return linkedInput
|
||||
.getConnectedWidgets()
|
||||
.filter(hasWidgetNode)
|
||||
.map((widget) => ({
|
||||
nodeId: stripGraphPrefix(String(widget.node.id)),
|
||||
widgetName: widget.name,
|
||||
widget
|
||||
}))
|
||||
return linkedInput.getConnectedWidgets().filter(hasWidgetNode)
|
||||
}
|
||||
|
||||
private getLinkedInputWidgetStates(): WidgetState[] {
|
||||
const widgetStore = useWidgetValueStore()
|
||||
|
||||
const widgetValueStore = useWidgetValueStore()
|
||||
return this.getLinkedInputWidgets()
|
||||
.map(({ nodeId, widgetName }) =>
|
||||
widgetStore.getWidget(this.graphId, nodeId, widgetName)
|
||||
)
|
||||
.map((widget) => widget.entityId)
|
||||
.filter((id): id is WidgetEntityId => id !== undefined)
|
||||
.map((id) => widgetValueStore.getWidget(id))
|
||||
.filter((state): state is WidgetState => state !== undefined)
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { useLitegraphService } from '@/services/litegraphService'
|
||||
import { usePreviewExposureStore } from '@/stores/previewExposureStore'
|
||||
import { useSubgraphNavigationStore } from '@/stores/subgraphNavigationStore'
|
||||
import { readWidgetValue } from '@/world/widgetValueIO'
|
||||
import { useWidgetValueStore } from '@/stores/widgetValueStore'
|
||||
|
||||
type PartialNode = Pick<LGraphNode, 'title' | 'id' | 'type'>
|
||||
|
||||
@@ -121,7 +121,7 @@ function getExplicitHostWidgetValue(
|
||||
if (!widget) return undefined
|
||||
if (!isPromotedWidgetView(widget)) return widget.value
|
||||
|
||||
const value = readWidgetValue(widget.entityId)
|
||||
const value = useWidgetValueStore().getWidget(widget.entityId)?.value
|
||||
return isWidgetValue(value) ? value : undefined
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
||||
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
|
||||
import type { LLink } from '@/lib/litegraph/src/LLink'
|
||||
import { commonType } from '@/lib/litegraph/src/utils/type'
|
||||
import { resolveNodeRootGraphId } from '@/lib/litegraph/src/utils/widget'
|
||||
|
||||
import { transformInputSpecV1ToV2 } from '@/schemas/nodeDef/migration'
|
||||
import type { ComboInputSpec, InputSpec } from '@/schemas/nodeDefSchema'
|
||||
import type { InputSpec as InputSpecV2 } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
@@ -187,11 +187,9 @@ function dynamicComboWidget(
|
||||
//A little hacky, but onConfigure won't work.
|
||||
//It fires too late and is overly disruptive
|
||||
let widgetValue = widget.value
|
||||
const getState = () => {
|
||||
const graphId = resolveNodeRootGraphId(node)
|
||||
if (!graphId) return undefined
|
||||
return useWidgetValueStore().getWidget(graphId, node.id, widget.name)
|
||||
}
|
||||
const widgetValueStore = useWidgetValueStore()
|
||||
const getState = () =>
|
||||
widget.entityId ? widgetValueStore.getWidget(widget.entityId) : undefined
|
||||
Object.defineProperty(widget, 'value', {
|
||||
get() {
|
||||
return getState()?.value ?? widgetValue
|
||||
|
||||
@@ -47,21 +47,20 @@ function onCustomComboCreated(this: LGraphNode) {
|
||||
const widget = node.addWidget('string', widgetName, '', () => {})
|
||||
if (!widget) return
|
||||
let localValue = `${widget.value ?? ''}`
|
||||
const widgetValueStore = useWidgetValueStore()
|
||||
|
||||
Object.defineProperty(widget, 'value', {
|
||||
get() {
|
||||
return (
|
||||
useWidgetValueStore().getWidget(app.rootGraph.id, node.id, widgetName)
|
||||
?.value ?? localValue
|
||||
)
|
||||
const state = widget.entityId
|
||||
? widgetValueStore.getWidget(widget.entityId)
|
||||
: undefined
|
||||
return state?.value ?? localValue
|
||||
},
|
||||
set(v: string) {
|
||||
localValue = v
|
||||
const state = useWidgetValueStore().getWidget(
|
||||
app.rootGraph.id,
|
||||
node.id,
|
||||
widgetName
|
||||
)
|
||||
const state = widget.entityId
|
||||
? widgetValueStore.getWidget(widget.entityId)
|
||||
: undefined
|
||||
if (state) state.value = v
|
||||
updateCombo()
|
||||
if (!node.widgets) return
|
||||
|
||||
@@ -24,6 +24,7 @@ import { getNodeByLocatorId } from '@/utils/graphTraversalUtil'
|
||||
|
||||
import { api } from '../../scripts/api'
|
||||
import { app } from '../../scripts/app'
|
||||
import { deriveWidgetEntityId } from '@/world/entityIds'
|
||||
import { useWidgetValueStore } from '@/stores/widgetValueStore'
|
||||
|
||||
function updateUIWidget(
|
||||
@@ -150,19 +151,27 @@ app.registerExtension({
|
||||
}
|
||||
}
|
||||
|
||||
audioUIWidget.options.getValue = () =>
|
||||
(useWidgetValueStore().getWidget(
|
||||
const widgetValueStore = useWidgetValueStore()
|
||||
audioUIWidget.options.getValue = () => {
|
||||
const entityId = deriveWidgetEntityId(
|
||||
resolveNodeRootGraphId(node, app.rootGraph.id),
|
||||
node.id,
|
||||
inputName
|
||||
)?.value as string) ?? ''
|
||||
)
|
||||
const widgetState = entityId
|
||||
? widgetValueStore.getWidget(entityId)
|
||||
: undefined
|
||||
return (widgetState?.value as string) ?? ''
|
||||
}
|
||||
audioUIWidget.options.setValue = (v) => {
|
||||
const graphId = resolveNodeRootGraphId(node, app.rootGraph.id)
|
||||
const widgetState = useWidgetValueStore().getWidget(
|
||||
graphId,
|
||||
const entityId = deriveWidgetEntityId(
|
||||
resolveNodeRootGraphId(node, app.rootGraph.id),
|
||||
node.id,
|
||||
inputName
|
||||
)
|
||||
const widgetState = entityId
|
||||
? widgetValueStore.getWidget(entityId)
|
||||
: undefined
|
||||
if (widgetState) widgetState.value = v
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import type { UUID } from '@/utils/uuid'
|
||||
import { zeroUuid } from '@/utils/uuid'
|
||||
import { usePreviewExposureStore } from '@/stores/previewExposureStore'
|
||||
import { useWidgetValueStore } from '@/stores/widgetValueStore'
|
||||
import { asGraphId, widgetEntityId } from '@/world/entityIds'
|
||||
import {
|
||||
createTestSubgraph,
|
||||
createTestSubgraphData,
|
||||
@@ -296,9 +297,8 @@ describe('Graph Clearing and Callbacks', () => {
|
||||
})
|
||||
|
||||
const widgetValueStore = useWidgetValueStore()
|
||||
widgetValueStore.registerWidget(graphId, {
|
||||
nodeId: '10' as NodeId,
|
||||
name: 'seed',
|
||||
const seedId = widgetEntityId(asGraphId(graphId), '10' as NodeId, 'seed')
|
||||
widgetValueStore.registerWidget(seedId, {
|
||||
type: 'number',
|
||||
value: 1,
|
||||
options: {},
|
||||
@@ -307,7 +307,7 @@ describe('Graph Clearing and Callbacks', () => {
|
||||
disabled: undefined
|
||||
})
|
||||
|
||||
expect(widgetValueStore.getWidget(graphId, '10' as NodeId, 'seed')).toEqual(
|
||||
expect(widgetValueStore.getWidget(seedId)).toEqual(
|
||||
expect.objectContaining({ value: 1 })
|
||||
)
|
||||
expect(
|
||||
@@ -316,9 +316,7 @@ describe('Graph Clearing and Callbacks', () => {
|
||||
|
||||
graph.clear()
|
||||
|
||||
expect(
|
||||
widgetValueStore.getWidget(graphId, '10' as NodeId, 'seed')
|
||||
).toBeUndefined()
|
||||
expect(widgetValueStore.getWidget(seedId)).toBeUndefined()
|
||||
expect(previewExposureStore.getExposures(graphId, `${graphId}:1`)).toEqual(
|
||||
[]
|
||||
)
|
||||
|
||||
@@ -45,7 +45,7 @@ import { parseProxyWidgetErrorQuarantine } from '@/core/schemas/proxyWidgetQuara
|
||||
import { useDomWidgetStore } from '@/stores/domWidgetStore'
|
||||
import { usePreviewExposureStore } from '@/stores/previewExposureStore'
|
||||
import { createNodeLocatorId } from '@/types/nodeIdentification'
|
||||
import { readWidgetValue } from '@/world/widgetValueIO'
|
||||
import { useWidgetValueStore } from '@/stores/widgetValueStore'
|
||||
|
||||
import { ExecutableNodeDTO } from './ExecutableNodeDTO'
|
||||
import type { ExecutableLGraphNode, ExecutionId } from './ExecutableNodeDTO'
|
||||
@@ -1088,10 +1088,11 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
|
||||
)
|
||||
}
|
||||
|
||||
const widgetValueStore = useWidgetValueStore()
|
||||
const widgetValues = this.inputs.flatMap((input) => {
|
||||
const widget = input._widget
|
||||
if (!widget || !isPromotedWidgetView(widget)) return []
|
||||
const value = readWidgetValue(widget.entityId)
|
||||
const value = widgetValueStore.getWidget(widget.entityId)?.value
|
||||
return [isWidgetValue(value) ? value : undefined]
|
||||
})
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ import { usePreviewExposureStore } from '@/stores/previewExposureStore'
|
||||
import { useWidgetValueStore } from '@/stores/widgetValueStore'
|
||||
import { createNodeLocatorId } from '@/types/nodeIdentification'
|
||||
import { graphToPrompt } from '@/utils/executionUtil'
|
||||
import { asGraphId, nodeEntityId, widgetEntityId } from '@/world/entityIds'
|
||||
|
||||
import {
|
||||
createEventCapture,
|
||||
@@ -588,13 +589,18 @@ describe('SubgraphWidgetPromotion', () => {
|
||||
const hostNode = createTestSubgraphNode(subgraph)
|
||||
const hostWidget = hostNode.widgets[0]
|
||||
expectPromotedWidgetView(hostWidget)
|
||||
useWidgetValueStore().registerWidget(hostNode.rootGraph.id, {
|
||||
nodeId: hostNode.id,
|
||||
name: hostWidget.name,
|
||||
type: hostWidget.type,
|
||||
value: 99,
|
||||
options: {}
|
||||
})
|
||||
useWidgetValueStore().registerWidget(
|
||||
widgetEntityId(
|
||||
asGraphId(hostNode.rootGraph.id),
|
||||
hostNode.id,
|
||||
hostWidget.name
|
||||
),
|
||||
{
|
||||
type: hostWidget.type,
|
||||
value: 99,
|
||||
options: {}
|
||||
}
|
||||
)
|
||||
hostNode.serialize()
|
||||
|
||||
expect(interiorWidget.value).toBe(42)
|
||||
@@ -944,8 +950,10 @@ describe('SubgraphWidgetPromotion', () => {
|
||||
if (c.expect.storeSeedValue !== undefined) {
|
||||
expect(
|
||||
useWidgetValueStore()
|
||||
.getNodeWidgets(host.rootGraph.id, host.id)
|
||||
.find((entry) => entry.name === 'seed')?.value
|
||||
.getNodeWidgetsByName(
|
||||
nodeEntityId(asGraphId(host.rootGraph.id), host.id)
|
||||
)
|
||||
.get('seed')?.value
|
||||
).toBe(c.expect.storeSeedValue)
|
||||
}
|
||||
})
|
||||
@@ -1006,13 +1014,14 @@ describe('SubgraphWidgetPromotion', () => {
|
||||
const host = createTestSubgraphNode(subgraph)
|
||||
const widgetStore = useWidgetValueStore()
|
||||
for (const { node, widget } of sources) {
|
||||
widgetStore.registerWidget(host.rootGraph.id, {
|
||||
nodeId: node.id,
|
||||
name: widget.name,
|
||||
type: widget.type,
|
||||
value: `${node.title} value`,
|
||||
options: {}
|
||||
})
|
||||
widgetStore.registerWidget(
|
||||
widgetEntityId(asGraphId(host.rootGraph.id), node.id, widget.name),
|
||||
{
|
||||
type: widget.type,
|
||||
value: `${node.title} value`,
|
||||
options: {}
|
||||
}
|
||||
)
|
||||
}
|
||||
reorderSubgraphInputsByName(host, ['second', 'first'])
|
||||
expect(host.serialize().widgets_values).toBeUndefined()
|
||||
@@ -1032,13 +1041,18 @@ describe('SubgraphWidgetPromotion', () => {
|
||||
|
||||
const host = createTestSubgraphNode(subgraph, { id: 101 })
|
||||
const widgetStore = useWidgetValueStore()
|
||||
widgetStore.registerWidget(host.rootGraph.id, {
|
||||
nodeId: interiorNode.id,
|
||||
name: interiorWidget.name,
|
||||
type: interiorWidget.type,
|
||||
value: 'source fallback',
|
||||
options: {}
|
||||
})
|
||||
widgetStore.registerWidget(
|
||||
widgetEntityId(
|
||||
asGraphId(host.rootGraph.id),
|
||||
interiorNode.id,
|
||||
interiorWidget.name
|
||||
),
|
||||
{
|
||||
type: interiorWidget.type,
|
||||
value: 'source fallback',
|
||||
options: {}
|
||||
}
|
||||
)
|
||||
const serialized = host.serialize()
|
||||
expect(serialized.widgets_values).toBeUndefined()
|
||||
|
||||
@@ -1047,7 +1061,9 @@ describe('SubgraphWidgetPromotion', () => {
|
||||
reloaded.configure(serialized)
|
||||
|
||||
expect(
|
||||
widgetStore.getNodeWidgets(reloaded.rootGraph.id, reloaded.id)
|
||||
widgetStore.getNodeWidgets(
|
||||
nodeEntityId(asGraphId(reloaded.rootGraph.id), reloaded.id)
|
||||
)
|
||||
).toEqual([])
|
||||
expect(reloaded.serialize().widgets_values).toBeUndefined()
|
||||
})
|
||||
@@ -1073,14 +1089,27 @@ describe('SubgraphWidgetPromotion', () => {
|
||||
expectPromotedWidgetView(first)
|
||||
expectPromotedWidgetView(second)
|
||||
expect(
|
||||
widgetStore.getWidget(reloaded.rootGraph.id, reloaded.id, first.name)
|
||||
widgetStore.getWidget(
|
||||
widgetEntityId(
|
||||
asGraphId(reloaded.rootGraph.id),
|
||||
reloaded.id,
|
||||
first.name
|
||||
)
|
||||
)
|
||||
).toBeUndefined()
|
||||
expect(
|
||||
widgetStore.getWidget(reloaded.rootGraph.id, reloaded.id, second.name)
|
||||
?.value
|
||||
widgetStore.getWidget(
|
||||
widgetEntityId(
|
||||
asGraphId(reloaded.rootGraph.id),
|
||||
reloaded.id,
|
||||
second.name
|
||||
)
|
||||
)?.value
|
||||
).toBe('second host value')
|
||||
expect(
|
||||
widgetStore.getNodeWidgets(reloaded.rootGraph.id, reloaded.id)
|
||||
widgetStore.getNodeWidgets(
|
||||
nodeEntityId(asGraphId(reloaded.rootGraph.id), reloaded.id)
|
||||
)
|
||||
).toHaveLength(1)
|
||||
expect(reloaded.serialize().widgets_values).toEqual([
|
||||
undefined,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Bounds } from '@/renderer/core/layout/types'
|
||||
import type { CurveData } from '@/components/curve/types'
|
||||
import type { WidgetEntityId } from '@/world/entityIds'
|
||||
import type { WidgetState } from '@/world/widgets/widgetState'
|
||||
|
||||
import type {
|
||||
CanvasColour,
|
||||
@@ -394,7 +395,7 @@ export interface IBaseWidget<
|
||||
TValue = boolean | number | string | object | undefined,
|
||||
TType extends string = string,
|
||||
TOptions extends IWidgetOptions = IWidgetOptions
|
||||
> {
|
||||
> extends WidgetState<TValue, TType, TOptions> {
|
||||
[symbol: symbol]: boolean
|
||||
|
||||
linkedWidgets?: IBaseWidget[]
|
||||
@@ -402,25 +403,9 @@ export interface IBaseWidget<
|
||||
readonly entityId?: WidgetEntityId
|
||||
|
||||
name: string
|
||||
options: TOptions
|
||||
|
||||
label?: string
|
||||
/** Widget type (see {@link TWidgetType}) */
|
||||
type: TType
|
||||
value?: TValue
|
||||
|
||||
/**
|
||||
* Whether the widget value is persisted in the workflow JSON
|
||||
* (`widgets_values`). Checked by {@link LGraphNode.serialize} and
|
||||
* {@link LGraphNode.configure}.
|
||||
*
|
||||
* This is distinct from {@link IWidgetOptions.serialize}, which controls
|
||||
* whether the value is included in the API prompt sent for execution.
|
||||
*
|
||||
* @default true
|
||||
* @see IWidgetOptions.serialize — API prompt inclusion
|
||||
*/
|
||||
serialize?: boolean
|
||||
|
||||
/**
|
||||
* The computed height of the widget. Used by customized node resize logic.
|
||||
@@ -444,15 +429,10 @@ export interface IBaseWidget<
|
||||
last_y?: number
|
||||
|
||||
width?: number
|
||||
/**
|
||||
* Whether the widget is disabled. Disabled widgets are rendered at half opacity.
|
||||
* See also {@link IBaseWidget.computedDisabled}.
|
||||
*/
|
||||
disabled?: boolean
|
||||
|
||||
/**
|
||||
* The disabled state used for rendering based on various conditions including
|
||||
* {@link IBaseWidget.disabled}.
|
||||
* {@link WidgetState.disabled}.
|
||||
* @readonly [Computed] This property is computed by the node.
|
||||
*/
|
||||
computedDisabled?: boolean
|
||||
|
||||
@@ -4,9 +4,23 @@ import { setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it } from 'vitest'
|
||||
|
||||
import { LGraph, LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type { NodeId } from '@/lib/litegraph/src/LGraphNode'
|
||||
import type { INumericWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
import { NumberWidget } from '@/lib/litegraph/src/widgets/NumberWidget'
|
||||
import { useWidgetValueStore } from '@/stores/widgetValueStore'
|
||||
import type { UUID } from '@/utils/uuid'
|
||||
import { asGraphId, widgetEntityId } from '@/world/entityIds'
|
||||
|
||||
function lookup(
|
||||
store: ReturnType<typeof useWidgetValueStore>,
|
||||
graphId: string,
|
||||
nodeId: NodeId,
|
||||
name: string
|
||||
) {
|
||||
return store.getWidget(
|
||||
widgetEntityId(asGraphId(graphId as UUID), nodeId, name)
|
||||
)
|
||||
}
|
||||
|
||||
function createTestWidget(
|
||||
node: LGraphNode,
|
||||
@@ -95,7 +109,7 @@ describe('BaseWidget store integration', () => {
|
||||
widget.disabled = true
|
||||
widget.advanced = true
|
||||
|
||||
const state = store.getWidget(graph.id, 1, 'writeWidget')
|
||||
const state = lookup(store, graph.id, 1, 'writeWidget')
|
||||
expect(state?.label).toBe('Updated Label')
|
||||
expect(state?.disabled).toBe(true)
|
||||
|
||||
@@ -108,9 +122,9 @@ describe('BaseWidget store integration', () => {
|
||||
widget.setNodeId(1)
|
||||
|
||||
widget.value = 99
|
||||
expect(store.getWidget(graph.id, 1, 'valueWidget')?.value).toBe(99)
|
||||
expect(lookup(store, graph.id, 1, 'valueWidget')?.value).toBe(99)
|
||||
|
||||
const state = store.getWidget(graph.id, 1, 'valueWidget')!
|
||||
const state = lookup(store, graph.id, 1, 'valueWidget')!
|
||||
state.value = 55
|
||||
expect(widget.value).toBe(55)
|
||||
})
|
||||
@@ -128,10 +142,8 @@ describe('BaseWidget store integration', () => {
|
||||
})
|
||||
widget.setNodeId(1)
|
||||
|
||||
const state = store.getWidget(graph.id, 1, 'autoRegWidget')
|
||||
const state = lookup(store, graph.id, 1, 'autoRegWidget')
|
||||
expect(state).toBeDefined()
|
||||
expect(state?.nodeId).toBe(1)
|
||||
expect(state?.name).toBe('autoRegWidget')
|
||||
expect(state?.type).toBe('number')
|
||||
expect(state?.value).toBe(100)
|
||||
expect(state?.label).toBe('Auto Label')
|
||||
@@ -146,7 +158,7 @@ describe('BaseWidget store integration', () => {
|
||||
const widget = createTestWidget(node, { name: 'defaultsWidget' })
|
||||
widget.setNodeId(1)
|
||||
|
||||
const state = store.getWidget(graph.id, 1, 'defaultsWidget')
|
||||
const state = lookup(store, graph.id, 1, 'defaultsWidget')
|
||||
expect(state).toBeDefined()
|
||||
expect(state?.disabled).toBe(false)
|
||||
expect(state?.label).toBeUndefined()
|
||||
@@ -159,7 +171,7 @@ describe('BaseWidget store integration', () => {
|
||||
const widget = createTestWidget(node, { name: 'valuesWidget', value: 77 })
|
||||
widget.setNodeId(1)
|
||||
|
||||
expect(store.getWidget(graph.id, 1, 'valuesWidget')?.value).toBe(77)
|
||||
expect(lookup(store, graph.id, 1, 'valuesWidget')?.value).toBe(77)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -177,20 +189,20 @@ describe('BaseWidget store integration', () => {
|
||||
get() {
|
||||
const graphId = widget.node.graph?.rootGraph.id
|
||||
if (!graphId) return defaultValue
|
||||
const state = store.getWidget(graphId, node.id, 'system_prompt')
|
||||
const state = lookup(store, graphId, node.id, 'system_prompt')
|
||||
return (state?.value as string) ?? defaultValue
|
||||
},
|
||||
set(v: string) {
|
||||
const graphId = widget.node.graph?.rootGraph.id
|
||||
if (!graphId) return
|
||||
const state = store.getWidget(graphId, node.id, 'system_prompt')
|
||||
const state = lookup(store, graphId, node.id, 'system_prompt')
|
||||
if (state) state.value = v
|
||||
}
|
||||
})
|
||||
|
||||
widget.setNodeId(node.id)
|
||||
|
||||
const state = store.getWidget(graph.id, node.id, 'system_prompt')
|
||||
const state = lookup(store, graph.id, node.id, 'system_prompt')
|
||||
expect(state?.value).toBe(defaultValue)
|
||||
})
|
||||
})
|
||||
@@ -211,7 +223,7 @@ describe('BaseWidget store integration', () => {
|
||||
|
||||
widget.disabled = undefined
|
||||
|
||||
const state = store.getWidget(graph.id, 1, 'testWidget')
|
||||
const state = lookup(store, graph.id, 1, 'testWidget')
|
||||
expect(state?.disabled).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -20,7 +20,7 @@ import type {
|
||||
import type { WidgetState } from '@/stores/widgetValueStore'
|
||||
import { useWidgetValueStore } from '@/stores/widgetValueStore'
|
||||
import type { WidgetEntityId } from '@/world/entityIds'
|
||||
import { widgetEntityId } from '@/world/entityIds'
|
||||
import { deriveWidgetEntityId } from '@/world/entityIds'
|
||||
|
||||
export interface DrawWidgetOptions {
|
||||
/** The width of the node where this widget will be displayed. */
|
||||
@@ -85,8 +85,8 @@ export abstract class BaseWidget<TWidget extends IBaseWidget = IBaseWidget>
|
||||
computedDisabled?: boolean
|
||||
tooltip?: string
|
||||
|
||||
private _state: Omit<WidgetState, 'nodeId'> &
|
||||
Partial<Pick<WidgetState, 'nodeId'>>
|
||||
private _state: WidgetState
|
||||
private _nodeId?: NodeId
|
||||
|
||||
get label(): string | undefined {
|
||||
return this._state.label
|
||||
@@ -133,10 +133,11 @@ export abstract class BaseWidget<TWidget extends IBaseWidget = IBaseWidget>
|
||||
}
|
||||
|
||||
get entityId(): WidgetEntityId | undefined {
|
||||
const graphId = this.node.graph?.rootGraph.id
|
||||
const nodeId = this._state.nodeId
|
||||
if (!graphId || nodeId === undefined) return undefined
|
||||
return widgetEntityId(graphId, nodeId, this.name)
|
||||
return deriveWidgetEntityId(
|
||||
this.node.graph?.rootGraph.id,
|
||||
this._nodeId,
|
||||
this.name
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -144,16 +145,16 @@ export abstract class BaseWidget<TWidget extends IBaseWidget = IBaseWidget>
|
||||
* Once set, value reads/writes will be delegated to the store.
|
||||
*/
|
||||
setNodeId(nodeId: NodeId): void {
|
||||
const graphId = this.node.graph?.rootGraph.id
|
||||
if (!graphId) return
|
||||
this._nodeId = nodeId
|
||||
const widgetId = this.entityId
|
||||
if (!widgetId) return
|
||||
|
||||
this._state = useWidgetValueStore().registerWidget(graphId, {
|
||||
this._state = useWidgetValueStore().registerWidget(widgetId, {
|
||||
...this._state,
|
||||
// BaseWidget: this.value getter returns this._state.value. So value: this.value === value: this._state.value.
|
||||
// BaseDOMWidgetImpl: this.value getter returns options.getValue?.() ?? ''. Resolves the correct initial value instead of undefined.
|
||||
// I.e., calls overriden getter -> options.getValue() -> correct value (https://github.com/Comfy-Org/ComfyUI_frontend/issues/9194).
|
||||
value: this.value,
|
||||
nodeId
|
||||
value: this.value
|
||||
})
|
||||
}
|
||||
|
||||
@@ -202,7 +203,6 @@ export abstract class BaseWidget<TWidget extends IBaseWidget = IBaseWidget>
|
||||
Object.assign(this, safeValues)
|
||||
|
||||
this._state = {
|
||||
name: this.name,
|
||||
type: this.type as TWidgetType,
|
||||
value,
|
||||
label,
|
||||
|
||||
@@ -294,16 +294,17 @@ describe('NodeWidgets', () => {
|
||||
|
||||
const { container } = renderComponent(nodeData)
|
||||
const widgetValueStore = useWidgetValueStore()
|
||||
widgetValueStore.registerWidget('graph-test', {
|
||||
nodeId: 'test_node',
|
||||
name: 'test_widget',
|
||||
type: 'combo',
|
||||
value: 'value',
|
||||
options: { hidden: true },
|
||||
label: undefined,
|
||||
serialize: true,
|
||||
disabled: false
|
||||
})
|
||||
widgetValueStore.registerWidget(
|
||||
widgetEntityId(GRAPH_ID, 'test_node', 'test_widget'),
|
||||
{
|
||||
type: 'combo',
|
||||
value: 'value',
|
||||
options: { hidden: true },
|
||||
label: undefined,
|
||||
serialize: true,
|
||||
disabled: false
|
||||
}
|
||||
)
|
||||
|
||||
await nextTick()
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import type { NodeBadgeProps } from '@/renderer/extensions/vueNodes/components/NodeBadge.vue'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useWidgetValueStore } from '@/stores/widgetValueStore'
|
||||
import { deriveWidgetEntityId } from '@/world/entityIds'
|
||||
import { useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
import { NodeBadgeMode } from '@/types/nodeSource'
|
||||
|
||||
@@ -40,13 +41,13 @@ export function trackNodePrice(node: TrackableNode) {
|
||||
|
||||
// Access only the widget values that affect pricing (from widgetValueStore)
|
||||
const relevantNames = getRelevantWidgetNames(node.type)
|
||||
const widgetStore = useWidgetValueStore()
|
||||
const graphId = app.canvas?.graph?.rootGraph.id
|
||||
if (relevantNames.length > 0 && node.id != null) {
|
||||
const widgetValueStore = useWidgetValueStore()
|
||||
for (const name of relevantNames) {
|
||||
// Access value from store to create reactive dependency
|
||||
if (!graphId) continue
|
||||
void widgetStore.getWidget(graphId, node.id, name)?.value
|
||||
const entityId = deriveWidgetEntityId(graphId, node.id, name)
|
||||
if (entityId) void widgetValueStore.getWidget(entityId)?.value
|
||||
}
|
||||
}
|
||||
// Access input connections for regular inputs
|
||||
@@ -142,13 +143,13 @@ export function usePartitionedBadges(nodeData: VueNodeData) {
|
||||
if (isDynamicPricing.value) {
|
||||
// Access only the widget values that affect pricing (from widgetValueStore)
|
||||
const relevantNames = relevantPricingWidgets.value
|
||||
const widgetStore = useWidgetValueStore()
|
||||
const graphId = app.canvas?.graph?.rootGraph.id
|
||||
if (relevantNames.length > 0 && nodeData?.id != null) {
|
||||
const widgetValueStore = useWidgetValueStore()
|
||||
for (const name of relevantNames) {
|
||||
// Access value from store to create reactive dependency
|
||||
if (!graphId) continue
|
||||
void widgetStore.getWidget(graphId, nodeData.id, name)?.value
|
||||
const entityId = deriveWidgetEntityId(graphId, nodeData.id, name)
|
||||
if (entityId) void widgetValueStore.getWidget(entityId)?.value
|
||||
}
|
||||
}
|
||||
// Access input connections for regular inputs
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
import { useExecutionErrorStore } from '@/stores/executionErrorStore'
|
||||
import { useMissingModelStore } from '@/platform/missingModel/missingModelStore'
|
||||
import { useWidgetValueStore } from '@/stores/widgetValueStore'
|
||||
import { widgetEntityId } from '@/world/entityIds'
|
||||
import { deriveWidgetEntityId, widgetEntityId } from '@/world/entityIds'
|
||||
|
||||
const GRAPH_ID = 'graph-test'
|
||||
|
||||
@@ -432,13 +432,14 @@ describe('createWidgetUpdateHandler (via computeProcessedWidgets)', () => {
|
||||
callback
|
||||
})
|
||||
|
||||
useWidgetValueStore().registerWidget(GRAPH_ID, {
|
||||
nodeId: NODE_ID,
|
||||
name: 'seed',
|
||||
type: 'combo',
|
||||
value: 0,
|
||||
options: {}
|
||||
})
|
||||
useWidgetValueStore().registerWidget(
|
||||
widgetEntityId(GRAPH_ID, NODE_ID, 'seed'),
|
||||
{
|
||||
type: 'combo',
|
||||
value: 0,
|
||||
options: {}
|
||||
}
|
||||
)
|
||||
|
||||
const [processed] = processWidgets([widget])
|
||||
processed.updateHandler(42)
|
||||
@@ -466,21 +467,47 @@ describe('createWidgetUpdateHandler (via computeProcessedWidgets)', () => {
|
||||
nodeId: NODE_ID
|
||||
})
|
||||
|
||||
useWidgetValueStore().registerWidget(GRAPH_ID, {
|
||||
nodeId: NODE_ID,
|
||||
name: 'seed',
|
||||
type: 'combo',
|
||||
value: 0,
|
||||
options: {}
|
||||
})
|
||||
useWidgetValueStore().registerWidget(
|
||||
widgetEntityId(GRAPH_ID, NODE_ID, 'seed'),
|
||||
{
|
||||
type: 'combo',
|
||||
value: 0,
|
||||
options: {}
|
||||
}
|
||||
)
|
||||
|
||||
const [processed] = processWidgets([widget])
|
||||
processed.updateHandler(99)
|
||||
|
||||
const state = useWidgetValueStore().getWidget(GRAPH_ID, NODE_ID, 'seed')
|
||||
const entityId = deriveWidgetEntityId(GRAPH_ID, NODE_ID, 'seed')
|
||||
const state = entityId
|
||||
? useWidgetValueStore().getWidget(entityId)
|
||||
: undefined
|
||||
expect(state?.value).toBe(99)
|
||||
})
|
||||
|
||||
it('uses slotName for fallback widget-state lookup when entityId is absent', () => {
|
||||
const widget = createMockWidget({
|
||||
name: 'source_seed',
|
||||
slotName: 'host_seed',
|
||||
nodeId: NODE_ID,
|
||||
entityId: undefined
|
||||
})
|
||||
|
||||
useWidgetValueStore().registerWidget(
|
||||
widgetEntityId(GRAPH_ID, NODE_ID, 'host_seed'),
|
||||
{
|
||||
type: 'combo',
|
||||
value: 42,
|
||||
options: {}
|
||||
}
|
||||
)
|
||||
|
||||
const [processed] = processWidgets([widget])
|
||||
|
||||
expect(processed.value).toBe(42)
|
||||
})
|
||||
|
||||
it('clears execution errors on update', () => {
|
||||
const widget = createMockWidget({
|
||||
name: 'seed',
|
||||
|
||||
@@ -25,14 +25,11 @@ import {
|
||||
} from '@/renderer/extensions/vueNodes/widgets/registry/widgetRegistry'
|
||||
import { nodeTypeValidForApp } from '@/stores/appModeStore'
|
||||
import type { WidgetState } from '@/stores/widgetValueStore'
|
||||
import {
|
||||
stripGraphPrefix,
|
||||
useWidgetValueStore
|
||||
} from '@/stores/widgetValueStore'
|
||||
import { useWidgetValueStore } from '@/stores/widgetValueStore'
|
||||
import { useMissingModelStore } from '@/platform/missingModel/missingModelStore'
|
||||
import { useExecutionErrorStore } from '@/stores/executionErrorStore'
|
||||
import { deriveWidgetEntityId } from '@/world/entityIds'
|
||||
import type { WidgetEntityId } from '@/world/entityIds'
|
||||
import { getWidgetState } from '@/world/widgetValueIO'
|
||||
import type { LGraph } from '@/lib/litegraph/src/litegraph'
|
||||
import type {
|
||||
LinkedUpstreamInfo,
|
||||
@@ -77,6 +74,15 @@ interface ComputeProcessedWidgetsOptions {
|
||||
ui: WidgetUiCallbacks
|
||||
}
|
||||
|
||||
/**
|
||||
* Strips graph-scope prefix segments from a node id (e.g. `outer:inner:42`
|
||||
* → `42`) so nested node renders get stable DOM identity keys. Not for
|
||||
* widget value lookup — that routes through {@link WidgetEntityId}.
|
||||
*/
|
||||
function extractRawNodeId(scopedId: string | number): string {
|
||||
return String(scopedId).replace(/^(.*:)+/, '')
|
||||
}
|
||||
|
||||
function createWidgetUpdateHandler(
|
||||
widgetState: WidgetState | undefined,
|
||||
widget: SafeWidgetData,
|
||||
@@ -135,10 +141,10 @@ export function getWidgetIdentity(
|
||||
const slotNameForIdentity = widget.slotName ?? widget.name
|
||||
const hostNodeIdRoot =
|
||||
nodeId !== undefined && nodeId !== ''
|
||||
? `node:${String(stripGraphPrefix(nodeId))}`
|
||||
? `node:${String(extractRawNodeId(nodeId))}`
|
||||
: undefined
|
||||
const stableIdentityRoot = widget.nodeId
|
||||
? `node:${String(stripGraphPrefix(widget.nodeId))}`
|
||||
? `node:${String(extractRawNodeId(widget.nodeId))}`
|
||||
: widget.sourceExecutionId
|
||||
? `exec:${widget.sourceExecutionId}`
|
||||
: hostNodeIdRoot
|
||||
@@ -198,15 +204,19 @@ export function computeProcessedWidgets({
|
||||
if (!shouldRenderAsVue(widget)) continue
|
||||
|
||||
const identity = getWidgetIdentity(widget, nodeId, index)
|
||||
const widgetState = widget.entityId
|
||||
? getWidgetState(widget.entityId)
|
||||
: graphId
|
||||
? widgetValueStore.getWidget(
|
||||
graphId,
|
||||
String(stripGraphPrefix(widget.nodeId ?? nodeId ?? '')),
|
||||
widget.name
|
||||
)
|
||||
let widgetState: WidgetState | undefined
|
||||
if (widget.entityId) {
|
||||
widgetState = widgetValueStore.getWidget(widget.entityId)
|
||||
} else {
|
||||
const fallbackEntityId = deriveWidgetEntityId(
|
||||
graphId,
|
||||
String(extractRawNodeId(widget.nodeId ?? nodeId ?? '')),
|
||||
widget.slotName ?? widget.name
|
||||
)
|
||||
widgetState = fallbackEntityId
|
||||
? widgetValueStore.getWidget(fallbackEntityId)
|
||||
: undefined
|
||||
}
|
||||
const mergedOptions: IWidgetOptions = {
|
||||
...(widget.options ?? {}),
|
||||
...(widgetState?.options ?? {})
|
||||
@@ -254,7 +264,7 @@ export function computeProcessedWidgets({
|
||||
widgetState,
|
||||
identity: { renderKey }
|
||||
} of uniqueWidgets) {
|
||||
const bareWidgetId = String(stripGraphPrefix(widget.nodeId ?? nodeId ?? ''))
|
||||
const bareWidgetId = String(extractRawNodeId(widget.nodeId ?? nodeId ?? ''))
|
||||
|
||||
const vueComponent =
|
||||
getComponent(widget.type) ||
|
||||
@@ -318,7 +328,7 @@ export function computeProcessedWidgets({
|
||||
e,
|
||||
widget.name,
|
||||
widget.nodeId !== undefined
|
||||
? String(stripGraphPrefix(widget.nodeId))
|
||||
? String(extractRawNodeId(widget.nodeId))
|
||||
: undefined
|
||||
)
|
||||
}
|
||||
|
||||
@@ -10,11 +10,11 @@ import { Markdown as TiptapMarkdown } from 'tiptap-markdown'
|
||||
import { useChainCallback } from '@/composables/functional/useChainCallback'
|
||||
import { resolveNodeRootGraphId } from '@/lib/litegraph/src/litegraph'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { forwardMiddleButtonToCanvas } from '@/renderer/extensions/vueNodes/widgets/utils/forwardMiddleButtonToCanvas'
|
||||
import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import { app } from '@/scripts/app'
|
||||
import type { ComfyWidgetConstructorV2 } from '@/scripts/widgets'
|
||||
import { useWidgetValueStore } from '@/stores/widgetValueStore'
|
||||
import { deriveWidgetEntityId } from '@/world/entityIds'
|
||||
|
||||
// TODO: This widget manually syncs with widgetValueStore via getValue/setValue.
|
||||
// Consolidate with useStringWidget into shared helpers (domWidgetHelpers.ts).
|
||||
@@ -42,24 +42,35 @@ function addMarkdownWidget(
|
||||
editable: false
|
||||
})
|
||||
|
||||
const widgetStore = useWidgetValueStore()
|
||||
|
||||
const inputEl = editor.options.element as HTMLElement
|
||||
inputEl.classList.add('comfy-markdown')
|
||||
const textarea = document.createElement('textarea')
|
||||
inputEl.append(textarea)
|
||||
|
||||
const widgetValueStore = useWidgetValueStore()
|
||||
const widget = node.addDOMWidget(name, 'MARKDOWN', inputEl, {
|
||||
getValue(): string {
|
||||
const graphId = resolveNodeRootGraphId(node, app.rootGraph.id)
|
||||
const storedValue = widgetStore.getWidget(graphId, node.id, name)?.value
|
||||
const entityId = deriveWidgetEntityId(
|
||||
resolveNodeRootGraphId(node, app.rootGraph.id),
|
||||
node.id,
|
||||
name
|
||||
)
|
||||
const storedValue = entityId
|
||||
? widgetValueStore.getWidget(entityId)?.value
|
||||
: undefined
|
||||
return typeof storedValue === 'string' ? storedValue : textarea.value
|
||||
},
|
||||
setValue(v: string) {
|
||||
textarea.value = v
|
||||
editor.commands.setContent(v)
|
||||
const graphId = resolveNodeRootGraphId(node, app.rootGraph.id)
|
||||
const widgetState = widgetStore.getWidget(graphId, node.id, name)
|
||||
const entityId = deriveWidgetEntityId(
|
||||
resolveNodeRootGraphId(node, app.rootGraph.id),
|
||||
node.id,
|
||||
name
|
||||
)
|
||||
const widgetState = entityId
|
||||
? widgetValueStore.getWidget(entityId)
|
||||
: undefined
|
||||
if (widgetState) widgetState.value = v
|
||||
}
|
||||
})
|
||||
@@ -106,7 +117,29 @@ function addMarkdownWidget(
|
||||
signal
|
||||
})
|
||||
|
||||
forwardMiddleButtonToCanvas(inputEl, signal)
|
||||
inputEl.addEventListener(
|
||||
'pointerdown',
|
||||
(event) => {
|
||||
if (event.button === 1) app.canvas.processMouseDown(event)
|
||||
},
|
||||
{ signal }
|
||||
)
|
||||
|
||||
inputEl.addEventListener(
|
||||
'pointermove',
|
||||
(event) => {
|
||||
if ((event.buttons & 4) === 4) app.canvas.processMouseMove(event)
|
||||
},
|
||||
{ signal }
|
||||
)
|
||||
|
||||
inputEl.addEventListener(
|
||||
'pointerup',
|
||||
(event) => {
|
||||
if (event.button === 1) app.canvas.processMouseUp(event)
|
||||
},
|
||||
{ signal }
|
||||
)
|
||||
|
||||
widget.onRemove = useChainCallback(widget.onRemove, () => {
|
||||
controller.abort()
|
||||
|
||||
@@ -8,6 +8,7 @@ import { app } from '@/scripts/app'
|
||||
import type { ComfyWidgetConstructorV2 } from '@/scripts/widgets'
|
||||
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
import { useWidgetValueStore } from '@/stores/widgetValueStore'
|
||||
import { deriveWidgetEntityId } from '@/world/entityIds'
|
||||
|
||||
type TextPreviewCustomProps = Omit<
|
||||
InstanceType<typeof TextPreviewWidget>['$props'],
|
||||
@@ -25,6 +26,7 @@ export function useTextPreviewWidget(
|
||||
node: LGraphNode,
|
||||
inputSpec: InputSpec
|
||||
): IBaseWidget {
|
||||
const widgetValueStore = useWidgetValueStore()
|
||||
const widget = new ComponentWidgetImpl<
|
||||
string | object,
|
||||
TextPreviewCustomProps
|
||||
@@ -37,19 +39,26 @@ export function useTextPreviewWidget(
|
||||
nodeId: node.id
|
||||
},
|
||||
options: {
|
||||
getValue: () =>
|
||||
useWidgetValueStore().getWidget(
|
||||
getValue: () => {
|
||||
const entityId = deriveWidgetEntityId(
|
||||
resolveNodeRootGraphId(node, app.rootGraph.id),
|
||||
node.id,
|
||||
inputSpec.name
|
||||
)?.value ?? '',
|
||||
)
|
||||
const widgetState = entityId
|
||||
? widgetValueStore.getWidget(entityId)
|
||||
: undefined
|
||||
return widgetState?.value ?? ''
|
||||
},
|
||||
setValue: (value: string | object) => {
|
||||
const graphId = resolveNodeRootGraphId(node, app.rootGraph.id)
|
||||
const widgetState = useWidgetValueStore().getWidget(
|
||||
graphId,
|
||||
const entityId = deriveWidgetEntityId(
|
||||
resolveNodeRootGraphId(node, app.rootGraph.id),
|
||||
node.id,
|
||||
inputSpec.name
|
||||
)
|
||||
const widgetState = entityId
|
||||
? widgetValueStore.getWidget(entityId)
|
||||
: undefined
|
||||
if (widgetState)
|
||||
widgetState.value =
|
||||
typeof value === 'string' ? value : String(value)
|
||||
|
||||
@@ -3,12 +3,12 @@ import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { resolveNodeRootGraphId } from '@/lib/litegraph/src/litegraph'
|
||||
import { defineDeprecatedProperty } from '@/lib/litegraph/src/utils/feedback'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { forwardMiddleButtonToCanvas } from '@/renderer/extensions/vueNodes/widgets/utils/forwardMiddleButtonToCanvas'
|
||||
import { isStringInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import { app } from '@/scripts/app'
|
||||
import type { ComfyWidgetConstructorV2 } from '@/scripts/widgets'
|
||||
import { useWidgetValueStore } from '@/stores/widgetValueStore'
|
||||
import { deriveWidgetEntityId } from '@/world/entityIds'
|
||||
|
||||
const TRACKPAD_DETECTION_THRESHOLD = 50
|
||||
|
||||
@@ -19,7 +19,6 @@ function addMultilineWidget(
|
||||
name: string,
|
||||
opts: { defaultVal: string; placeholder?: string }
|
||||
) {
|
||||
const widgetStore = useWidgetValueStore()
|
||||
const inputEl = document.createElement('textarea')
|
||||
inputEl.className = 'comfy-multiline-input'
|
||||
inputEl.dataset.testid = 'dom-widget-textarea'
|
||||
@@ -27,17 +26,29 @@ function addMultilineWidget(
|
||||
inputEl.placeholder = opts.placeholder || name
|
||||
inputEl.spellcheck = useSettingStore().get('Comfy.TextareaWidget.Spellcheck')
|
||||
|
||||
const widgetValueStore = useWidgetValueStore()
|
||||
const widget = node.addDOMWidget(name, 'customtext', inputEl, {
|
||||
getValue(): string {
|
||||
const graphId = resolveNodeRootGraphId(node, app.rootGraph.id)
|
||||
const widgetState = widgetStore.getWidget(graphId, node.id, name)
|
||||
|
||||
const entityId = deriveWidgetEntityId(
|
||||
resolveNodeRootGraphId(node, app.rootGraph.id),
|
||||
node.id,
|
||||
name
|
||||
)
|
||||
const widgetState = entityId
|
||||
? widgetValueStore.getWidget(entityId)
|
||||
: undefined
|
||||
return (widgetState?.value as string) ?? inputEl.value
|
||||
},
|
||||
setValue(v: string) {
|
||||
inputEl.value = v
|
||||
const graphId = resolveNodeRootGraphId(node, app.rootGraph.id)
|
||||
const widgetState = widgetStore.getWidget(graphId, node.id, name)
|
||||
const entityId = deriveWidgetEntityId(
|
||||
resolveNodeRootGraphId(node, app.rootGraph.id),
|
||||
node.id,
|
||||
name
|
||||
)
|
||||
const widgetState = entityId
|
||||
? widgetValueStore.getWidget(entityId)
|
||||
: undefined
|
||||
if (widgetState) widgetState.value = v
|
||||
}
|
||||
})
|
||||
@@ -67,7 +78,30 @@ function addMultilineWidget(
|
||||
{ signal }
|
||||
)
|
||||
|
||||
forwardMiddleButtonToCanvas(inputEl, signal)
|
||||
// Allow middle mouse button panning
|
||||
inputEl.addEventListener(
|
||||
'pointerdown',
|
||||
(event: PointerEvent) => {
|
||||
if (event.button === 1) app.canvas.processMouseDown(event)
|
||||
},
|
||||
{ signal }
|
||||
)
|
||||
|
||||
inputEl.addEventListener(
|
||||
'pointermove',
|
||||
(event: PointerEvent) => {
|
||||
if ((event.buttons & 4) === 4) app.canvas.processMouseMove(event)
|
||||
},
|
||||
{ signal }
|
||||
)
|
||||
|
||||
inputEl.addEventListener(
|
||||
'pointerup',
|
||||
(event: PointerEvent) => {
|
||||
if (event.button === 1) app.canvas.processMouseUp(event)
|
||||
},
|
||||
{ signal }
|
||||
)
|
||||
|
||||
inputEl.addEventListener(
|
||||
'wheel',
|
||||
|
||||
@@ -4,15 +4,18 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { nextTick, reactive, ref, shallowRef } from 'vue'
|
||||
import type { MaybeRefOrGetter } from 'vue'
|
||||
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
||||
import type { LGraphNode, NodeId } from '@/lib/litegraph/src/LGraphNode'
|
||||
import type { GLSLRendererConfig } from '@/renderer/glsl/useGLSLRenderer'
|
||||
import { useGLSLPreview } from '@/renderer/glsl/useGLSLPreview'
|
||||
import { useWidgetValueStore } from '@/stores/widgetValueStore'
|
||||
import { asGraphId, widgetEntityId } from '@/world/entityIds'
|
||||
|
||||
type WidgetValueStoreStub = {
|
||||
_widgetMap: Map<string, { value: unknown }>
|
||||
}
|
||||
|
||||
const TEST_GRAPH_ID = 'test-graph-id'
|
||||
|
||||
const mockRendererFactory = vi.hoisted(() => {
|
||||
const init = vi.fn(() => true)
|
||||
const compileFragment = vi.fn(() => ({ success: true, log: '' }))
|
||||
@@ -78,12 +81,12 @@ vi.mock('@/stores/nodeOutputStore', () => ({
|
||||
|
||||
vi.mock('@/stores/widgetValueStore', () => {
|
||||
const widgetMap = new Map<string, { value: unknown }>()
|
||||
const getWidget = vi.fn((_graphId: string, _nodeId: string, name: string) =>
|
||||
widgetMap.get(name)
|
||||
)
|
||||
const getWidget = vi.fn((widgetId: string) => widgetMap.get(widgetId))
|
||||
const getNodeWidgets = vi.fn(() => [])
|
||||
return {
|
||||
useWidgetValueStore: () => ({
|
||||
getWidget,
|
||||
getNodeWidgets,
|
||||
_widgetMap: widgetMap
|
||||
})
|
||||
}
|
||||
@@ -102,7 +105,7 @@ vi.mock('@/utils/objectUrlUtil', () => ({
|
||||
}))
|
||||
|
||||
function createMockNode(overrides: Record<string, unknown> = {}): LGraphNode {
|
||||
const graph = { id: 'test-graph-id', rootGraph: { id: 'test-graph-id' } }
|
||||
const graph = { id: TEST_GRAPH_ID, rootGraph: { id: TEST_GRAPH_ID } }
|
||||
return fromAny<LGraphNode, unknown>({
|
||||
id: 1,
|
||||
type: 'GLSLShader',
|
||||
@@ -120,6 +123,18 @@ function wrapNode(
|
||||
return ref(node) as MaybeRefOrGetter<LGraphNode | null>
|
||||
}
|
||||
|
||||
function setWidget(
|
||||
store: WidgetValueStoreStub,
|
||||
node: LGraphNode,
|
||||
name: string,
|
||||
value: unknown
|
||||
) {
|
||||
store._widgetMap.set(
|
||||
widgetEntityId(asGraphId(TEST_GRAPH_ID), node.id as NodeId, name),
|
||||
{ value }
|
||||
)
|
||||
}
|
||||
|
||||
describe('useGLSLPreview', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
@@ -184,9 +199,7 @@ describe('useGLSLPreview', () => {
|
||||
const store = fromAny<WidgetValueStoreStub, unknown>(
|
||||
useWidgetValueStore()
|
||||
)
|
||||
store._widgetMap.set('fragment_shader', {
|
||||
value: 'void main() {}'
|
||||
})
|
||||
setWidget(store, node, 'fragment_shader', 'void main() {}')
|
||||
|
||||
const nodeRef = shallowRef<LGraphNode | null>(null)
|
||||
useGLSLPreview(nodeRef)
|
||||
@@ -248,9 +261,7 @@ describe('useGLSLPreview', () => {
|
||||
const store = fromAny<WidgetValueStoreStub, unknown>(
|
||||
useWidgetValueStore()
|
||||
)
|
||||
store._widgetMap.set('fragment_shader', {
|
||||
value: 'void main() {}'
|
||||
})
|
||||
setWidget(store, node, 'fragment_shader', 'void main() {}')
|
||||
|
||||
const nodeRef = shallowRef<LGraphNode | null>(null)
|
||||
const result = useGLSLPreview(nodeRef)
|
||||
@@ -306,9 +317,15 @@ describe('useGLSLPreview', () => {
|
||||
const store = fromAny<WidgetValueStoreStub, unknown>(
|
||||
useWidgetValueStore()
|
||||
)
|
||||
store._widgetMap.delete('fragment_shader')
|
||||
|
||||
const node = createMockNode()
|
||||
store._widgetMap.delete(
|
||||
widgetEntityId(
|
||||
asGraphId(TEST_GRAPH_ID),
|
||||
node.id as NodeId,
|
||||
'fragment_shader'
|
||||
)
|
||||
)
|
||||
mockNodeOutputs[String(node.id)] = {
|
||||
images: [{ filename: 'test.png', subfolder: '', type: 'temp' }]
|
||||
}
|
||||
@@ -327,32 +344,30 @@ describe('useGLSLPreview', () => {
|
||||
const store = fromAny<WidgetValueStoreStub, unknown>(
|
||||
useWidgetValueStore()
|
||||
)
|
||||
store._widgetMap.set('size_mode', { value: 'custom' })
|
||||
store._widgetMap.set('size_mode.width', { value: 800 })
|
||||
store._widgetMap.set('size_mode.height', { value: 600 })
|
||||
|
||||
const node = createMockNode()
|
||||
setWidget(store, node, 'size_mode', 'custom')
|
||||
setWidget(store, node, 'size_mode.width', 800)
|
||||
setWidget(store, node, 'size_mode.height', 600)
|
||||
await setupAndRender(node)
|
||||
|
||||
expect(mockRendererFactory.setResolution).toHaveBeenCalledWith(800, 600)
|
||||
|
||||
store._widgetMap.delete('size_mode')
|
||||
store._widgetMap.delete('size_mode.width')
|
||||
store._widgetMap.delete('size_mode.height')
|
||||
store._widgetMap.clear()
|
||||
})
|
||||
|
||||
it('uses default resolution when size_mode is not custom', async () => {
|
||||
const store = fromAny<WidgetValueStoreStub, unknown>(
|
||||
useWidgetValueStore()
|
||||
)
|
||||
store._widgetMap.set('size_mode', { value: 'from_input' })
|
||||
|
||||
const node = createMockNode()
|
||||
setWidget(store, node, 'size_mode', 'from_input')
|
||||
await setupAndRender(node)
|
||||
|
||||
expect(mockRendererFactory.setResolution).toHaveBeenCalledWith(512, 512)
|
||||
|
||||
store._widgetMap.delete('size_mode')
|
||||
store._widgetMap.clear()
|
||||
})
|
||||
|
||||
it('disposes renderer and cancels debounce on cleanup', async () => {
|
||||
|
||||
@@ -8,6 +8,7 @@ import type { UUID } from '@/utils/uuid'
|
||||
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
|
||||
import { useNodeOutputStore } from '@/stores/nodeOutputStore'
|
||||
import { useWidgetValueStore } from '@/stores/widgetValueStore'
|
||||
import { deriveWidgetEntityId } from '@/world/entityIds'
|
||||
|
||||
import { curveDataToFloatLUT } from '@/components/curve/curveUtils'
|
||||
import type { GLSLRendererConfig } from '@/renderer/glsl/useGLSLRenderer'
|
||||
@@ -121,8 +122,8 @@ function createInnerPreview(
|
||||
lastError: Ref<string | null>,
|
||||
isActiveOut: Ref<boolean>
|
||||
): () => void {
|
||||
const widgetValueStore = useWidgetValueStore()
|
||||
const nodeOutputStore = useNodeOutputStore()
|
||||
const widgetValueStore = useWidgetValueStore()
|
||||
const { nodeToNodeLocatorId } = useWorkflowStore()
|
||||
|
||||
let renderer: ReturnType<typeof useGLSLRenderer> | null = null
|
||||
@@ -194,18 +195,22 @@ function createInnerPreview(
|
||||
if (isGLSLNode.value) {
|
||||
const nId = nodeId.value
|
||||
if (nId == null) return undefined
|
||||
return widgetValueStore.getWidget(gId, nId, 'fragment_shader')?.value as
|
||||
| string
|
||||
| undefined
|
||||
const entityId = deriveWidgetEntityId(gId, nId, 'fragment_shader')
|
||||
return entityId
|
||||
? (widgetValueStore.getWidget(entityId)?.value as string | undefined)
|
||||
: undefined
|
||||
}
|
||||
|
||||
const inner = innerGLSLNode
|
||||
if (inner) {
|
||||
return widgetValueStore.getWidget(
|
||||
const entityId = deriveWidgetEntityId(
|
||||
gId,
|
||||
inner.id as NodeId,
|
||||
'fragment_shader'
|
||||
)?.value as string | undefined
|
||||
)
|
||||
return entityId
|
||||
? (widgetValueStore.getWidget(entityId)?.value as string | undefined)
|
||||
: undefined
|
||||
}
|
||||
|
||||
return undefined
|
||||
@@ -291,23 +296,16 @@ function createInnerPreview(
|
||||
: nodeId.value
|
||||
if (sizeModeNodeId == null) return null
|
||||
|
||||
const sizeMode = widgetValueStore.getWidget(
|
||||
gId,
|
||||
sizeModeNodeId,
|
||||
'size_mode'
|
||||
)
|
||||
const lookup = (name: string) => {
|
||||
const entityId = deriveWidgetEntityId(gId, sizeModeNodeId, name)
|
||||
return entityId ? widgetValueStore.getWidget(entityId) : undefined
|
||||
}
|
||||
|
||||
const sizeMode = lookup('size_mode')
|
||||
if (sizeMode?.value !== 'custom') return null
|
||||
|
||||
const widthWidget = widgetValueStore.getWidget(
|
||||
gId,
|
||||
sizeModeNodeId,
|
||||
'size_mode.width'
|
||||
)
|
||||
const heightWidget = widgetValueStore.getWidget(
|
||||
gId,
|
||||
sizeModeNodeId,
|
||||
'size_mode.height'
|
||||
)
|
||||
const widthWidget = lookup('size_mode.width')
|
||||
const heightWidget = lookup('size_mode.height')
|
||||
if (!widthWidget || !heightWidget) return null
|
||||
|
||||
return clampResolution(
|
||||
|
||||
@@ -6,6 +6,11 @@ import { SUBGRAPH_INPUT_ID } from '@/lib/litegraph/src/constants'
|
||||
import type { Subgraph } from '@/lib/litegraph/src/subgraph/Subgraph'
|
||||
import type { UUID } from '@/utils/uuid'
|
||||
import { useWidgetValueStore } from '@/stores/widgetValueStore'
|
||||
import {
|
||||
asGraphId,
|
||||
deriveWidgetEntityId,
|
||||
nodeEntityId
|
||||
} from '@/world/entityIds'
|
||||
|
||||
import { isCurveData } from '@/components/curve/curveUtils'
|
||||
import type { CurveData } from '@/components/curve/types'
|
||||
@@ -132,7 +137,10 @@ export function useGLSLUniforms(
|
||||
|
||||
if (subgraphSources) {
|
||||
return subgraphSources.map(({ nodeId: nId, widgetName, directValue }) => {
|
||||
const widget = widgetValueStore.getWidget(gId, nId, widgetName)
|
||||
const entityId = deriveWidgetEntityId(gId, nId, widgetName)
|
||||
const widget = entityId
|
||||
? widgetValueStore.getWidget(entityId)
|
||||
: undefined
|
||||
return coerce(widget?.value ?? directValue() ?? defaultValue)
|
||||
})
|
||||
}
|
||||
@@ -144,7 +152,8 @@ export function useGLSLUniforms(
|
||||
const values: T[] = []
|
||||
for (let i = 0; i < maxCount; i++) {
|
||||
const inputName = `${groupName}.${uniformPrefix}${i}`
|
||||
const widget = widgetValueStore.getWidget(gId, nId, inputName)
|
||||
const entityId = deriveWidgetEntityId(gId, nId, inputName)
|
||||
const widget = entityId ? widgetValueStore.getWidget(entityId) : undefined
|
||||
if (widget !== undefined) {
|
||||
values.push(coerce(widget.value))
|
||||
continue
|
||||
@@ -158,8 +167,7 @@ export function useGLSLUniforms(
|
||||
const upstreamNode = node.getInputNode(slot)
|
||||
if (!upstreamNode) break
|
||||
const upstreamWidgets = widgetValueStore.getNodeWidgets(
|
||||
gId,
|
||||
upstreamNode.id as NodeId
|
||||
nodeEntityId(asGraphId(gId), upstreamNode.id as NodeId)
|
||||
)
|
||||
if (
|
||||
upstreamWidgets.length === 0 ||
|
||||
@@ -214,7 +222,10 @@ export function useGLSLUniforms(
|
||||
if (sources && sources.length > 0) {
|
||||
return sources
|
||||
.map(({ nodeId: nId, widgetName, directValue }) => {
|
||||
const widget = widgetValueStore.getWidget(gId, nId, widgetName)
|
||||
const entityId = deriveWidgetEntityId(gId, nId, widgetName)
|
||||
const widget = entityId
|
||||
? widgetValueStore.getWidget(entityId)
|
||||
: undefined
|
||||
const value = widget?.value ?? directValue()
|
||||
return isCurveData(value) ? (value as CurveData) : null
|
||||
})
|
||||
@@ -230,7 +241,8 @@ export function useGLSLUniforms(
|
||||
for (let i = 0; i < max; i++) {
|
||||
const inputName = `curves.u_curve${i}`
|
||||
|
||||
const widget = widgetValueStore.getWidget(gId, nId, inputName)
|
||||
const entityId = deriveWidgetEntityId(gId, nId, inputName)
|
||||
const widget = entityId ? widgetValueStore.getWidget(entityId) : undefined
|
||||
if (widget && isCurveData(widget.value)) {
|
||||
values.push(widget.value as CurveData)
|
||||
continue
|
||||
@@ -243,8 +255,7 @@ export function useGLSLUniforms(
|
||||
if (!upstreamNode) break
|
||||
|
||||
const upstreamWidgets = widgetValueStore.getNodeWidgets(
|
||||
gId,
|
||||
upstreamNode.id as NodeId
|
||||
nodeEntityId(asGraphId(gId), upstreamNode.id as NodeId)
|
||||
)
|
||||
const curveWidget = upstreamWidgets.find((w) => isCurveData(w.value))
|
||||
if (!curveWidget) break
|
||||
|
||||
@@ -18,6 +18,7 @@ import { app } from '@/scripts/app'
|
||||
import { ChangeTracker } from '@/scripts/changeTracker'
|
||||
import { createMockChangeTracker } from '@/utils/__tests__/litegraphTestUtils'
|
||||
import type { WidgetEntityId } from '@/world/entityIds'
|
||||
import { widgetEntityId } from '@/world/entityIds'
|
||||
|
||||
const mockEmptyWorkflowDialog = vi.hoisted(() => {
|
||||
let lastOptions: { onEnterBuilder: () => void; onDismiss: () => void }
|
||||
@@ -127,16 +128,16 @@ function createWorkflowWithLinearData(
|
||||
}
|
||||
|
||||
const rootGraphId = '11111111-1111-4111-8111-111111111111'
|
||||
const entityPrompt = `${rootGraphId}:1:prompt` as WidgetEntityId
|
||||
const entitySeed = `${rootGraphId}:1:seed` as WidgetEntityId
|
||||
const entitySteps = `${rootGraphId}:1:steps` as WidgetEntityId
|
||||
const entityPrompt = widgetEntityId(rootGraphId, 1, 'prompt')
|
||||
const entitySeed = widgetEntityId(rootGraphId, 1, 'seed')
|
||||
const entitySteps = widgetEntityId(rootGraphId, 1, 'steps')
|
||||
|
||||
function nodeWithWidgets(id: number, widgetNames: string[]) {
|
||||
return fromAny<LGraphNode, unknown>({
|
||||
id,
|
||||
widgets: widgetNames.map((name) => ({
|
||||
name,
|
||||
entityId: `${rootGraphId}:${id}:${name}` as WidgetEntityId
|
||||
entityId: widgetEntityId(rootGraphId, id, name)
|
||||
}))
|
||||
})
|
||||
}
|
||||
@@ -803,6 +804,30 @@ describe('appModeStore', () => {
|
||||
describe('legacy selectedInput tuple migration', () => {
|
||||
const rootGraphId = '11111111-1111-4111-8111-111111111111'
|
||||
|
||||
it('migrates legacy unprefixed widget entity ids to canonical widget ids', () => {
|
||||
const legacyEntityId = `${rootGraphId}:42:seed`
|
||||
const canonicalEntityId = widgetEntityId(rootGraphId, 42, 'seed')
|
||||
const rootNode = fromAny<LGraphNode, unknown>({
|
||||
id: 42,
|
||||
widgets: [{ name: 'seed', entityId: canonicalEntityId }]
|
||||
})
|
||||
|
||||
vi.mocked(app.rootGraph).id = rootGraphId
|
||||
vi.mocked(app.rootGraph).nodes = [rootNode]
|
||||
vi.mocked(app.rootGraph).getNodeById = vi.fn(
|
||||
(id: NodeId | null | undefined) => (id == 42 ? rootNode : null)
|
||||
)
|
||||
|
||||
const result = store.pruneLinearData({
|
||||
inputs: [[legacyEntityId, 'seed', { height: 120 }]],
|
||||
outputs: []
|
||||
})
|
||||
|
||||
expect(result.inputs).toEqual([
|
||||
[canonicalEntityId, 'seed', { height: 120 }]
|
||||
])
|
||||
})
|
||||
|
||||
it('migrates legacy `(sourceNodeId, sourceWidgetName)` to the host promoted widget entity id', () => {
|
||||
const hostId = 5
|
||||
const sourceNodeId = 42
|
||||
|
||||
@@ -26,7 +26,11 @@ import {
|
||||
resolveNodeWidget
|
||||
} from '@/utils/litegraphUtil'
|
||||
import type { WidgetEntityId } from '@/world/entityIds'
|
||||
import { isWidgetEntityId, parseWidgetEntityId } from '@/world/entityIds'
|
||||
import {
|
||||
isWidgetEntityId,
|
||||
parseLegacyWidgetEntityId,
|
||||
parseWidgetEntityId
|
||||
} from '@/world/entityIds'
|
||||
|
||||
function findWidgetByEntityId(
|
||||
rootGraph: LGraph,
|
||||
@@ -105,6 +109,17 @@ export const useAppModeStore = defineStore('appMode', () => {
|
||||
return null
|
||||
}
|
||||
|
||||
if (typeof storedId === 'string') {
|
||||
const legacyId = parseLegacyWidgetEntityId(storedId, rootGraph.id)
|
||||
if (legacyId) {
|
||||
const node = rootGraph.getNodeById?.(legacyId.nodeId)
|
||||
const widget = node?.widgets?.find((w) => w.name === legacyId.name)
|
||||
const entityId =
|
||||
node && widget && getWidgetEntityIdForNode(node, widget)
|
||||
return entityId ? buildEntry(entityId, widgetName, config) : null
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof storedId === 'string' && storedId.includes(':')) {
|
||||
const [, widget] = resolveNodeWidget(storedId, widgetName)
|
||||
if (!widget?.entityId) return null
|
||||
|
||||
@@ -2,25 +2,33 @@ import { createTestingPinia } from '@pinia/testing'
|
||||
import { setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it } from 'vitest'
|
||||
|
||||
import type { NodeId } from '@/lib/litegraph/src/LGraphNode'
|
||||
import type { UUID } from '@/utils/uuid'
|
||||
import { asGraphId, nodeEntityId, widgetEntityId } from '@/world/entityIds'
|
||||
import type { WidgetEntityId } from '@/world/entityIds'
|
||||
|
||||
import type { WidgetState } from './widgetValueStore'
|
||||
import { useWidgetValueStore } from './widgetValueStore'
|
||||
|
||||
function widget<T>(
|
||||
nodeId: string,
|
||||
name: string,
|
||||
function widgetState<T>(
|
||||
type: string,
|
||||
value: T,
|
||||
extra: Partial<
|
||||
Omit<WidgetState<T>, 'nodeId' | 'name' | 'type' | 'value'>
|
||||
> = {}
|
||||
extra: Partial<Omit<WidgetState<T>, 'type' | 'value'>> = {}
|
||||
): WidgetState<T> {
|
||||
return { nodeId, name, type, value, options: {}, ...extra }
|
||||
return {
|
||||
type,
|
||||
value,
|
||||
options: {},
|
||||
...extra
|
||||
}
|
||||
}
|
||||
|
||||
describe('useWidgetValueStore', () => {
|
||||
const graphA = 'graph-a' as UUID
|
||||
const graphB = 'graph-b' as UUID
|
||||
const wid = (graphId: UUID, nodeId: NodeId, name: string): WidgetEntityId =>
|
||||
widgetEntityId(asGraphId(graphId), nodeId, name)
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePinia(createTestingPinia({ stubActions: false }))
|
||||
})
|
||||
@@ -28,35 +36,45 @@ describe('useWidgetValueStore', () => {
|
||||
describe('widgetState.value access', () => {
|
||||
it('getWidget returns undefined for unregistered widget', () => {
|
||||
const store = useWidgetValueStore()
|
||||
expect(store.getWidget(graphA, 'missing', 'widget')).toBeUndefined()
|
||||
expect(
|
||||
store.getWidget(wid(graphA, 'missing' as NodeId, 'widget'))
|
||||
).toBeUndefined()
|
||||
})
|
||||
|
||||
it('widgetState.value can be read and written directly', () => {
|
||||
const store = useWidgetValueStore()
|
||||
const state = store.registerWidget(
|
||||
graphA,
|
||||
widget('node-1', 'seed', 'number', 100)
|
||||
)
|
||||
const id = wid(graphA, 'node-1' as NodeId, 'seed')
|
||||
const state = store.registerWidget(id, widgetState('number', 100))
|
||||
expect(state.value).toBe(100)
|
||||
|
||||
state.value = 200
|
||||
expect(store.getWidget(graphA, 'node-1', 'seed')?.value).toBe(200)
|
||||
expect(store.getWidget(id)?.value).toBe(200)
|
||||
})
|
||||
|
||||
it('stores different value types', () => {
|
||||
const store = useWidgetValueStore()
|
||||
store.registerWidget(graphA, widget('node-1', 'text', 'string', 'hello'))
|
||||
store.registerWidget(graphA, widget('node-1', 'number', 'number', 42))
|
||||
store.registerWidget(graphA, widget('node-1', 'boolean', 'toggle', true))
|
||||
const node = 'node-1' as NodeId
|
||||
store.registerWidget(
|
||||
graphA,
|
||||
widget('node-1', 'array', 'combo', [1, 2, 3])
|
||||
wid(graphA, node, 'text'),
|
||||
widgetState('string', 'hello')
|
||||
)
|
||||
store.registerWidget(
|
||||
wid(graphA, node, 'number'),
|
||||
widgetState('number', 42)
|
||||
)
|
||||
store.registerWidget(
|
||||
wid(graphA, node, 'boolean'),
|
||||
widgetState('toggle', true)
|
||||
)
|
||||
store.registerWidget(
|
||||
wid(graphA, node, 'array'),
|
||||
widgetState('combo', [1, 2, 3])
|
||||
)
|
||||
|
||||
expect(store.getWidget(graphA, 'node-1', 'text')?.value).toBe('hello')
|
||||
expect(store.getWidget(graphA, 'node-1', 'number')?.value).toBe(42)
|
||||
expect(store.getWidget(graphA, 'node-1', 'boolean')?.value).toBe(true)
|
||||
expect(store.getWidget(graphA, 'node-1', 'array')?.value).toEqual([
|
||||
expect(store.getWidget(wid(graphA, node, 'text'))?.value).toBe('hello')
|
||||
expect(store.getWidget(wid(graphA, node, 'number'))?.value).toBe(42)
|
||||
expect(store.getWidget(wid(graphA, node, 'boolean'))?.value).toBe(true)
|
||||
expect(store.getWidget(wid(graphA, node, 'array'))?.value).toEqual([
|
||||
1, 2, 3
|
||||
])
|
||||
})
|
||||
@@ -66,15 +84,13 @@ describe('useWidgetValueStore', () => {
|
||||
it('registers a widget with minimal properties', () => {
|
||||
const store = useWidgetValueStore()
|
||||
const state = store.registerWidget(
|
||||
graphA,
|
||||
widget('node-1', 'seed', 'number', 12345)
|
||||
wid(graphA, 'node-1' as NodeId, 'seed'),
|
||||
widgetState('number', 12345)
|
||||
)
|
||||
|
||||
expect(state.nodeId).toBe('node-1')
|
||||
expect(state.name).toBe('seed')
|
||||
expect(state.type).toBe('number')
|
||||
expect(state.value).toBe(12345)
|
||||
expect(state.disabled).toBeUndefined()
|
||||
expect(state.disabled).toBe(false)
|
||||
expect(state.serialize).toBeUndefined()
|
||||
expect(state.options).toEqual({})
|
||||
})
|
||||
@@ -82,8 +98,8 @@ describe('useWidgetValueStore', () => {
|
||||
it('registers a widget with all properties', () => {
|
||||
const store = useWidgetValueStore()
|
||||
const state = store.registerWidget(
|
||||
graphA,
|
||||
widget('node-1', 'prompt', 'string', 'test', {
|
||||
wid(graphA, 'node-1' as NodeId, 'prompt'),
|
||||
widgetState('string', 'test', {
|
||||
label: 'Prompt Text',
|
||||
disabled: true,
|
||||
serialize: false,
|
||||
@@ -96,84 +112,189 @@ describe('useWidgetValueStore', () => {
|
||||
expect(state.serialize).toBe(false)
|
||||
expect(state.options).toEqual({ multiline: true })
|
||||
})
|
||||
|
||||
it('overwrites existing widget state when registerWidget is called twice', () => {
|
||||
const store = useWidgetValueStore()
|
||||
const id = wid(graphA, 'node-1' as NodeId, 'seed')
|
||||
const first = store.registerWidget(id, widgetState('number', 11))
|
||||
first.value = 99
|
||||
|
||||
store.registerWidget(id, widgetState('number', 11))
|
||||
expect(store.getWidget(id)?.value).toBe(11)
|
||||
})
|
||||
|
||||
it('register-if-absent pattern preserves existing state', () => {
|
||||
// Captures the idempotency guarantee that the prior IO helper used to
|
||||
// provide: callers that want non-destructive init must check getWidget
|
||||
// first.
|
||||
const store = useWidgetValueStore()
|
||||
const id = wid(graphA, 'node-1' as NodeId, 'seed')
|
||||
store.registerWidget(id, widgetState('number', 11))
|
||||
const first = store.getWidget(id)!
|
||||
first.value = 99
|
||||
|
||||
const existing = store.getWidget(id)
|
||||
if (!existing) store.registerWidget(id, widgetState('number', 11))
|
||||
|
||||
expect(store.getWidget(id)?.value).toBe(99)
|
||||
})
|
||||
})
|
||||
|
||||
describe('widget getters', () => {
|
||||
it('getWidget returns widget state', () => {
|
||||
const store = useWidgetValueStore()
|
||||
store.registerWidget(graphA, widget('node-1', 'seed', 'number', 100))
|
||||
const id = wid(graphA, 'node-1' as NodeId, 'seed')
|
||||
store.registerWidget(id, widgetState('number', 100))
|
||||
|
||||
const state = store.getWidget(graphA, 'node-1', 'seed')
|
||||
const state = store.getWidget(id)
|
||||
expect(state).toBeDefined()
|
||||
expect(state?.name).toBe('seed')
|
||||
expect(state?.type).toBe('number')
|
||||
expect(state?.value).toBe(100)
|
||||
})
|
||||
|
||||
it('getWidget returns undefined for missing widget', () => {
|
||||
const store = useWidgetValueStore()
|
||||
expect(store.getWidget(graphA, 'missing', 'widget')).toBeUndefined()
|
||||
expect(
|
||||
store.getWidget(wid(graphA, 'missing' as NodeId, 'widget'))
|
||||
).toBeUndefined()
|
||||
})
|
||||
|
||||
it('getNodeWidgets returns all widgets for a node', () => {
|
||||
const store = useWidgetValueStore()
|
||||
store.registerWidget(graphA, widget('node-1', 'seed', 'number', 1))
|
||||
store.registerWidget(graphA, widget('node-1', 'steps', 'number', 20))
|
||||
store.registerWidget(graphA, widget('node-2', 'cfg', 'number', 7))
|
||||
const node1 = 'node-1' as NodeId
|
||||
const node2 = 'node-2' as NodeId
|
||||
store.registerWidget(wid(graphA, node1, 'seed'), widgetState('number', 1))
|
||||
store.registerWidget(
|
||||
wid(graphA, node1, 'steps'),
|
||||
widgetState('number', 20)
|
||||
)
|
||||
store.registerWidget(wid(graphA, node2, 'cfg'), widgetState('number', 7))
|
||||
|
||||
const widgets = store.getNodeWidgets(graphA, 'node-1')
|
||||
const widgets = store.getNodeWidgets(
|
||||
nodeEntityId(asGraphId(graphA), node1)
|
||||
)
|
||||
expect(widgets).toHaveLength(2)
|
||||
expect(widgets.map((w) => w.name).sort()).toEqual(['seed', 'steps'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('direct property mutation', () => {
|
||||
it('disabled can be set directly via getWidget', () => {
|
||||
const store = useWidgetValueStore()
|
||||
const state = store.registerWidget(
|
||||
graphA,
|
||||
widget('node-1', 'seed', 'number', 100)
|
||||
)
|
||||
const id = wid(graphA, 'node-1' as NodeId, 'seed')
|
||||
const state = store.registerWidget(id, widgetState('number', 100))
|
||||
|
||||
state.disabled = true
|
||||
expect(store.getWidget(graphA, 'node-1', 'seed')?.disabled).toBe(true)
|
||||
expect(store.getWidget(id)?.disabled).toBe(true)
|
||||
})
|
||||
|
||||
it('label can be set directly via getWidget', () => {
|
||||
const store = useWidgetValueStore()
|
||||
const state = store.registerWidget(
|
||||
graphA,
|
||||
widget('node-1', 'seed', 'number', 100)
|
||||
)
|
||||
const id = wid(graphA, 'node-1' as NodeId, 'seed')
|
||||
const state = store.registerWidget(id, widgetState('number', 100))
|
||||
|
||||
state.label = 'Random Seed'
|
||||
expect(store.getWidget(graphA, 'node-1', 'seed')?.label).toBe(
|
||||
'Random Seed'
|
||||
)
|
||||
expect(store.getWidget(id)?.label).toBe('Random Seed')
|
||||
|
||||
state.label = undefined
|
||||
expect(store.getWidget(graphA, 'node-1', 'seed')?.label).toBeUndefined()
|
||||
expect(store.getWidget(id)?.label).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('graph isolation', () => {
|
||||
it('isolates widget states by graph', () => {
|
||||
const store = useWidgetValueStore()
|
||||
store.registerWidget(graphA, widget('node-1', 'seed', 'number', 1))
|
||||
store.registerWidget(graphB, widget('node-1', 'seed', 'number', 2))
|
||||
const node = 'node-1' as NodeId
|
||||
store.registerWidget(wid(graphA, node, 'seed'), widgetState('number', 1))
|
||||
store.registerWidget(wid(graphB, node, 'seed'), widgetState('number', 2))
|
||||
|
||||
expect(store.getWidget(graphA, 'node-1', 'seed')?.value).toBe(1)
|
||||
expect(store.getWidget(graphB, 'node-1', 'seed')?.value).toBe(2)
|
||||
expect(store.getWidget(wid(graphA, node, 'seed'))?.value).toBe(1)
|
||||
expect(store.getWidget(wid(graphB, node, 'seed'))?.value).toBe(2)
|
||||
})
|
||||
|
||||
it('clearGraph only removes one graph namespace', () => {
|
||||
const store = useWidgetValueStore()
|
||||
store.registerWidget(graphA, widget('node-1', 'seed', 'number', 1))
|
||||
store.registerWidget(graphB, widget('node-1', 'seed', 'number', 2))
|
||||
const node = 'node-1' as NodeId
|
||||
store.registerWidget(wid(graphA, node, 'seed'), widgetState('number', 1))
|
||||
store.registerWidget(wid(graphB, node, 'seed'), widgetState('number', 2))
|
||||
|
||||
store.clearGraph(graphA)
|
||||
|
||||
expect(store.getWidget(graphA, 'node-1', 'seed')).toBeUndefined()
|
||||
expect(store.getWidget(graphB, 'node-1', 'seed')?.value).toBe(2)
|
||||
expect(store.getWidget(wid(graphA, node, 'seed'))).toBeUndefined()
|
||||
expect(store.getWidget(wid(graphB, node, 'seed'))?.value).toBe(2)
|
||||
})
|
||||
})
|
||||
|
||||
describe('returned state identity', () => {
|
||||
const node = 'node-1' as NodeId
|
||||
const widgetId = wid(graphA, node, 'seed')
|
||||
|
||||
it('getWidget returns the same reference as registerWidget', () => {
|
||||
const store = useWidgetValueStore()
|
||||
const registered = store.registerWidget(
|
||||
widgetId,
|
||||
widgetState('number', 100)
|
||||
)
|
||||
expect(store.getWidget(widgetId)).toBe(registered)
|
||||
})
|
||||
|
||||
it('cached references detach safely after clearGraph', () => {
|
||||
const store = useWidgetValueStore()
|
||||
const view = store.registerWidget(widgetId, widgetState('number', 100))
|
||||
store.clearGraph(graphA)
|
||||
view.value = 999
|
||||
view.label = 'ignored'
|
||||
view.disabled = true
|
||||
expect(store.getWidget(widgetId)).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('getNodeWidgetsByName', () => {
|
||||
it('returns empty map when node has no widgets', () => {
|
||||
const store = useWidgetValueStore()
|
||||
const map = store.getNodeWidgetsByName(
|
||||
nodeEntityId(asGraphId(graphA), 'no-such' as NodeId)
|
||||
)
|
||||
expect(map.size).toBe(0)
|
||||
})
|
||||
|
||||
it('returns map keyed by widget name', () => {
|
||||
const store = useWidgetValueStore()
|
||||
const node = 'node-1' as NodeId
|
||||
store.registerWidget(wid(graphA, node, 'seed'), widgetState('number', 1))
|
||||
store.registerWidget(wid(graphA, node, 'cfg'), widgetState('number', 7))
|
||||
const map = store.getNodeWidgetsByName(
|
||||
nodeEntityId(asGraphId(graphA), node)
|
||||
)
|
||||
expect(map.size).toBe(2)
|
||||
expect(map.get('seed')?.value).toBe(1)
|
||||
expect(map.get('cfg')?.value).toBe(7)
|
||||
expect(map.get('missing')).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('setValue', () => {
|
||||
it('updates an existing widget value and returns true', () => {
|
||||
const store = useWidgetValueStore()
|
||||
const id = wid(graphA, 'node-1' as NodeId, 'seed')
|
||||
store.registerWidget(id, widgetState('number', 1))
|
||||
expect(store.setValue(id, 99)).toBe(true)
|
||||
expect(store.getWidget(id)?.value).toBe(99)
|
||||
})
|
||||
|
||||
it('returns false when setting value on an unregistered widget', () => {
|
||||
const store = useWidgetValueStore()
|
||||
const id = wid(graphA, 'node-1' as NodeId, 'seed')
|
||||
expect(store.setValue(id, 99)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('reactivity through the view', () => {
|
||||
it('clearGraph removes data; subsequent getWidget returns undefined', () => {
|
||||
const store = useWidgetValueStore()
|
||||
const id = wid(graphA, 'node-1' as NodeId, 'seed')
|
||||
store.registerWidget(id, widgetState('number', 100))
|
||||
store.clearGraph(graphA)
|
||||
expect(store.getWidget(id)).toBeUndefined()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,97 +1,97 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { reactive, ref } from 'vue'
|
||||
import { reactive } from 'vue'
|
||||
|
||||
import type { NodeId } from '@/lib/litegraph/src/LGraphNode'
|
||||
import type { UUID } from '@/utils/uuid'
|
||||
import type {
|
||||
IBaseWidget,
|
||||
IWidgetOptions
|
||||
} from '@/lib/litegraph/src/types/widgets'
|
||||
import {
|
||||
asGraphId,
|
||||
isNodeIdForGraph,
|
||||
isWidgetIdForGraph,
|
||||
nodeEntityId,
|
||||
parseWidgetEntityId
|
||||
} from '@/world/entityIds'
|
||||
import type { NodeEntityId, WidgetEntityId } from '@/world/entityIds'
|
||||
import type { WidgetState } from '@/world/widgets/widgetState'
|
||||
|
||||
type WidgetKey = `${NodeId}:${string}`
|
||||
|
||||
export function stripGraphPrefix(scopedId: NodeId | string): NodeId {
|
||||
return String(scopedId).replace(/^(.*:)+/, '') as NodeId
|
||||
}
|
||||
|
||||
export interface WidgetState<
|
||||
TValue = unknown,
|
||||
TType extends string = string,
|
||||
TOptions extends IWidgetOptions = IWidgetOptions
|
||||
> extends Pick<
|
||||
IBaseWidget<TValue, TType, TOptions>,
|
||||
'name' | 'type' | 'value' | 'options' | 'label' | 'serialize' | 'disabled'
|
||||
> {
|
||||
nodeId: NodeId
|
||||
}
|
||||
export type { WidgetState } from '@/world/widgets/widgetState'
|
||||
|
||||
export const useWidgetValueStore = defineStore('widgetValue', () => {
|
||||
const graphWidgetStates = ref(new Map<UUID, Map<WidgetKey, WidgetState>>())
|
||||
const widgets = reactive(new Map<WidgetEntityId, WidgetState>())
|
||||
const widgetIdsByNode = reactive(new Map<NodeEntityId, WidgetEntityId[]>())
|
||||
|
||||
function getWidgetStateMap(graphId: UUID): Map<WidgetKey, WidgetState> {
|
||||
const widgetStates = graphWidgetStates.value.get(graphId)
|
||||
if (widgetStates) return widgetStates
|
||||
|
||||
const nextWidgetStates = reactive(new Map<WidgetKey, WidgetState>())
|
||||
graphWidgetStates.value.set(graphId, nextWidgetStates)
|
||||
return nextWidgetStates
|
||||
}
|
||||
|
||||
function makeKey(nodeId: NodeId, widgetName: string): WidgetKey {
|
||||
return `${nodeId}:${widgetName}`
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use `ensureWidgetState(widget.entityId, init)` from
|
||||
* `src/world/widgetValueIO.ts` — the branded `WidgetEntityId` prevents
|
||||
* producer/consumer drift that loose triples allow.
|
||||
*/
|
||||
function registerWidget<TValue = unknown>(
|
||||
graphId: UUID,
|
||||
widgetId: WidgetEntityId,
|
||||
state: WidgetState<TValue>
|
||||
): WidgetState<TValue> {
|
||||
const widgetStates = getWidgetStateMap(graphId)
|
||||
const key = makeKey(state.nodeId, state.name)
|
||||
widgetStates.set(key, state)
|
||||
return widgetStates.get(key) as WidgetState<TValue>
|
||||
const { graphId, nodeId } = parseWidgetEntityId(widgetId)
|
||||
|
||||
widgets.set(widgetId, {
|
||||
...state,
|
||||
disabled: state.disabled ?? false
|
||||
})
|
||||
|
||||
const ownerId = nodeEntityId(graphId, nodeId)
|
||||
const ids = widgetIdsByNode.get(ownerId)
|
||||
if (!ids) {
|
||||
widgetIdsByNode.set(ownerId, [widgetId])
|
||||
} else if (!ids.includes(widgetId)) {
|
||||
ids.push(widgetId)
|
||||
}
|
||||
|
||||
return widgets.get(widgetId) as WidgetState<TValue>
|
||||
}
|
||||
|
||||
function getNodeWidgets(graphId: UUID, nodeId: NodeId): WidgetState[] {
|
||||
const widgetStates = getWidgetStateMap(graphId)
|
||||
const prefix = `${nodeId}:`
|
||||
return [...widgetStates]
|
||||
.filter(([key]) => key.startsWith(prefix))
|
||||
.map(([, state]) => state)
|
||||
function getWidget(widgetId: WidgetEntityId): WidgetState | undefined {
|
||||
return widgets.get(widgetId)
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use `getWidgetState(widget.entityId)` or
|
||||
* `readWidgetValue(widget.entityId)` from `src/world/widgetValueIO.ts` —
|
||||
* the branded `WidgetEntityId` prevents producer/consumer drift that loose
|
||||
* triples allow.
|
||||
*/
|
||||
function getWidget(
|
||||
graphId: UUID,
|
||||
nodeId: NodeId,
|
||||
widgetName: string
|
||||
): WidgetState | undefined {
|
||||
return getWidgetStateMap(graphId).get(makeKey(nodeId, widgetName))
|
||||
function getNodeWidgets(nodeId: NodeEntityId): WidgetState[] {
|
||||
const ids = widgetIdsByNode.get(nodeId)
|
||||
if (!ids) return []
|
||||
const result: WidgetState[] = []
|
||||
for (const widgetId of ids) {
|
||||
const w = widgets.get(widgetId)
|
||||
if (w) result.push(w)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function getNodeWidgetsByName(
|
||||
nodeId: NodeEntityId
|
||||
): Map<string, WidgetState> {
|
||||
const result = new Map<string, WidgetState>()
|
||||
const ids = widgetIdsByNode.get(nodeId)
|
||||
if (!ids) return result
|
||||
for (const widgetId of ids) {
|
||||
const w = widgets.get(widgetId)
|
||||
if (!w) continue
|
||||
const { name } = parseWidgetEntityId(widgetId)
|
||||
result.set(name, w)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function setValue(
|
||||
graphId: UUID,
|
||||
nodeId: NodeId,
|
||||
widgetName: string,
|
||||
widgetId: WidgetEntityId,
|
||||
value: WidgetState['value']
|
||||
): boolean {
|
||||
const state = getWidgetStateMap(graphId).get(makeKey(nodeId, widgetName))
|
||||
if (!state) return false
|
||||
state.value = value
|
||||
const widget = widgets.get(widgetId)
|
||||
if (!widget) return false
|
||||
widget.value = value
|
||||
return true
|
||||
}
|
||||
|
||||
function clearGraph(graphId: UUID): void {
|
||||
graphWidgetStates.value.delete(graphId)
|
||||
const branded = asGraphId(graphId)
|
||||
for (const widgetId of widgets.keys()) {
|
||||
if (isWidgetIdForGraph(branded, widgetId)) {
|
||||
widgets.delete(widgetId)
|
||||
}
|
||||
}
|
||||
for (const nodeId of widgetIdsByNode.keys()) {
|
||||
if (isNodeIdForGraph(branded, nodeId)) {
|
||||
widgetIdsByNode.delete(nodeId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -99,6 +99,7 @@ export const useWidgetValueStore = defineStore('widgetValue', () => {
|
||||
getWidget,
|
||||
setValue,
|
||||
getNodeWidgets,
|
||||
getNodeWidgetsByName,
|
||||
clearGraph
|
||||
}
|
||||
})
|
||||
|
||||
@@ -29,7 +29,7 @@ import { app } from '@/scripts/app'
|
||||
import { t } from '@/i18n'
|
||||
import { parseNodeLocatorId } from '@/types/nodeIdentification'
|
||||
import type { WidgetEntityId } from '@/world/entityIds'
|
||||
import { widgetEntityId } from '@/world/entityIds'
|
||||
import { deriveWidgetEntityId } from '@/world/entityIds'
|
||||
|
||||
type ImageNode = LGraphNode & { imgs: HTMLImageElement[] | undefined }
|
||||
type VideoNode = LGraphNode & {
|
||||
@@ -377,9 +377,7 @@ export function getWidgetEntityIdForNode(
|
||||
widget: Pick<IBaseWidget, 'name' | 'entityId'>
|
||||
): WidgetEntityId | undefined {
|
||||
if (widget.entityId) return widget.entityId
|
||||
const graphId = node.graph?.rootGraph.id
|
||||
if (!graphId || node.id === -1) return undefined
|
||||
return widgetEntityId(graphId, node.id, widget.name)
|
||||
return deriveWidgetEntityId(node.graph?.rootGraph.id, node.id, widget.name)
|
||||
}
|
||||
|
||||
export function isLoad3dNode(node: LGraphNode) {
|
||||
|
||||
@@ -1,45 +1,23 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { describe, expect, expectTypeOf, it } from 'vitest'
|
||||
|
||||
import type { WidgetEntityId } from './entityIds'
|
||||
import type { UUID } from '@/utils/uuid'
|
||||
|
||||
import type { NodeEntityId, NodeId, WidgetEntityId } from './entityIds'
|
||||
import {
|
||||
asGraphId,
|
||||
deriveWidgetEntityId,
|
||||
isWidgetEntityId,
|
||||
nodeEntityId,
|
||||
parseLegacyWidgetEntityId,
|
||||
parseWidgetEntityId,
|
||||
widgetEntityId
|
||||
} from './entityIds'
|
||||
|
||||
describe('widgetEntityId', () => {
|
||||
const graphId = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890'
|
||||
|
||||
it('builds a deterministic id from its components', () => {
|
||||
const id = widgetEntityId(graphId, 42, 'seed')
|
||||
expect(id).toBe(`${graphId}:42:seed`)
|
||||
})
|
||||
|
||||
it('produces equal ids for equal inputs', () => {
|
||||
expect(widgetEntityId(graphId, 42, 'seed')).toBe(
|
||||
widgetEntityId(graphId, 42, 'seed')
|
||||
)
|
||||
})
|
||||
|
||||
it('produces distinct ids when any component differs', () => {
|
||||
const baseline = widgetEntityId(graphId, 42, 'seed')
|
||||
expect(widgetEntityId(graphId, 43, 'seed')).not.toBe(baseline)
|
||||
expect(widgetEntityId(graphId, 42, 'steps')).not.toBe(baseline)
|
||||
const otherGraph = 'b1b2c3d4-e5f6-7890-abcd-ef1234567890'
|
||||
expect(widgetEntityId(otherGraph, 42, 'seed')).not.toBe(baseline)
|
||||
})
|
||||
|
||||
it('accepts string node ids', () => {
|
||||
const id = widgetEntityId(graphId, 'node-7', 'value')
|
||||
expect(id).toBe(`${graphId}:node-7:value`)
|
||||
})
|
||||
})
|
||||
|
||||
describe('parseWidgetEntityId', () => {
|
||||
const graphId = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890'
|
||||
const graphId = asGraphId('a3f2c1d8-4567-89ab-cdef-1234567890ab' as UUID)
|
||||
|
||||
it('round-trips a constructed id', () => {
|
||||
const id = widgetEntityId(graphId, 42, 'seed')
|
||||
it('round-trips a simple name', () => {
|
||||
const id = widgetEntityId(graphId, 42 as NodeId, 'seed')
|
||||
expect(parseWidgetEntityId(id)).toEqual({
|
||||
graphId,
|
||||
nodeId: '42',
|
||||
@@ -47,29 +25,64 @@ describe('parseWidgetEntityId', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('preserves colons inside the name segment', () => {
|
||||
const rawName = 'nested:label:with:colons'
|
||||
const rawId = `${graphId}:42:${rawName}` as WidgetEntityId
|
||||
expect(parseWidgetEntityId(rawId)).toEqual({
|
||||
it('preserves names containing colons', () => {
|
||||
const id = widgetEntityId(graphId, 7 as NodeId, 'images.image:0')
|
||||
expect(parseWidgetEntityId(id).name).toBe('images.image:0')
|
||||
})
|
||||
|
||||
it('handles string node ids', () => {
|
||||
const id = widgetEntityId(graphId, '12:5' as NodeId, 'sub_widget')
|
||||
const parsed = parseWidgetEntityId(id)
|
||||
expect(parsed.graphId).toBe(graphId)
|
||||
})
|
||||
|
||||
it('round-trips an empty name', () => {
|
||||
const id = widgetEntityId(graphId, 1 as NodeId, '')
|
||||
expect(parseWidgetEntityId(id)).toEqual({
|
||||
graphId,
|
||||
nodeId: '42',
|
||||
name: rawName
|
||||
nodeId: '1',
|
||||
name: ''
|
||||
})
|
||||
})
|
||||
|
||||
it('throws on missing widget: prefix', () => {
|
||||
expect(() =>
|
||||
parseWidgetEntityId(`node:${graphId}:42` as unknown as WidgetEntityId)
|
||||
).toThrow(/Malformed WidgetEntityId/)
|
||||
})
|
||||
|
||||
it('throws on too few colons', () => {
|
||||
expect(() => parseWidgetEntityId('widget:abc' as WidgetEntityId)).toThrow(
|
||||
/Malformed WidgetEntityId/
|
||||
)
|
||||
})
|
||||
|
||||
it('throws when nodeId segment is missing', () => {
|
||||
expect(() =>
|
||||
parseWidgetEntityId(`widget:${graphId}:42` as WidgetEntityId)
|
||||
).toThrow(/Malformed WidgetEntityId/)
|
||||
})
|
||||
})
|
||||
|
||||
describe('isWidgetEntityId', () => {
|
||||
const graphId = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890'
|
||||
const graphId = asGraphId('a1b2c3d4-e5f6-7890-abcd-ef1234567890' as UUID)
|
||||
|
||||
it('accepts ids built by the constructor', () => {
|
||||
expect(isWidgetEntityId(widgetEntityId(graphId, 1, 'x'))).toBe(true)
|
||||
expect(isWidgetEntityId(widgetEntityId(graphId, 1 as NodeId, 'x'))).toBe(
|
||||
true
|
||||
)
|
||||
})
|
||||
|
||||
it('rejects strings without two colon-separated segments', () => {
|
||||
it('rejects strings lacking the widget: prefix', () => {
|
||||
expect(isWidgetEntityId('only-one-colon:42')).toBe(false)
|
||||
expect(isWidgetEntityId('no-colons')).toBe(false)
|
||||
expect(isWidgetEntityId(':leading-colon:name')).toBe(false)
|
||||
expect(isWidgetEntityId('graph::name')).toBe(false)
|
||||
expect(isWidgetEntityId(`${graphId}:42:seed`)).toBe(false)
|
||||
expect(isWidgetEntityId(`node:${graphId}:42`)).toBe(false)
|
||||
})
|
||||
|
||||
it('rejects strings with too few segments', () => {
|
||||
expect(isWidgetEntityId('widget:abc')).toBe(false)
|
||||
expect(isWidgetEntityId(`widget:${graphId}:42`)).toBe(false)
|
||||
})
|
||||
|
||||
it('rejects non-strings', () => {
|
||||
@@ -79,3 +92,90 @@ describe('isWidgetEntityId', () => {
|
||||
expect(isWidgetEntityId({})).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('parseLegacyWidgetEntityId', () => {
|
||||
const graphId = asGraphId('11111111-1111-4111-8111-111111111111' as UUID)
|
||||
|
||||
it('parses an unprefixed widget id for the current graph', () => {
|
||||
expect(parseLegacyWidgetEntityId(`${graphId}:42:seed`, graphId)).toEqual({
|
||||
graphId,
|
||||
nodeId: '42',
|
||||
name: 'seed'
|
||||
})
|
||||
})
|
||||
|
||||
it('preserves widget names containing colons', () => {
|
||||
expect(
|
||||
parseLegacyWidgetEntityId(`${graphId}:7:images.image:0`, graphId)?.name
|
||||
).toBe('images.image:0')
|
||||
})
|
||||
|
||||
it('rejects canonical widget ids and other graph ids', () => {
|
||||
expect(
|
||||
parseLegacyWidgetEntityId(widgetEntityId(graphId, 42, 'seed'), graphId)
|
||||
).toBeUndefined()
|
||||
expect(
|
||||
parseLegacyWidgetEntityId(
|
||||
'22222222-2222-4222-8222-222222222222:42:seed',
|
||||
graphId
|
||||
)
|
||||
).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('deriveWidgetEntityId', () => {
|
||||
const graphId = asGraphId('e1d2c3b4-a5f6-1234-5678-90abcdef1234' as UUID)
|
||||
|
||||
it('builds an entity id when all inputs are present', () => {
|
||||
const id = deriveWidgetEntityId(graphId, 5 as NodeId, 'seed')
|
||||
expect(id).toBe(widgetEntityId(graphId, 5 as NodeId, 'seed'))
|
||||
})
|
||||
|
||||
it('returns undefined when graphId is missing', () => {
|
||||
expect(deriveWidgetEntityId(undefined, 5 as NodeId, 'seed')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('returns undefined when nodeId is undefined', () => {
|
||||
expect(deriveWidgetEntityId(graphId, undefined, 'seed')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('returns undefined for the sentinel nodeId -1', () => {
|
||||
expect(deriveWidgetEntityId(graphId, -1, 'seed')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('accepts a plain UUID for graphId', () => {
|
||||
const plain = 'f0e1d2c3-b4a5-6789-0123-456789abcdef' as UUID
|
||||
expect(deriveWidgetEntityId(plain, 1 as NodeId, 'x')).toBe(
|
||||
widgetEntityId(plain, 1 as NodeId, 'x')
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('entityIds type shapes', () => {
|
||||
type GraphId = ReturnType<typeof asGraphId>
|
||||
|
||||
it('widgetEntityId returns the WidgetEntityId brand', () => {
|
||||
expectTypeOf(widgetEntityId).returns.toEqualTypeOf<WidgetEntityId>()
|
||||
})
|
||||
|
||||
it('nodeEntityId returns the NodeEntityId brand', () => {
|
||||
expectTypeOf(nodeEntityId).returns.toEqualTypeOf<NodeEntityId>()
|
||||
})
|
||||
|
||||
it('parseWidgetEntityId returns the documented shape', () => {
|
||||
expectTypeOf(parseWidgetEntityId).returns.toEqualTypeOf<{
|
||||
graphId: GraphId
|
||||
nodeId: NodeId
|
||||
name: string
|
||||
}>()
|
||||
})
|
||||
|
||||
it('WidgetEntityId and NodeEntityId are distinct brands', () => {
|
||||
expectTypeOf<
|
||||
WidgetEntityId extends NodeEntityId ? WidgetEntityId : never
|
||||
>().toEqualTypeOf<never>()
|
||||
expectTypeOf<
|
||||
NodeEntityId extends WidgetEntityId ? NodeEntityId : never
|
||||
>().toEqualTypeOf<never>()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,39 +1,128 @@
|
||||
/**
|
||||
* Entity IDs are deterministic, content-addressed, string-prefixed values
|
||||
* — not opaque numerics (cf. bitECS, koota, miniplex).
|
||||
*
|
||||
* Identity is keyed by `rootGraph.id`, so an entity viewed at different
|
||||
* subgraph depths shares state. Migrating to numeric IDs would break
|
||||
* cross-subgraph value sharing. See ADR 0008 and `widgetValueStore.ts`.
|
||||
*
|
||||
* The `graph*Prefix` and `*EntityId` helpers below are the sole owners of
|
||||
* the on-the-wire format. Never hand-construct or parse these strings.
|
||||
*/
|
||||
import type { UUID } from '@/utils/uuid'
|
||||
|
||||
import type { Brand } from './brand'
|
||||
|
||||
export type NodeId = number | string
|
||||
|
||||
type GraphId = Brand<UUID, 'GraphId'>
|
||||
|
||||
export function asGraphId(id: UUID): GraphId {
|
||||
return id as GraphId
|
||||
}
|
||||
|
||||
export type NodeEntityId = Brand<string, 'NodeEntityId'>
|
||||
|
||||
function graphNodePrefix(graphId: GraphId): string {
|
||||
return `node:${graphId}:`
|
||||
}
|
||||
|
||||
export function nodeEntityId(graphId: GraphId, nodeId: NodeId): NodeEntityId {
|
||||
return `${graphNodePrefix(graphId)}${nodeId}` as NodeEntityId
|
||||
}
|
||||
|
||||
export type WidgetEntityId = Brand<string, 'WidgetEntityId'>
|
||||
|
||||
const SEPARATOR = ':'
|
||||
function graphWidgetPrefix(graphId: GraphId): string {
|
||||
return `widget:${graphId}:`
|
||||
}
|
||||
|
||||
export function widgetEntityId(
|
||||
graphId: UUID,
|
||||
graphId: UUID | GraphId,
|
||||
nodeId: NodeId,
|
||||
name: string
|
||||
): WidgetEntityId {
|
||||
return `${graphId}${SEPARATOR}${nodeId}${SEPARATOR}${name}` as WidgetEntityId
|
||||
return `${graphWidgetPrefix(graphId as GraphId)}${nodeId}:${name}` as WidgetEntityId
|
||||
}
|
||||
|
||||
/**
|
||||
* Guarded factory for `WidgetEntityId`. Returns `undefined` when any input
|
||||
* required to construct the id is missing — graphless widgets, unbound
|
||||
* node ids (`-1`), or unknown node ids cannot have a valid entity id.
|
||||
*
|
||||
* Use this from call sites that hold raw widget identity (graphId, nodeId,
|
||||
* name); use the `widget.entityId` getter directly when you already have a
|
||||
* `BaseWidget` instance.
|
||||
*/
|
||||
export function deriveWidgetEntityId(
|
||||
graphId: UUID | GraphId | undefined,
|
||||
nodeId: NodeId | undefined,
|
||||
name: string
|
||||
): WidgetEntityId | undefined {
|
||||
if (!graphId || nodeId === undefined || nodeId === -1) return undefined
|
||||
return widgetEntityId(graphId, nodeId, name)
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a `WidgetEntityId` into its constituent parts.
|
||||
*
|
||||
* On-the-wire format: `widget:${graphId}:${nodeId}:${name}`. The regex
|
||||
* captures the first two colon-delimited segments as graphId and nodeId,
|
||||
* then takes the rest as the widget name — so widget names may contain
|
||||
* colons (e.g. `images.image:0`). NodeId values containing colons split
|
||||
* at the first colon; production NodeIds are scalar-shaped, so this is a
|
||||
* documented edge case rather than a defect. Throws on malformed input
|
||||
* so upstream cast bugs surface at the parse site.
|
||||
*/
|
||||
const WIDGET_ID_RE = /^widget:([^:]+):([^:]+):(.*)$/
|
||||
|
||||
export function parseWidgetEntityId(id: WidgetEntityId): {
|
||||
graphId: UUID
|
||||
graphId: GraphId
|
||||
nodeId: NodeId
|
||||
name: string
|
||||
} {
|
||||
const firstColon = id.indexOf(SEPARATOR)
|
||||
const secondColon = id.indexOf(SEPARATOR, firstColon + 1)
|
||||
const match = WIDGET_ID_RE.exec(id)
|
||||
if (!match) {
|
||||
throw new Error(`Malformed WidgetEntityId: ${id}`)
|
||||
}
|
||||
const [, graphId, nodeId, name] = match
|
||||
return {
|
||||
graphId: id.slice(0, firstColon),
|
||||
nodeId: id.slice(firstColon + 1, secondColon),
|
||||
name: id.slice(secondColon + 1)
|
||||
graphId: graphId as GraphId,
|
||||
nodeId: nodeId as NodeId,
|
||||
name
|
||||
}
|
||||
}
|
||||
|
||||
export function isWidgetEntityId(value: unknown): value is WidgetEntityId {
|
||||
if (typeof value !== 'string') return false
|
||||
const firstColon = value.indexOf(SEPARATOR)
|
||||
if (firstColon <= 0) return false
|
||||
const secondColon = value.indexOf(SEPARATOR, firstColon + 1)
|
||||
return secondColon > firstColon + 1
|
||||
return typeof value === 'string' && WIDGET_ID_RE.test(value)
|
||||
}
|
||||
|
||||
export function parseLegacyWidgetEntityId(
|
||||
id: string,
|
||||
graphId: UUID | GraphId
|
||||
): { graphId: GraphId; nodeId: NodeId; name: string } | undefined {
|
||||
const brandedGraphId = graphId as GraphId
|
||||
const prefix = `${brandedGraphId}:`
|
||||
if (!id.startsWith(prefix)) return undefined
|
||||
|
||||
const rest = id.slice(prefix.length)
|
||||
const separator = rest.indexOf(':')
|
||||
if (separator <= 0) return undefined
|
||||
|
||||
return {
|
||||
graphId: brandedGraphId,
|
||||
nodeId: rest.slice(0, separator) as NodeId,
|
||||
name: rest.slice(separator + 1)
|
||||
}
|
||||
}
|
||||
|
||||
export function isNodeIdForGraph(graphId: GraphId, id: NodeEntityId): boolean {
|
||||
return id.startsWith(graphNodePrefix(graphId))
|
||||
}
|
||||
|
||||
export function isWidgetIdForGraph(
|
||||
graphId: GraphId,
|
||||
id: WidgetEntityId
|
||||
): boolean {
|
||||
return id.startsWith(graphWidgetPrefix(graphId))
|
||||
}
|
||||
|
||||
@@ -1,164 +0,0 @@
|
||||
import { setActivePinia, createPinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it } from 'vitest'
|
||||
|
||||
import { useWidgetValueStore } from '@/stores/widgetValueStore'
|
||||
|
||||
import { widgetEntityId } from './entityIds'
|
||||
import {
|
||||
ensureWidgetState,
|
||||
getWidgetState,
|
||||
readWidgetValue,
|
||||
writeWidgetValue
|
||||
} from './widgetValueIO'
|
||||
|
||||
describe('widgetValueIO', () => {
|
||||
const graphA = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890'
|
||||
const graphB = 'b1b2c3d4-e5f6-7890-abcd-ef1234567890'
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
})
|
||||
|
||||
describe('ensureWidgetState', () => {
|
||||
it('registers a new state when none exists', () => {
|
||||
const id = widgetEntityId(graphA, 1, 'seed')
|
||||
const state = ensureWidgetState(id, {
|
||||
type: 'number',
|
||||
value: 11,
|
||||
options: {},
|
||||
label: undefined,
|
||||
serialize: true,
|
||||
disabled: false
|
||||
})
|
||||
expect(state.value).toBe(11)
|
||||
expect(state.nodeId).toBe('1')
|
||||
expect(state.name).toBe('seed')
|
||||
})
|
||||
|
||||
it('is idempotent — returns the same state on repeated calls', () => {
|
||||
const id = widgetEntityId(graphA, 1, 'seed')
|
||||
const init = {
|
||||
type: 'number',
|
||||
value: 11,
|
||||
options: {},
|
||||
label: undefined,
|
||||
serialize: true,
|
||||
disabled: false
|
||||
}
|
||||
const first = ensureWidgetState(id, init)
|
||||
const second = ensureWidgetState(id, init)
|
||||
expect(second).toBe(first)
|
||||
})
|
||||
|
||||
it('does not overwrite an existing state with init values', () => {
|
||||
const id = widgetEntityId(graphA, 1, 'seed')
|
||||
const first = ensureWidgetState(id, {
|
||||
type: 'number',
|
||||
value: 11,
|
||||
options: {},
|
||||
label: undefined,
|
||||
serialize: true,
|
||||
disabled: false
|
||||
})
|
||||
first.value = 99
|
||||
const second = ensureWidgetState(id, {
|
||||
type: 'number',
|
||||
value: 11,
|
||||
options: {},
|
||||
label: undefined,
|
||||
serialize: true,
|
||||
disabled: false
|
||||
})
|
||||
expect(second.value).toBe(99)
|
||||
})
|
||||
})
|
||||
|
||||
describe('readWidgetValue / writeWidgetValue', () => {
|
||||
it('round-trips a value through the entity-id surface', () => {
|
||||
const id = widgetEntityId(graphA, 1, 'seed')
|
||||
ensureWidgetState(id, {
|
||||
type: 'number',
|
||||
value: 11,
|
||||
options: {},
|
||||
label: undefined,
|
||||
serialize: true,
|
||||
disabled: false
|
||||
})
|
||||
expect(readWidgetValue(id)).toBe(11)
|
||||
|
||||
expect(writeWidgetValue(id, 22)).toBe(true)
|
||||
expect(readWidgetValue(id)).toBe(22)
|
||||
})
|
||||
|
||||
it('returns false when writing to an unregistered id', () => {
|
||||
const id = widgetEntityId(graphA, 1, 'seed')
|
||||
expect(writeWidgetValue(id, 22)).toBe(false)
|
||||
})
|
||||
|
||||
it('returns undefined when reading an unregistered id', () => {
|
||||
const id = widgetEntityId(graphA, 1, 'seed')
|
||||
expect(readWidgetValue(id)).toBeUndefined()
|
||||
expect(getWidgetState(id)).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('isolation', () => {
|
||||
it('keeps independent values across distinct entity ids', () => {
|
||||
const id1 = widgetEntityId(graphA, 1, 'seed')
|
||||
const id2 = widgetEntityId(graphA, 2, 'seed')
|
||||
const init = {
|
||||
type: 'number',
|
||||
value: 0,
|
||||
options: {},
|
||||
label: undefined,
|
||||
serialize: true,
|
||||
disabled: false
|
||||
}
|
||||
ensureWidgetState(id1, init)
|
||||
ensureWidgetState(id2, init)
|
||||
|
||||
writeWidgetValue(id1, 11)
|
||||
writeWidgetValue(id2, 22)
|
||||
|
||||
expect(readWidgetValue(id1)).toBe(11)
|
||||
expect(readWidgetValue(id2)).toBe(22)
|
||||
})
|
||||
|
||||
it('isolates values across graph ids', () => {
|
||||
const idA = widgetEntityId(graphA, 1, 'seed')
|
||||
const idB = widgetEntityId(graphB, 1, 'seed')
|
||||
const init = {
|
||||
type: 'number',
|
||||
value: 0,
|
||||
options: {},
|
||||
label: undefined,
|
||||
serialize: true,
|
||||
disabled: false
|
||||
}
|
||||
ensureWidgetState(idA, init)
|
||||
ensureWidgetState(idB, init)
|
||||
|
||||
writeWidgetValue(idA, 11)
|
||||
writeWidgetValue(idB, 22)
|
||||
|
||||
expect(readWidgetValue(idA)).toBe(11)
|
||||
expect(readWidgetValue(idB)).toBe(22)
|
||||
})
|
||||
|
||||
it('matches the legacy triple-keyed API for the same widget', () => {
|
||||
const id = widgetEntityId(graphA, 1, 'seed')
|
||||
const init = {
|
||||
type: 'number',
|
||||
value: 11,
|
||||
options: {},
|
||||
label: undefined,
|
||||
serialize: true,
|
||||
disabled: false
|
||||
}
|
||||
ensureWidgetState(id, init)
|
||||
|
||||
const viaLegacy = useWidgetValueStore().getWidget(graphA, '1', 'seed')
|
||||
expect(viaLegacy).toBe(getWidgetState(id))
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,42 +0,0 @@
|
||||
import { useWidgetValueStore } from '@/stores/widgetValueStore'
|
||||
import type { WidgetState } from '@/stores/widgetValueStore'
|
||||
|
||||
import { parseWidgetEntityId } from './entityIds'
|
||||
import type { WidgetEntityId } from './entityIds'
|
||||
|
||||
export function getWidgetState(
|
||||
entityId: WidgetEntityId
|
||||
): WidgetState | undefined {
|
||||
const { graphId, nodeId, name } = parseWidgetEntityId(entityId)
|
||||
return useWidgetValueStore().getWidget(graphId, nodeId, name)
|
||||
}
|
||||
|
||||
export function readWidgetValue(
|
||||
entityId: WidgetEntityId
|
||||
): WidgetState['value'] | undefined {
|
||||
return getWidgetState(entityId)?.value
|
||||
}
|
||||
|
||||
export function writeWidgetValue(
|
||||
entityId: WidgetEntityId,
|
||||
value: WidgetState['value']
|
||||
): boolean {
|
||||
const { graphId, nodeId, name } = parseWidgetEntityId(entityId)
|
||||
return useWidgetValueStore().setValue(graphId, nodeId, name, value)
|
||||
}
|
||||
|
||||
type WidgetStateInit = Omit<WidgetState, 'nodeId' | 'name'>
|
||||
|
||||
export function ensureWidgetState(
|
||||
entityId: WidgetEntityId,
|
||||
init: WidgetStateInit
|
||||
): WidgetState {
|
||||
const existing = getWidgetState(entityId)
|
||||
if (existing) return existing
|
||||
const { graphId, nodeId, name } = parseWidgetEntityId(entityId)
|
||||
return useWidgetValueStore().registerWidget(graphId, {
|
||||
...init,
|
||||
nodeId,
|
||||
name
|
||||
})
|
||||
}
|
||||
19
src/world/widgets/widgetState.ts
Normal file
19
src/world/widgets/widgetState.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
export interface WidgetState<
|
||||
TValue = unknown,
|
||||
TType extends string = string,
|
||||
TOptions extends object = object
|
||||
> {
|
||||
value?: TValue
|
||||
label?: string
|
||||
disabled?: boolean
|
||||
type: TType
|
||||
options: TOptions
|
||||
/**
|
||||
* Whether the widget value is persisted in the workflow JSON
|
||||
* (`widgets_values`). Distinct from `IWidgetOptions.serialize`, which
|
||||
* controls whether the value is included in the API prompt sent for
|
||||
* execution. See `src/lib/litegraph/docs/WIDGET_SERIALIZATION.md`.
|
||||
* @default true
|
||||
*/
|
||||
serialize?: boolean
|
||||
}
|
||||
Reference in New Issue
Block a user