mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-22 05:19:03 +00:00
refactor(subgraph): key promoted widget state by entityId, drop encoded host-state name
Replace the encoded `[name, sourceNodeId, sourceWidgetName].join(':')`
host-state-name with the canonical `WidgetEntityId` per ADR 0009. Producers
and consumers flip in the same diff:
- Delete `getPromotedWidgetHostStateName` (was internal disambiguator)
- `PromotedWidgetView` reads/writes via `widgetValueIO` keyed by `entityId`
- `SubgraphNode.serialize` reads via `readWidgetValue(widget.entityId)`
- `getExplicitHostWidgetValue` drops its now-unused `subgraphNode` param
- `safeWidgetMapper` no longer overrides `storeNodeId/storeName`; those
fields are removed from `SafeWidgetData`
- `useProcessedWidgets` looks up state via `getWidgetState(entityId)`
- `getWidgetIdentity` collapses to `${entityId}:${type}` (type suffix
preserves the PR #9896 invariant for node-def schema drift)
Net -66 LOC. No fallback / accept-either branches: entityId is the single
source of truth for widget identity across the renderer.
Amp-Thread-ID: https://ampcode.com/threads/T-019e2260-c2ba-70b4-9962-1638be4bf645
Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
@@ -7,6 +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 {
|
||||
createTestSubgraph,
|
||||
createTestSubgraphNode
|
||||
@@ -440,8 +441,9 @@ describe('Nested promoted widget mapping', () => {
|
||||
expect(mappedWidget?.type).toBe('combo')
|
||||
// Promoted widgets read from a host-scoped store entry so per-host values
|
||||
// stay independent across SubgraphNode instances sharing the same subgraph.
|
||||
expect(mappedWidget?.storeNodeId).toBe(String(subgraphNodeB.id))
|
||||
expect(mappedWidget?.storeName).toBe('b_input:11:a_input')
|
||||
expect(mappedWidget?.entityId).toBe(
|
||||
widgetEntityId(graph.id, subgraphNodeB.id, 'b_input')
|
||||
)
|
||||
})
|
||||
|
||||
it('preserves distinct store identity for duplicate-named promoted widgets', () => {
|
||||
@@ -475,13 +477,15 @@ describe('Nested promoted widget mapping', () => {
|
||||
const widgets = nodeData?.widgets
|
||||
|
||||
expect(widgets).toHaveLength(2)
|
||||
// Both promoted widgets live on the same host node, so storeNodeId is
|
||||
// shared; storeName encodes the source identity to keep them distinct.
|
||||
expect(widgets?.[0]?.storeNodeId).toBe(String(subgraphNode.id))
|
||||
expect(widgets?.[1]?.storeNodeId).toBe(String(subgraphNode.id))
|
||||
expect(widgets?.[0]?.storeName).toBe(`first_seed:${firstNode.id}:seed`)
|
||||
expect(widgets?.[1]?.storeName).toBe(`second_seed:${secondNode.id}:seed`)
|
||||
expect(widgets?.[0]?.storeName).not.toBe(widgets?.[1]?.storeName)
|
||||
// Both promoted widgets live on the same host node; their entityIds use
|
||||
// the distinct subgraph input names to keep them independent.
|
||||
expect(widgets?.[0]?.entityId).toBe(
|
||||
widgetEntityId(graph.id, subgraphNode.id, 'first_seed')
|
||||
)
|
||||
expect(widgets?.[1]?.entityId).toBe(
|
||||
widgetEntityId(graph.id, subgraphNode.id, 'second_seed')
|
||||
)
|
||||
expect(widgets?.[0]?.entityId).not.toBe(widgets?.[1]?.entityId)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -7,10 +7,7 @@ import { reactive, shallowReactive } from 'vue'
|
||||
|
||||
import { useChainCallback } from '@/composables/functional/useChainCallback'
|
||||
import type { PromotedWidgetSource } from '@/core/graph/subgraph/promotedWidgetTypes'
|
||||
import {
|
||||
getPromotedWidgetHostStateName,
|
||||
isPromotedWidgetView
|
||||
} from '@/core/graph/subgraph/promotedWidgetTypes'
|
||||
import { isPromotedWidgetView } from '@/core/graph/subgraph/promotedWidgetTypes'
|
||||
import { matchPromotedInput } from '@/core/graph/subgraph/matchPromotedInput'
|
||||
import {
|
||||
resolveConcretePromotedWidget,
|
||||
@@ -69,9 +66,7 @@ export interface SafeWidgetData {
|
||||
*/
|
||||
entityId?: WidgetEntityId
|
||||
nodeId?: NodeId
|
||||
storeNodeId?: NodeId
|
||||
name: string
|
||||
storeName?: string
|
||||
type: string
|
||||
/** Callback to invoke when widget value changes (wraps LiteGraph callback + triggerDraw) */
|
||||
callback?: ((value: unknown) => void) | undefined
|
||||
@@ -339,23 +334,14 @@ function safeWidgetMapper(
|
||||
: undefined
|
||||
const name = sourceWidgetName ?? displayName
|
||||
|
||||
// Promoted widgets carry per-host values that the migration writes to a
|
||||
// host-scoped store entry. Vue rendering must read from that same entry
|
||||
// (not the shared interior key) for per-instance independence.
|
||||
let storeNodeId: NodeId | undefined = nodeId
|
||||
let storeName = sourceWidgetName
|
||||
if (isPromotedWidgetView(widget)) {
|
||||
widget.ensureHostWidgetState()
|
||||
storeNodeId = String(node.id)
|
||||
storeName = getPromotedWidgetHostStateName(widget)
|
||||
}
|
||||
// Promoted widgets carry per-host values keyed by `widget.entityId`.
|
||||
// Seed the store entry so Vue rendering can read it on first render.
|
||||
if (isPromotedWidgetView(widget)) widget.ensureHostWidgetState()
|
||||
|
||||
return {
|
||||
entityId: widget.entityId,
|
||||
nodeId,
|
||||
storeNodeId,
|
||||
name,
|
||||
storeName,
|
||||
type: effectiveWidget.type,
|
||||
...sharedEnhancements,
|
||||
callback,
|
||||
|
||||
@@ -52,12 +52,6 @@ export interface PromotedWidgetView extends IBaseWidget {
|
||||
ensureHostWidgetState(): void
|
||||
}
|
||||
|
||||
export function getPromotedWidgetHostStateName(
|
||||
widget: PromotedWidgetView
|
||||
): string {
|
||||
return [widget.name, widget.sourceNodeId, widget.sourceWidgetName].join(':')
|
||||
}
|
||||
|
||||
export function isPromotedWidgetView(
|
||||
widget: IBaseWidget
|
||||
): widget is PromotedWidgetView {
|
||||
|
||||
@@ -23,18 +23,13 @@ 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 {
|
||||
getPromotedWidgetHostStateName,
|
||||
isPromotedWidgetView
|
||||
} from './promotedWidgetTypes'
|
||||
import { isPromotedWidgetView } from './promotedWidgetTypes'
|
||||
import type { PromotedWidgetView as IPromotedWidgetView } from './promotedWidgetTypes'
|
||||
|
||||
export type { PromotedWidgetView } from './promotedWidgetTypes'
|
||||
export {
|
||||
getPromotedWidgetHostStateName,
|
||||
isPromotedWidgetView
|
||||
} from './promotedWidgetTypes'
|
||||
export { isPromotedWidgetView } from './promotedWidgetTypes'
|
||||
|
||||
interface SubgraphSlotRef {
|
||||
name: string
|
||||
@@ -230,11 +225,7 @@ class PromotedWidgetView implements IPromotedWidgetView {
|
||||
}
|
||||
|
||||
private getHostWidgetState(): WidgetState | undefined {
|
||||
return useWidgetValueStore().getWidget(
|
||||
this.graphId,
|
||||
this.subgraphNode.id,
|
||||
this.hostWidgetStateName
|
||||
)
|
||||
return getWidgetState(this.entityId)
|
||||
}
|
||||
|
||||
private setHostWidgetState(value: IBaseWidget['value']): void {
|
||||
@@ -251,9 +242,9 @@ class PromotedWidgetView implements IPromotedWidgetView {
|
||||
|
||||
/**
|
||||
* Idempotently register a host-scoped widget state seeded with the current
|
||||
* effective value. Vue rendering reads from this entry (via `safeWidgetMapper`
|
||||
* exposing the host-scoped `storeNodeId`/`storeName` for promoted widgets),
|
||||
* so the entry must exist before first render even if migration has not run.
|
||||
* effective value. Vue rendering reads from this entry keyed by
|
||||
* {@link entityId}, so it must exist before first render even if migration
|
||||
* has not run.
|
||||
*/
|
||||
ensureHostWidgetState(): void {
|
||||
if (this.getHostWidgetState()) return
|
||||
@@ -262,9 +253,7 @@ class PromotedWidgetView implements IPromotedWidgetView {
|
||||
|
||||
private registerHostWidgetState(value: IBaseWidget['value']): void {
|
||||
const resolved = this.resolveDeepest()
|
||||
useWidgetValueStore().registerWidget(this.graphId, {
|
||||
nodeId: this.subgraphNode.id,
|
||||
name: this.hostWidgetStateName,
|
||||
ensureWidgetState(this.entityId, {
|
||||
type: resolved?.widget.type ?? 'button',
|
||||
value,
|
||||
// Clone — never share the interior widget's options reference, or
|
||||
@@ -277,10 +266,6 @@ class PromotedWidgetView implements IPromotedWidgetView {
|
||||
})
|
||||
}
|
||||
|
||||
private get hostWidgetStateName(): string {
|
||||
return getPromotedWidgetHostStateName(this)
|
||||
}
|
||||
|
||||
get label(): string | undefined {
|
||||
const slot = this.getBoundSubgraphSlot()
|
||||
if (slot) return slot.label ?? slot.displayName ?? slot.name
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import * as Sentry from '@sentry/vue'
|
||||
import type { PromotedWidgetSource } from '@/core/graph/subgraph/promotedWidgetTypes'
|
||||
import {
|
||||
getPromotedWidgetHostStateName,
|
||||
isPromotedWidgetView
|
||||
} from '@/core/graph/subgraph/promotedWidgetTypes'
|
||||
import { isPromotedWidgetView } from '@/core/graph/subgraph/promotedWidgetTypes'
|
||||
import { t } from '@/i18n'
|
||||
import type {
|
||||
IContextMenuValue,
|
||||
@@ -21,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 { useWidgetValueStore } from '@/stores/widgetValueStore'
|
||||
import { readWidgetValue } from '@/world/widgetValueIO'
|
||||
|
||||
type PartialNode = Pick<LGraphNode, 'title' | 'id' | 'type'>
|
||||
|
||||
@@ -143,10 +140,7 @@ function applySubgraphInputOrder(
|
||||
const rows = subgraphNode.subgraph.inputs.map((input, index) => ({
|
||||
subgraphInput: input,
|
||||
hostInput: subgraphNode.inputs[index],
|
||||
value: getExplicitHostWidgetValue(
|
||||
subgraphNode,
|
||||
subgraphNode.inputs[index]?._widget
|
||||
)
|
||||
value: getExplicitHostWidgetValue(subgraphNode.inputs[index]?._widget)
|
||||
}))
|
||||
|
||||
const orderedRows = orderedIndices.flatMap((index) => rows[index] ?? [])
|
||||
@@ -177,18 +171,13 @@ function applySubgraphInputOrder(
|
||||
}
|
||||
|
||||
function getExplicitHostWidgetValue(
|
||||
subgraphNode: SubgraphNode,
|
||||
widget: IBaseWidget | undefined
|
||||
): IBaseWidget['value'] | undefined {
|
||||
if (!widget) return undefined
|
||||
if (!isPromotedWidgetView(widget)) return widget.value
|
||||
|
||||
const state = useWidgetValueStore().getWidget(
|
||||
subgraphNode.rootGraph.id,
|
||||
subgraphNode.id,
|
||||
getPromotedWidgetHostStateName(widget)
|
||||
)
|
||||
return state && isWidgetValue(state.value) ? state.value : undefined
|
||||
const value = readWidgetValue(widget.entityId)
|
||||
return isWidgetValue(value) ? value : undefined
|
||||
}
|
||||
|
||||
function isSamePromotedWidget(left: IBaseWidget, right: IBaseWidget): boolean {
|
||||
|
||||
@@ -34,7 +34,6 @@ import type {
|
||||
} from '@/lib/litegraph/src/types/widgets'
|
||||
import {
|
||||
createPromotedWidgetView,
|
||||
getPromotedWidgetHostStateName,
|
||||
isPromotedWidgetView
|
||||
} from '@/core/graph/subgraph/promotedWidgetView'
|
||||
import type { PromotedWidgetView } from '@/core/graph/subgraph/promotedWidgetView'
|
||||
@@ -48,7 +47,7 @@ import { parsePreviewExposures } from '@/core/schemas/previewExposureSchema'
|
||||
import { parseProxyWidgetErrorQuarantine } from '@/core/schemas/proxyWidgetQuarantineSchema'
|
||||
import { useDomWidgetStore } from '@/stores/domWidgetStore'
|
||||
import { usePreviewExposureStore } from '@/stores/previewExposureStore'
|
||||
import { useWidgetValueStore } from '@/stores/widgetValueStore'
|
||||
import { readWidgetValue } from '@/world/widgetValueIO'
|
||||
|
||||
import { ExecutableNodeDTO } from './ExecutableNodeDTO'
|
||||
import type { ExecutableLGraphNode, ExecutionId } from './ExecutableNodeDTO'
|
||||
@@ -1168,16 +1167,11 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
|
||||
|
||||
serialized.properties = serializedProperties
|
||||
|
||||
const widgetStore = useWidgetValueStore()
|
||||
const widgetValues = this.inputs.flatMap((input) => {
|
||||
const widget = input._widget
|
||||
if (!widget || !isPromotedWidgetView(widget)) return []
|
||||
const state = widgetStore.getWidget(
|
||||
rootGraphId,
|
||||
this.id,
|
||||
getPromotedWidgetHostStateName(widget)
|
||||
)
|
||||
return [state && isWidgetValue(state.value) ? state.value : undefined]
|
||||
const value = readWidgetValue(widget.entityId)
|
||||
return [isWidgetValue(value) ? value : undefined]
|
||||
})
|
||||
|
||||
if (widgetValues.some((value) => value !== undefined)) {
|
||||
|
||||
@@ -12,6 +12,9 @@ import type {
|
||||
} from '@/composables/graph/useGraphNodeManager'
|
||||
import NodeWidgets from '@/renderer/extensions/vueNodes/components/NodeWidgets.vue'
|
||||
import { useWidgetValueStore } from '@/stores/widgetValueStore'
|
||||
import { widgetEntityId } from '@/world/entityIds'
|
||||
|
||||
const GRAPH_ID = 'graph-test'
|
||||
|
||||
vi.mock('@/renderer/core/canvas/canvasStore', () => ({
|
||||
useCanvasStore: () => ({
|
||||
@@ -126,28 +129,35 @@ describe('NodeWidgets', () => {
|
||||
})
|
||||
|
||||
it('deduplicates widgets with identical render identity while keeping distinct promoted sources', () => {
|
||||
const duplicateEntityId = widgetEntityId(
|
||||
GRAPH_ID,
|
||||
'5e0670b8-ea2c-4fb6-8b73-a1100a2d4f8f:19',
|
||||
'string_a'
|
||||
)
|
||||
const distinctEntityId = widgetEntityId(
|
||||
GRAPH_ID,
|
||||
'5e0670b8-ea2c-4fb6-8b73-a1100a2d4f8f:20',
|
||||
'string_a'
|
||||
)
|
||||
const duplicateA = createMockWidget({
|
||||
name: 'string_a',
|
||||
type: 'text',
|
||||
nodeId: '5e0670b8-ea2c-4fb6-8b73-a1100a2d4f8f:19',
|
||||
storeNodeId: '5e0670b8-ea2c-4fb6-8b73-a1100a2d4f8f:19',
|
||||
storeName: 'string_a',
|
||||
entityId: duplicateEntityId,
|
||||
slotName: 'string_a'
|
||||
})
|
||||
const duplicateB = createMockWidget({
|
||||
name: 'string_a',
|
||||
type: 'text',
|
||||
nodeId: '5e0670b8-ea2c-4fb6-8b73-a1100a2d4f8f:19',
|
||||
storeNodeId: '5e0670b8-ea2c-4fb6-8b73-a1100a2d4f8f:19',
|
||||
storeName: 'string_a',
|
||||
entityId: duplicateEntityId,
|
||||
slotName: 'string_a'
|
||||
})
|
||||
const distinct = createMockWidget({
|
||||
name: 'string_a',
|
||||
type: 'text',
|
||||
nodeId: '5e0670b8-ea2c-4fb6-8b73-a1100a2d4f8f:20',
|
||||
storeNodeId: '5e0670b8-ea2c-4fb6-8b73-a1100a2d4f8f:20',
|
||||
storeName: 'string_a',
|
||||
entityId: distinctEntityId,
|
||||
slotName: 'string_a'
|
||||
})
|
||||
const nodeData = createMockNodeData('SubgraphNode', [
|
||||
@@ -162,12 +172,16 @@ describe('NodeWidgets', () => {
|
||||
})
|
||||
|
||||
it('prefers a visible duplicate over a hidden duplicate when identities collide', () => {
|
||||
const sharedEntityId = widgetEntityId(
|
||||
GRAPH_ID,
|
||||
'5e0670b8-ea2c-4fb6-8b73-a1100a2d4f8f:19',
|
||||
'string_a'
|
||||
)
|
||||
const hiddenDuplicate = createMockWidget({
|
||||
name: 'string_a',
|
||||
type: 'text',
|
||||
nodeId: '5e0670b8-ea2c-4fb6-8b73-a1100a2d4f8f:19',
|
||||
storeNodeId: '5e0670b8-ea2c-4fb6-8b73-a1100a2d4f8f:19',
|
||||
storeName: 'string_a',
|
||||
entityId: sharedEntityId,
|
||||
slotName: 'string_a',
|
||||
options: { hidden: true }
|
||||
})
|
||||
@@ -175,8 +189,7 @@ describe('NodeWidgets', () => {
|
||||
name: 'string_a',
|
||||
type: 'text',
|
||||
nodeId: '5e0670b8-ea2c-4fb6-8b73-a1100a2d4f8f:19',
|
||||
storeNodeId: '5e0670b8-ea2c-4fb6-8b73-a1100a2d4f8f:19',
|
||||
storeName: 'string_a',
|
||||
entityId: sharedEntityId,
|
||||
slotName: 'string_a',
|
||||
options: { hidden: false }
|
||||
})
|
||||
@@ -191,20 +204,23 @@ describe('NodeWidgets', () => {
|
||||
})
|
||||
|
||||
it('does not deduplicate entries that share names but have different widget types', () => {
|
||||
const sharedEntityId = widgetEntityId(
|
||||
GRAPH_ID,
|
||||
'5e0670b8-ea2c-4fb6-8b73-a1100a2d4f8f:19',
|
||||
'string_a'
|
||||
)
|
||||
const textWidget = createMockWidget({
|
||||
name: 'string_a',
|
||||
type: 'text',
|
||||
nodeId: '5e0670b8-ea2c-4fb6-8b73-a1100a2d4f8f:19',
|
||||
storeNodeId: '5e0670b8-ea2c-4fb6-8b73-a1100a2d4f8f:19',
|
||||
storeName: 'string_a',
|
||||
entityId: sharedEntityId,
|
||||
slotName: 'string_a'
|
||||
})
|
||||
const comboWidget = createMockWidget({
|
||||
name: 'string_a',
|
||||
type: 'combo',
|
||||
nodeId: '5e0670b8-ea2c-4fb6-8b73-a1100a2d4f8f:19',
|
||||
storeNodeId: '5e0670b8-ea2c-4fb6-8b73-a1100a2d4f8f:19',
|
||||
storeName: 'string_a',
|
||||
entityId: sharedEntityId,
|
||||
slotName: 'string_a'
|
||||
})
|
||||
const nodeData = createMockNodeData('SubgraphNode', [
|
||||
@@ -220,18 +236,14 @@ describe('NodeWidgets', () => {
|
||||
it('keeps unresolved same-name promoted entries distinct by source execution identity', () => {
|
||||
const firstTransientEntry = createMockWidget({
|
||||
nodeId: undefined,
|
||||
storeNodeId: undefined,
|
||||
name: 'string_a',
|
||||
storeName: 'string_a',
|
||||
slotName: 'string_a',
|
||||
type: 'text',
|
||||
sourceExecutionId: '65:18'
|
||||
})
|
||||
const secondTransientEntry = createMockWidget({
|
||||
nodeId: undefined,
|
||||
storeNodeId: undefined,
|
||||
name: 'string_a',
|
||||
storeName: 'string_a',
|
||||
slotName: 'string_a',
|
||||
type: 'text',
|
||||
sourceExecutionId: '65:19'
|
||||
@@ -251,16 +263,14 @@ describe('NodeWidgets', () => {
|
||||
name: 'text',
|
||||
type: 'text',
|
||||
nodeId: 'outer-subgraph:1',
|
||||
storeNodeId: 'outer-subgraph:1',
|
||||
storeName: 'text',
|
||||
entityId: widgetEntityId(GRAPH_ID, 'outer-subgraph:1', 'text'),
|
||||
slotName: 'text'
|
||||
})
|
||||
const secondPromoted = createMockWidget({
|
||||
name: 'text',
|
||||
type: 'text',
|
||||
nodeId: 'outer-subgraph:2',
|
||||
storeNodeId: 'outer-subgraph:2',
|
||||
storeName: 'text',
|
||||
entityId: widgetEntityId(GRAPH_ID, 'outer-subgraph:2', 'text'),
|
||||
slotName: 'text'
|
||||
})
|
||||
|
||||
|
||||
@@ -13,6 +13,9 @@ import {
|
||||
import { useExecutionErrorStore } from '@/stores/executionErrorStore'
|
||||
import { useMissingModelStore } from '@/platform/missingModel/missingModelStore'
|
||||
import { useWidgetValueStore } from '@/stores/widgetValueStore'
|
||||
import { widgetEntityId } from '@/world/entityIds'
|
||||
|
||||
const GRAPH_ID = 'graph-test'
|
||||
|
||||
vi.mock('@/renderer/core/canvas/canvasStore', () => ({
|
||||
useCanvasStore: () => ({
|
||||
@@ -41,37 +44,19 @@ const createMockWidget = (
|
||||
})
|
||||
|
||||
describe('getWidgetIdentity', () => {
|
||||
it('returns stable dedupeIdentity for widgets with storeNodeId', () => {
|
||||
const widget = createMockWidget({
|
||||
storeNodeId: 'subgraph:19',
|
||||
storeName: 'text',
|
||||
slotName: 'text',
|
||||
type: 'text'
|
||||
})
|
||||
it('keys dedupeIdentity by entityId and widget type', () => {
|
||||
const entityId = widgetEntityId(GRAPH_ID, 'subgraph:19', 'text')
|
||||
const widget = createMockWidget({ entityId, name: 'text', type: 'text' })
|
||||
const { dedupeIdentity, renderKey } = getWidgetIdentity(widget, '1', 0)
|
||||
expect(dedupeIdentity).toBe('node:19:text:text:text')
|
||||
expect(dedupeIdentity).toBe(`${entityId}:text`)
|
||||
expect(renderKey).toBe(dedupeIdentity)
|
||||
})
|
||||
|
||||
it('returns transient renderKey for widgets without stable identity', () => {
|
||||
const widget = createMockWidget({
|
||||
nodeId: undefined,
|
||||
storeNodeId: undefined,
|
||||
sourceExecutionId: undefined
|
||||
})
|
||||
it('falls back to a transient renderKey when no entityId is set', () => {
|
||||
const widget = createMockWidget({ entityId: undefined })
|
||||
const { dedupeIdentity, renderKey } = getWidgetIdentity(widget, '5', 3)
|
||||
expect(dedupeIdentity).toBeUndefined()
|
||||
expect(renderKey).toBe('transient:5:test_widget:test_widget:combo:3')
|
||||
})
|
||||
|
||||
it('uses sourceExecutionId for identity when no nodeId', () => {
|
||||
const widget = createMockWidget({
|
||||
nodeId: undefined,
|
||||
storeNodeId: undefined,
|
||||
sourceExecutionId: '65:18'
|
||||
})
|
||||
const { dedupeIdentity } = getWidgetIdentity(widget, '1', 0)
|
||||
expect(dedupeIdentity).toBe('exec:65:18:test_widget:test_widget:combo')
|
||||
expect(renderKey).toBe('transient:5:test_widget:combo:3')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -211,8 +196,7 @@ describe('computeProcessedWidgets borderStyle', () => {
|
||||
name: 'text',
|
||||
type: 'combo',
|
||||
nodeId: 'inner-subgraph:1',
|
||||
storeNodeId: 'inner-subgraph:1',
|
||||
storeName: 'text',
|
||||
entityId: widgetEntityId(GRAPH_ID, 'inner-subgraph:1', 'text'),
|
||||
slotName: 'text',
|
||||
promotedLabel: 'Text'
|
||||
})
|
||||
@@ -245,8 +229,7 @@ describe('computeProcessedWidgets borderStyle', () => {
|
||||
name: 'text',
|
||||
type: 'combo',
|
||||
nodeId: 'inner-subgraph:1',
|
||||
storeNodeId: 'inner-subgraph:1',
|
||||
storeName: 'text',
|
||||
entityId: widgetEntityId(GRAPH_ID, 'inner-subgraph:1', 'text'),
|
||||
slotName: 'text'
|
||||
})
|
||||
|
||||
@@ -306,12 +289,12 @@ describe('computeProcessedWidgets borderStyle', () => {
|
||||
})
|
||||
|
||||
it('deduplication keeps visible widget over hidden duplicate', () => {
|
||||
const sharedEntityId = widgetEntityId(GRAPH_ID, '1', 'text')
|
||||
const hiddenWidget = createMockWidget({
|
||||
name: 'text',
|
||||
type: 'combo',
|
||||
nodeId: '1',
|
||||
storeNodeId: '1',
|
||||
storeName: 'text',
|
||||
entityId: sharedEntityId,
|
||||
slotName: 'text',
|
||||
options: { hidden: true }
|
||||
})
|
||||
@@ -320,8 +303,7 @@ describe('computeProcessedWidgets borderStyle', () => {
|
||||
name: 'text',
|
||||
type: 'combo',
|
||||
nodeId: '1',
|
||||
storeNodeId: '1',
|
||||
storeName: 'text',
|
||||
entityId: sharedEntityId,
|
||||
slotName: 'text'
|
||||
})
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
} from '@/stores/widgetValueStore'
|
||||
import { useMissingModelStore } from '@/platform/missingModel/missingModelStore'
|
||||
import { useExecutionErrorStore } from '@/stores/executionErrorStore'
|
||||
import { getWidgetState } from '@/world/widgetValueIO'
|
||||
import type { LGraph } from '@/lib/litegraph/src/litegraph'
|
||||
import type {
|
||||
LinkedUpstreamInfo,
|
||||
@@ -125,26 +126,13 @@ export function getWidgetIdentity(
|
||||
dedupeIdentity?: string
|
||||
renderKey: string
|
||||
} {
|
||||
const rawWidgetId = widget.storeNodeId ?? widget.nodeId
|
||||
const storeWidgetName = widget.storeName ?? widget.name
|
||||
const slotNameForIdentity = widget.slotName ?? widget.name
|
||||
const stableIdentityRoot = rawWidgetId
|
||||
? `node:${String(stripGraphPrefix(rawWidgetId))}`
|
||||
: widget.sourceExecutionId
|
||||
? `exec:${widget.sourceExecutionId}`
|
||||
: undefined
|
||||
|
||||
const dedupeIdentity = stableIdentityRoot
|
||||
? `${stableIdentityRoot}:${storeWidgetName}:${slotNameForIdentity}:${widget.type}`
|
||||
const dedupeIdentity = widget.entityId
|
||||
? `${widget.entityId}:${widget.type}`
|
||||
: undefined
|
||||
const renderKey =
|
||||
dedupeIdentity ??
|
||||
`transient:${String(nodeId ?? '')}:${storeWidgetName}:${slotNameForIdentity}:${widget.type}:${index}`
|
||||
|
||||
return {
|
||||
dedupeIdentity,
|
||||
renderKey
|
||||
}
|
||||
`transient:${String(nodeId ?? '')}:${widget.name}:${widget.type}:${index}`
|
||||
return { dedupeIdentity, renderKey }
|
||||
}
|
||||
|
||||
export function isWidgetVisible(
|
||||
@@ -193,13 +181,15 @@ export function computeProcessedWidgets({
|
||||
if (!shouldRenderAsVue(widget)) continue
|
||||
|
||||
const identity = getWidgetIdentity(widget, nodeId, index)
|
||||
const storeWidgetName = widget.storeName ?? widget.name
|
||||
const bareWidgetId = String(
|
||||
stripGraphPrefix(widget.storeNodeId ?? widget.nodeId ?? nodeId ?? '')
|
||||
)
|
||||
const widgetState = graphId
|
||||
? widgetValueStore.getWidget(graphId, bareWidgetId, storeWidgetName)
|
||||
: undefined
|
||||
const widgetState = widget.entityId
|
||||
? getWidgetState(widget.entityId)
|
||||
: graphId
|
||||
? widgetValueStore.getWidget(
|
||||
graphId,
|
||||
String(stripGraphPrefix(widget.nodeId ?? nodeId ?? '')),
|
||||
widget.name
|
||||
)
|
||||
: undefined
|
||||
const mergedOptions: IWidgetOptions = {
|
||||
...(widget.options ?? {}),
|
||||
...(widgetState?.options ?? {})
|
||||
@@ -247,9 +237,7 @@ export function computeProcessedWidgets({
|
||||
widgetState,
|
||||
identity: { renderKey }
|
||||
} of uniqueWidgets) {
|
||||
const bareWidgetId = String(
|
||||
stripGraphPrefix(widget.storeNodeId ?? widget.nodeId ?? nodeId ?? '')
|
||||
)
|
||||
const bareWidgetId = String(stripGraphPrefix(widget.nodeId ?? nodeId ?? ''))
|
||||
|
||||
const vueComponent =
|
||||
getComponent(widget.type) ||
|
||||
|
||||
Reference in New Issue
Block a user