mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-20 14:30:41 +00:00
refactor(widget): Phase 3 - integrate widget types into stores
- widgetValueStore uses WidgetId for Map keys, getWidgetId() for generation - widgetValueStore.getWidget() supports (nodeId, name) and WidgetIdentity - WidgetState extends WidgetRuntimeState with MutableWidgetIdentity - NodeId imports from LiteGraph for compatibility (number | string) - Added MutableWidgetIdentity for stores with late binding - domWidgetStore keeps UUID keys (different identity system) - favoritedWidgetsStore unchanged (uses nodeLocatorId) Amp-Thread-ID: https://ampcode.com/threads/T-019c2eff-f52d-71b9-975c-5a4f7de2ac5f Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
@@ -1,5 +1,9 @@
|
||||
/**
|
||||
* Stores all DOM widgets that are used in the canvas.
|
||||
*
|
||||
* Note: This store uses UUID strings as keys (from BaseDOMWidget.id),
|
||||
* not WidgetId (`nodeId:name`). DOM widgets have their own identity
|
||||
* system separate from the canonical widget types.
|
||||
*/
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, markRaw, ref } from 'vue'
|
||||
@@ -9,7 +13,6 @@ import type { PositionConfig } from '@/composables/element/useAbsolutePosition'
|
||||
import type { BaseDOMWidget } from '@/scripts/domWidget'
|
||||
|
||||
export interface DomWidgetState extends PositionConfig {
|
||||
// Raw widget instance
|
||||
widget: Raw<BaseDOMWidget<object | string>>
|
||||
visible: boolean
|
||||
readonly: boolean
|
||||
|
||||
@@ -2,11 +2,13 @@ import { createTestingPinia } from '@pinia/testing'
|
||||
import { setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it } from 'vitest'
|
||||
|
||||
import type { NodeId } from '@/types/widget'
|
||||
|
||||
import type { WidgetState } from './widgetValueStore'
|
||||
import { useWidgetValueStore } from './widgetValueStore'
|
||||
|
||||
function widget<T>(
|
||||
nodeId: string,
|
||||
nodeId: NodeId,
|
||||
name: string,
|
||||
type: string,
|
||||
value: T,
|
||||
@@ -14,7 +16,16 @@ function widget<T>(
|
||||
Omit<WidgetState<T>, 'nodeId' | 'name' | 'type' | 'value'>
|
||||
> = {}
|
||||
): WidgetState<T> {
|
||||
return { nodeId, name, type, value, options: {}, ...extra }
|
||||
return {
|
||||
nodeId,
|
||||
name,
|
||||
type,
|
||||
value,
|
||||
options: {},
|
||||
hidden: false,
|
||||
disabled: false,
|
||||
...extra
|
||||
}
|
||||
}
|
||||
|
||||
describe('useWidgetValueStore', () => {
|
||||
@@ -25,47 +36,43 @@ describe('useWidgetValueStore', () => {
|
||||
describe('widgetState.value access', () => {
|
||||
it('getWidget returns undefined for unregistered widget', () => {
|
||||
const store = useWidgetValueStore()
|
||||
expect(store.getWidget('missing', 'widget')).toBeUndefined()
|
||||
expect(store.getWidget(999, 'widget')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('widgetState.value can be read and written directly', () => {
|
||||
const store = useWidgetValueStore()
|
||||
const state = store.registerWidget(
|
||||
widget('node-1', 'seed', 'number', 100)
|
||||
)
|
||||
const state = store.registerWidget(widget(1, 'seed', 'number', 100))
|
||||
expect(state.value).toBe(100)
|
||||
|
||||
state.value = 200
|
||||
expect(store.getWidget('node-1', 'seed')?.value).toBe(200)
|
||||
expect(store.getWidget(1, 'seed')?.value).toBe(200)
|
||||
})
|
||||
|
||||
it('stores different value types', () => {
|
||||
const store = useWidgetValueStore()
|
||||
store.registerWidget(widget('node-1', 'text', 'string', 'hello'))
|
||||
store.registerWidget(widget('node-1', 'number', 'number', 42))
|
||||
store.registerWidget(widget('node-1', 'boolean', 'toggle', true))
|
||||
store.registerWidget(widget('node-1', 'array', 'combo', [1, 2, 3]))
|
||||
store.registerWidget(widget(1, 'text', 'string', 'hello'))
|
||||
store.registerWidget(widget(1, 'number', 'number', 42))
|
||||
store.registerWidget(widget(1, 'boolean', 'toggle', true))
|
||||
store.registerWidget(widget(1, 'array', 'combo', [1, 2, 3]))
|
||||
|
||||
expect(store.getWidget('node-1', 'text')?.value).toBe('hello')
|
||||
expect(store.getWidget('node-1', 'number')?.value).toBe(42)
|
||||
expect(store.getWidget('node-1', 'boolean')?.value).toBe(true)
|
||||
expect(store.getWidget('node-1', 'array')?.value).toEqual([1, 2, 3])
|
||||
expect(store.getWidget(1, 'text')?.value).toBe('hello')
|
||||
expect(store.getWidget(1, 'number')?.value).toBe(42)
|
||||
expect(store.getWidget(1, 'boolean')?.value).toBe(true)
|
||||
expect(store.getWidget(1, 'array')?.value).toEqual([1, 2, 3])
|
||||
})
|
||||
})
|
||||
|
||||
describe('widget registration', () => {
|
||||
it('registers a widget with minimal properties', () => {
|
||||
const store = useWidgetValueStore()
|
||||
const state = store.registerWidget(
|
||||
widget('node-1', 'seed', 'number', 12345)
|
||||
)
|
||||
const state = store.registerWidget(widget(1, 'seed', 'number', 12345))
|
||||
|
||||
expect(state.nodeId).toBe('node-1')
|
||||
expect(state.nodeId).toBe(1)
|
||||
expect(state.name).toBe('seed')
|
||||
expect(state.type).toBe('number')
|
||||
expect(state.value).toBe(12345)
|
||||
expect(state.hidden).toBeUndefined()
|
||||
expect(state.disabled).toBeUndefined()
|
||||
expect(state.hidden).toBe(false)
|
||||
expect(state.disabled).toBe(false)
|
||||
expect(state.advanced).toBeUndefined()
|
||||
expect(state.promoted).toBeUndefined()
|
||||
expect(state.serialize).toBeUndefined()
|
||||
@@ -75,7 +82,7 @@ describe('useWidgetValueStore', () => {
|
||||
it('registers a widget with all properties', () => {
|
||||
const store = useWidgetValueStore()
|
||||
const state = store.registerWidget(
|
||||
widget('node-1', 'prompt', 'string', 'test', {
|
||||
widget(1, 'prompt', 'string', 'test', {
|
||||
label: 'Prompt Text',
|
||||
hidden: true,
|
||||
disabled: true,
|
||||
@@ -99,26 +106,35 @@ describe('useWidgetValueStore', () => {
|
||||
describe('widget getters', () => {
|
||||
it('getWidget returns widget state', () => {
|
||||
const store = useWidgetValueStore()
|
||||
store.registerWidget(widget('node-1', 'seed', 'number', 100))
|
||||
store.registerWidget(widget(1, 'seed', 'number', 100))
|
||||
|
||||
const state = store.getWidget('node-1', 'seed')
|
||||
const state = store.getWidget(1, 'seed')
|
||||
expect(state).toBeDefined()
|
||||
expect(state?.name).toBe('seed')
|
||||
expect(state?.value).toBe(100)
|
||||
})
|
||||
|
||||
it('getWidget accepts WidgetIdentity', () => {
|
||||
const store = useWidgetValueStore()
|
||||
store.registerWidget(widget(1, 'seed', 'number', 100))
|
||||
|
||||
const state = store.getWidget({ nodeId: 1, name: 'seed' })
|
||||
expect(state).toBeDefined()
|
||||
expect(state?.value).toBe(100)
|
||||
})
|
||||
|
||||
it('getWidget returns undefined for missing widget', () => {
|
||||
const store = useWidgetValueStore()
|
||||
expect(store.getWidget('missing', 'widget')).toBeUndefined()
|
||||
expect(store.getWidget(999, 'widget')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('getNodeWidgets returns all widgets for a node', () => {
|
||||
const store = useWidgetValueStore()
|
||||
store.registerWidget(widget('node-1', 'seed', 'number', 1))
|
||||
store.registerWidget(widget('node-1', 'steps', 'number', 20))
|
||||
store.registerWidget(widget('node-2', 'cfg', 'number', 7))
|
||||
store.registerWidget(widget(1, 'seed', 'number', 1))
|
||||
store.registerWidget(widget(1, 'steps', 'number', 20))
|
||||
store.registerWidget(widget(2, 'cfg', 'number', 7))
|
||||
|
||||
const widgets = store.getNodeWidgets('node-1')
|
||||
const widgets = store.getNodeWidgets(1)
|
||||
expect(widgets).toHaveLength(2)
|
||||
expect(widgets.map((w) => w.name).sort()).toEqual(['seed', 'steps'])
|
||||
})
|
||||
@@ -127,58 +143,48 @@ describe('useWidgetValueStore', () => {
|
||||
describe('direct property mutation', () => {
|
||||
it('hidden can be set directly via getWidget', () => {
|
||||
const store = useWidgetValueStore()
|
||||
const state = store.registerWidget(
|
||||
widget('node-1', 'seed', 'number', 100)
|
||||
)
|
||||
const state = store.registerWidget(widget(1, 'seed', 'number', 100))
|
||||
|
||||
state.hidden = true
|
||||
expect(store.getWidget('node-1', 'seed')?.hidden).toBe(true)
|
||||
expect(store.getWidget(1, 'seed')?.hidden).toBe(true)
|
||||
|
||||
state.hidden = false
|
||||
expect(store.getWidget('node-1', 'seed')?.hidden).toBe(false)
|
||||
expect(store.getWidget(1, 'seed')?.hidden).toBe(false)
|
||||
})
|
||||
|
||||
it('disabled can be set directly via getWidget', () => {
|
||||
const store = useWidgetValueStore()
|
||||
const state = store.registerWidget(
|
||||
widget('node-1', 'seed', 'number', 100)
|
||||
)
|
||||
const state = store.registerWidget(widget(1, 'seed', 'number', 100))
|
||||
|
||||
state.disabled = true
|
||||
expect(store.getWidget('node-1', 'seed')?.disabled).toBe(true)
|
||||
expect(store.getWidget(1, 'seed')?.disabled).toBe(true)
|
||||
})
|
||||
|
||||
it('advanced can be set directly via getWidget', () => {
|
||||
const store = useWidgetValueStore()
|
||||
const state = store.registerWidget(
|
||||
widget('node-1', 'seed', 'number', 100)
|
||||
)
|
||||
const state = store.registerWidget(widget(1, 'seed', 'number', 100))
|
||||
|
||||
state.advanced = true
|
||||
expect(store.getWidget('node-1', 'seed')?.advanced).toBe(true)
|
||||
expect(store.getWidget(1, 'seed')?.advanced).toBe(true)
|
||||
})
|
||||
|
||||
it('promoted can be set directly via getWidget', () => {
|
||||
const store = useWidgetValueStore()
|
||||
const state = store.registerWidget(
|
||||
widget('node-1', 'seed', 'number', 100)
|
||||
)
|
||||
const state = store.registerWidget(widget(1, 'seed', 'number', 100))
|
||||
|
||||
state.promoted = true
|
||||
expect(store.getWidget('node-1', 'seed')?.promoted).toBe(true)
|
||||
expect(store.getWidget(1, 'seed')?.promoted).toBe(true)
|
||||
})
|
||||
|
||||
it('label can be set directly via getWidget', () => {
|
||||
const store = useWidgetValueStore()
|
||||
const state = store.registerWidget(
|
||||
widget('node-1', 'seed', 'number', 100)
|
||||
)
|
||||
const state = store.registerWidget(widget(1, 'seed', 'number', 100))
|
||||
|
||||
state.label = 'Random Seed'
|
||||
expect(store.getWidget('node-1', 'seed')?.label).toBe('Random Seed')
|
||||
expect(store.getWidget(1, 'seed')?.label).toBe('Random Seed')
|
||||
|
||||
state.label = undefined
|
||||
expect(store.getWidget('node-1', 'seed')?.label).toBeUndefined()
|
||||
expect(store.getWidget(1, 'seed')?.label).toBeUndefined()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,58 +1,51 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import type { NodeId } from '@/lib/litegraph/src/LGraphNode'
|
||||
import type { IWidgetOptions } from '@/lib/litegraph/src/types/widgets'
|
||||
import type {
|
||||
IBaseWidget,
|
||||
IWidgetOptions
|
||||
} from '@/lib/litegraph/src/types/widgets'
|
||||
|
||||
/**
|
||||
* Widget state is keyed by `nodeId:widgetName` without graph context.
|
||||
* This is intentional: nodes viewed at different subgraph depths share
|
||||
* the same widget state, enabling synchronized values across the hierarchy.
|
||||
*/
|
||||
type WidgetKey = `${NodeId}:${string}`
|
||||
NodeId,
|
||||
WidgetId,
|
||||
WidgetIdentity,
|
||||
WidgetRuntimeState
|
||||
} from '@/types/widget'
|
||||
import { getWidgetId } from '@/types/widget'
|
||||
|
||||
/**
|
||||
* Strips graph/subgraph prefixes from a scoped node ID to get the bare node ID.
|
||||
* e.g., "graph1:subgraph2:42" → "42"
|
||||
*/
|
||||
export function stripGraphPrefix(scopedId: NodeId | string): NodeId {
|
||||
return String(scopedId).replace(/^(.*:)+/, '') as NodeId
|
||||
const stripped = String(scopedId).replace(/^(.*:)+/, '')
|
||||
const num = Number(stripped)
|
||||
return Number.isNaN(num) ? stripped : num
|
||||
}
|
||||
|
||||
/**
|
||||
* Extended widget state for the store, including type info and options.
|
||||
* Extends WidgetRuntimeState with additional fields needed for serialization.
|
||||
*/
|
||||
export interface WidgetState<
|
||||
TValue = unknown,
|
||||
TType extends string = string,
|
||||
TOptions extends IWidgetOptions<unknown> = IWidgetOptions<unknown>
|
||||
> extends Pick<
|
||||
IBaseWidget<TValue, TType, TOptions>,
|
||||
| 'name'
|
||||
| 'type'
|
||||
| 'value'
|
||||
| 'options'
|
||||
| 'label'
|
||||
| 'serialize'
|
||||
| 'disabled'
|
||||
| 'hidden'
|
||||
| 'advanced'
|
||||
| 'promoted'
|
||||
> {
|
||||
nodeId: NodeId
|
||||
> extends WidgetRuntimeState {
|
||||
value: TValue
|
||||
type?: string
|
||||
options?: TOptions
|
||||
serialize?: boolean
|
||||
}
|
||||
|
||||
export const useWidgetValueStore = defineStore('widgetValue', () => {
|
||||
const widgetStates = ref(new Map<WidgetKey, WidgetState>())
|
||||
|
||||
function makeKey(nodeId: NodeId, widgetName: string): WidgetKey {
|
||||
return `${nodeId}:${widgetName}`
|
||||
}
|
||||
/**
|
||||
* Widget state is keyed by WidgetId (`nodeId:widgetName`) without graph context.
|
||||
* This is intentional: nodes viewed at different subgraph depths share
|
||||
* the same widget state, enabling synchronized values across the hierarchy.
|
||||
*/
|
||||
const widgetStates = ref(new Map<WidgetId, WidgetState>())
|
||||
|
||||
function registerWidget<TValue = unknown>(
|
||||
state: WidgetState<TValue>
|
||||
): WidgetState<TValue> {
|
||||
const key = makeKey(state.nodeId, state.name)
|
||||
const key = getWidgetId(state)
|
||||
widgetStates.value.set(key, state)
|
||||
return widgetStates.value.get(key) as WidgetState<TValue>
|
||||
}
|
||||
@@ -64,11 +57,21 @@ export const useWidgetValueStore = defineStore('widgetValue', () => {
|
||||
.map(([, state]) => state)
|
||||
}
|
||||
|
||||
function getWidget(identity: WidgetIdentity): WidgetState | undefined
|
||||
function getWidget(
|
||||
nodeId: NodeId,
|
||||
widgetName: string
|
||||
): WidgetState | undefined
|
||||
function getWidget(
|
||||
nodeIdOrIdentity: NodeId | WidgetIdentity,
|
||||
widgetName?: string
|
||||
): WidgetState | undefined {
|
||||
return widgetStates.value.get(makeKey(nodeId, widgetName))
|
||||
if (typeof nodeIdOrIdentity === 'object') {
|
||||
return widgetStates.value.get(getWidgetId(nodeIdOrIdentity))
|
||||
}
|
||||
return widgetStates.value.get(
|
||||
getWidgetId({ nodeId: nodeIdOrIdentity, name: widgetName! })
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -7,11 +7,23 @@
|
||||
import type { NodeId, WidgetId } from './primitives'
|
||||
import { widgetId } from './primitives'
|
||||
|
||||
/**
|
||||
* Immutable widget identity for public APIs and type safety.
|
||||
*/
|
||||
export interface WidgetIdentity {
|
||||
readonly nodeId: NodeId
|
||||
readonly name: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Mutable widget identity for internal state management.
|
||||
* Used by stores that need to update identity after construction.
|
||||
*/
|
||||
export interface MutableWidgetIdentity {
|
||||
nodeId: NodeId
|
||||
name: string
|
||||
}
|
||||
|
||||
export function getWidgetId(widget: WidgetIdentity): WidgetId {
|
||||
return widgetId(widget.nodeId, widget.name)
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
export type { NodeId, WidgetId } from './primitives'
|
||||
export { widgetId } from './primitives'
|
||||
|
||||
export type { WidgetIdentity } from './identity'
|
||||
export type { MutableWidgetIdentity, WidgetIdentity } from './identity'
|
||||
export { getWidgetId } from './identity'
|
||||
|
||||
export type {
|
||||
|
||||
@@ -4,10 +4,15 @@
|
||||
* @module widget/primitives
|
||||
*/
|
||||
|
||||
export type NodeId = number
|
||||
import type { NodeId as LiteGraphNodeId } from '@/lib/litegraph/src/LGraphNode'
|
||||
|
||||
/**
|
||||
* NodeId matches LiteGraph's type for compatibility with the graph system.
|
||||
*/
|
||||
export type NodeId = LiteGraphNodeId
|
||||
|
||||
export type WidgetId = `${NodeId}:${string}`
|
||||
|
||||
export function widgetId(nodeId: NodeId, name: string): WidgetId {
|
||||
return `${nodeId}:${name}`
|
||||
return `${nodeId}:${name}` as WidgetId
|
||||
}
|
||||
|
||||
@@ -4,9 +4,13 @@
|
||||
* @module widget/state
|
||||
*/
|
||||
|
||||
import type { WidgetIdentity } from './identity'
|
||||
import type { MutableWidgetIdentity, WidgetIdentity } from './identity'
|
||||
|
||||
export interface WidgetRuntimeState extends WidgetIdentity {
|
||||
/**
|
||||
* Runtime state for widgets, with mutable identity for late binding.
|
||||
* Use this in stores where nodeId may be set after construction.
|
||||
*/
|
||||
export interface WidgetRuntimeState extends MutableWidgetIdentity {
|
||||
value: unknown
|
||||
hidden: boolean
|
||||
disabled: boolean
|
||||
@@ -15,11 +19,19 @@ export interface WidgetRuntimeState extends WidgetIdentity {
|
||||
promoted?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Layout state for DOM widgets positioned on the canvas.
|
||||
*
|
||||
* Note: Currently not used by domWidgetStore, which uses its own DomWidgetState
|
||||
* interface that extends PositionConfig (pos/size arrays) plus BaseDOMWidget ref.
|
||||
* This interface is provided for future use or alternative implementations.
|
||||
*/
|
||||
export interface WidgetLayoutState extends WidgetIdentity {
|
||||
visible: boolean
|
||||
x: number
|
||||
y: number
|
||||
w: number
|
||||
h: number
|
||||
hideOnZoom?: boolean
|
||||
zIndex: number
|
||||
active: boolean
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user