Files
ComfyUI_frontend/src/composables/node/usePromotedPreviews.ts
Alexander Brown fe489ec87c [backport core/1.42] test: subgraph integration contracts and expanded Playwright coverage (#10327)
Backport of #10123, #9967, and #9972 to `core/1.42`

Includes three cherry-picks in dependency order:
1. #9972 — `fix: resolve all lint warnings` (clean)
2. #9967 — `test: harden subgraph test coverage and remove low-value
tests` (clean)
3. #10123 — `test: subgraph integration contracts and expanded
Playwright coverage` (1 conflict, auto-resolved by rerere from #10326)

See #10326 for core/1.41 backport with detailed conflict resolution
notes.

---------

Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: bymyself <cbyrne@comfy.org>
Co-authored-by: GitHub Action <action@github.com>
2026-03-19 18:24:31 -07:00

80 lines
2.5 KiB
TypeScript

import type { MaybeRefOrGetter } from 'vue'
import { computed, toValue } from 'vue'
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
import { SubgraphNode } from '@/lib/litegraph/src/subgraph/SubgraphNode'
import { useNodeOutputStore } from '@/stores/nodeOutputStore'
import { usePromotionStore } from '@/stores/promotionStore'
import { createNodeLocatorId } from '@/types/nodeIdentification'
interface PromotedPreview {
sourceNodeId: string
sourceWidgetName: string
type: 'image' | 'video' | 'audio'
urls: string[]
}
/**
* Returns reactive preview media from promoted `$$` pseudo-widgets
* on a SubgraphNode. Each promoted preview interior node produces
* a separate entry so they render independently.
*/
export function usePromotedPreviews(
lgraphNode: MaybeRefOrGetter<LGraphNode | null | undefined>
) {
const promotionStore = usePromotionStore()
const nodeOutputStore = useNodeOutputStore()
const promotedPreviews = computed((): PromotedPreview[] => {
const node = toValue(lgraphNode)
if (!(node instanceof SubgraphNode)) return []
const entries = promotionStore.getPromotions(node.rootGraph.id, node.id)
const pseudoEntries = entries.filter((e) =>
e.sourceWidgetName.startsWith('$$')
)
if (!pseudoEntries.length) return []
const previews: PromotedPreview[] = []
for (const entry of pseudoEntries) {
const interiorNode = node.subgraph.getNodeById(entry.sourceNodeId)
if (!interiorNode) continue
// Read from both reactive refs to establish Vue dependency
// tracking. getNodeImageUrls reads from non-reactive
// app.nodeOutputs / app.nodePreviewImages, so without this
// access the computed would never re-evaluate.
const locatorId = createNodeLocatorId(
node.subgraph.id,
entry.sourceNodeId
)
const reactiveOutputs = nodeOutputStore.nodeOutputs[locatorId]
const reactivePreviews = nodeOutputStore.nodePreviewImages[locatorId]
if (!reactiveOutputs?.images?.length && !reactivePreviews?.length)
continue
const urls = nodeOutputStore.getNodeImageUrls(interiorNode)
if (!urls?.length) continue
const type =
interiorNode.previewMediaType === 'video'
? 'video'
: interiorNode.previewMediaType === 'audio'
? 'audio'
: 'image'
previews.push({
sourceNodeId: entry.sourceNodeId,
sourceWidgetName: entry.sourceWidgetName,
type,
urls
})
}
return previews
})
return { promotedPreviews }
}