Files
ComfyUI_frontend/src/platform/assets/composables/useOutputStacks.ts
Benjamin Lu 2740c7cdd5 Add expandable output stacks to assets list view (#8283)
Add expandable output stacks to the assets list view.

Monolith ver. of https://github.com/Comfy-Org/ComfyUI_frontend/pull/8298
and its children

List view currently collapses multi-output jobs into a single row, which
makes sibling outputs easy to miss and causes selection/zoom behavior to
drift once items are expanded elsewhere. This change adds a stack toggle
to list rows, expands child outputs derived from job data, and keeps
list-view selection and gallery navigation aligned with the expanded
list. Output mapping and “load full outputs” checks are centralized so
folder view and stacks share the same helper, and job-detail parsing now
yields previewable outputs for the list view. Asset actions now prefer
metadata prompt IDs to support the composite IDs used by stacked
outputs.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8283-Add-expandable-output-stacks-to-assets-list-view-2f16d73d365081a99fc6f1519ac2e57c)
by [Unito](https://www.unito.io)

---------

Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Johnpaul Chiwetelu <49923152+Myestery@users.noreply.github.com>
2026-02-02 03:31:01 -08:00

139 lines
3.6 KiB
TypeScript

import { computed, ref } from 'vue'
import type { Ref } from 'vue'
import { getOutputAssetMetadata } from '@/platform/assets/schemas/assetMetadataSchema'
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
import {
getOutputKey,
resolveOutputAssetItems
} from '@/platform/assets/utils/outputAssetUtil'
export type OutputStackListItem = {
key: string
asset: AssetItem
isChild?: boolean
}
type UseOutputStacksOptions = {
assets: Ref<AssetItem[]>
}
export function useOutputStacks({ assets }: UseOutputStacksOptions) {
const expandedStackPromptIds = ref<Set<string>>(new Set())
const stackChildrenByPromptId = ref<Record<string, AssetItem[]>>({})
const loadingStackPromptIds = ref<Set<string>>(new Set())
const assetItems = computed<OutputStackListItem[]>(() => {
const items: OutputStackListItem[] = []
for (const asset of assets.value) {
const promptId = getStackPromptId(asset)
items.push({
key: `asset-${asset.id}`,
asset
})
if (!promptId || !expandedStackPromptIds.value.has(promptId)) {
continue
}
const children = stackChildrenByPromptId.value[promptId] ?? []
for (const child of children) {
items.push({
key: `asset-${child.id}`,
asset: child,
isChild: true
})
}
}
return items
})
const selectableAssets = computed(() =>
assetItems.value.map((item) => item.asset)
)
function getStackPromptId(asset: AssetItem): string | null {
const metadata = getOutputAssetMetadata(asset.user_metadata)
return metadata?.promptId ?? null
}
function isStackExpanded(asset: AssetItem): boolean {
const promptId = getStackPromptId(asset)
if (!promptId) return false
return expandedStackPromptIds.value.has(promptId)
}
async function toggleStack(asset: AssetItem) {
const promptId = getStackPromptId(asset)
if (!promptId) return
if (expandedStackPromptIds.value.has(promptId)) {
const next = new Set(expandedStackPromptIds.value)
next.delete(promptId)
expandedStackPromptIds.value = next
return
}
if (!stackChildrenByPromptId.value[promptId]?.length) {
if (loadingStackPromptIds.value.has(promptId)) {
return
}
const nextLoading = new Set(loadingStackPromptIds.value)
nextLoading.add(promptId)
loadingStackPromptIds.value = nextLoading
const children = await resolveStackChildren(asset)
const afterLoading = new Set(loadingStackPromptIds.value)
afterLoading.delete(promptId)
loadingStackPromptIds.value = afterLoading
if (!children.length) {
return
}
stackChildrenByPromptId.value = {
...stackChildrenByPromptId.value,
[promptId]: children
}
}
const nextExpanded = new Set(expandedStackPromptIds.value)
nextExpanded.add(promptId)
expandedStackPromptIds.value = nextExpanded
}
async function resolveStackChildren(asset: AssetItem): Promise<AssetItem[]> {
const metadata = getOutputAssetMetadata(asset.user_metadata)
if (!metadata) {
return []
}
const excludeOutputKey =
getOutputKey({
nodeId: metadata.nodeId,
subfolder: metadata.subfolder,
filename: asset.name
}) ?? undefined
try {
return await resolveOutputAssetItems(metadata, {
createdAt: asset.created_at,
excludeOutputKey
})
} catch (error) {
console.error('Failed to resolve stack children:', error)
return []
}
}
return {
assetItems,
selectableAssets,
isStackExpanded,
toggleStack
}
}