mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-14 09:27:41 +00:00
Add stacked functionality to list view
This commit is contained in:
@@ -72,7 +72,12 @@
|
||||
type: getMediaTypeFromFilename(item.asset.name)
|
||||
})
|
||||
"
|
||||
:class="getAssetCardClass(isSelected(item.asset.id))"
|
||||
:class="
|
||||
cn(
|
||||
getAssetCardClass(isSelected(item.asset.id)),
|
||||
item.isChild && 'pl-6'
|
||||
)
|
||||
"
|
||||
:preview-url="item.asset.preview_url"
|
||||
:preview-alt="item.asset.name"
|
||||
:icon-name="
|
||||
@@ -82,10 +87,12 @@
|
||||
:secondary-text="getAssetSecondaryText(item.asset)"
|
||||
:stack-count="getStackCount(item.asset)"
|
||||
:stack-indicator-label="t('mediaAsset.actions.seeMoreOutputs')"
|
||||
:stack-expanded="isStackExpanded(item.asset)"
|
||||
@mouseenter="onAssetEnter(item.asset.id)"
|
||||
@mouseleave="onAssetLeave(item.asset.id)"
|
||||
@contextmenu.prevent.stop="emit('context-menu', $event, item.asset)"
|
||||
@click.stop="emit('select-asset', item.asset)"
|
||||
@click.stop="emit('select-asset', item.asset, selectableAssets)"
|
||||
@stack-toggle="toggleStack(item.asset)"
|
||||
>
|
||||
<template v-if="hoveredAssetId === item.asset.id" #actions>
|
||||
<Button
|
||||
@@ -104,7 +111,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import VirtualGrid from '@/components/common/VirtualGrid.vue'
|
||||
@@ -113,9 +120,18 @@ import { useJobActions } from '@/composables/queue/useJobActions'
|
||||
import type { JobListItem } from '@/composables/queue/useJobList'
|
||||
import { useJobList } from '@/composables/queue/useJobList'
|
||||
import AssetsListItem from '@/platform/assets/components/AssetsListItem.vue'
|
||||
import { getOutputAssetMetadata } from '@/platform/assets/schemas/assetMetadataSchema'
|
||||
import { getOutputAssetMetadata } from '@/platform/assets/schemas/assetMetadataSchema';
|
||||
import type { OutputAssetMetadata } from '@/platform/assets/schemas/assetMetadataSchema';
|
||||
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
|
||||
import {
|
||||
mapOutputsToAssetItems,
|
||||
shouldLoadFullOutputs
|
||||
} from '@/platform/assets/utils/outputAssetUtil'
|
||||
import { iconForMediaType } from '@/platform/assets/utils/mediaIconUtil'
|
||||
import {
|
||||
getJobDetail,
|
||||
getPreviewableOutputsFromJobDetail
|
||||
} from '@/services/jobOutputCache'
|
||||
import { isActiveJobState } from '@/utils/queueUtil'
|
||||
import {
|
||||
formatDuration,
|
||||
@@ -137,17 +153,25 @@ const {
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'select-asset', asset: AssetItem): void
|
||||
(e: 'select-asset', asset: AssetItem, assets?: AssetItem[]): void
|
||||
(e: 'context-menu', event: MouseEvent, asset: AssetItem): void
|
||||
(e: 'approach-end'): void
|
||||
(e: 'assets-change', assets: AssetItem[]): void
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const { jobItems } = useJobList()
|
||||
const hoveredJobId = ref<string | null>(null)
|
||||
const hoveredAssetId = ref<string | null>(null)
|
||||
const expandedStackPromptIds = ref<Set<string>>(new Set())
|
||||
const stackChildrenByPromptId = ref<Record<string, AssetItem[]>>({})
|
||||
const loadingStackPromptIds = ref<Set<string>>(new Set())
|
||||
|
||||
type AssetListItem = { key: string; asset: AssetItem }
|
||||
type AssetListItem = {
|
||||
key: string
|
||||
asset: AssetItem
|
||||
isChild?: boolean
|
||||
}
|
||||
|
||||
const activeJobItems = computed(() =>
|
||||
jobItems.value.filter((item) => isActiveJobState(item.state))
|
||||
@@ -160,11 +184,43 @@ const hoveredJob = computed(() =>
|
||||
)
|
||||
const { cancelAction, canCancelJob, runCancelJob } = useJobActions(hoveredJob)
|
||||
|
||||
const assetItems = computed<AssetListItem[]>(() =>
|
||||
assets.map((asset) => ({
|
||||
key: `asset-${asset.id}`,
|
||||
asset
|
||||
}))
|
||||
const assetItems = computed<AssetListItem[]>(() => {
|
||||
const items: AssetListItem[] = []
|
||||
|
||||
for (const asset of assets) {
|
||||
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)
|
||||
)
|
||||
|
||||
watch(
|
||||
selectableAssets,
|
||||
(nextAssets) => {
|
||||
emit('assets-change', nextAssets)
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
const listGridStyle = {
|
||||
@@ -196,6 +252,11 @@ function getAssetSecondaryText(asset: AssetItem): string {
|
||||
return ''
|
||||
}
|
||||
|
||||
function getStackPromptId(asset: AssetItem): string | null {
|
||||
const metadata = getOutputAssetMetadata(asset.user_metadata)
|
||||
return metadata?.promptId ?? null
|
||||
}
|
||||
|
||||
function getStackCount(asset: AssetItem): number | undefined {
|
||||
const metadata = getOutputAssetMetadata(asset.user_metadata)
|
||||
if (typeof metadata?.outputCount === 'number') {
|
||||
@@ -209,6 +270,89 @@ function getStackCount(asset: AssetItem): number | undefined {
|
||||
return undefined
|
||||
}
|
||||
|
||||
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 resolveStackOutputs(metadata: OutputAssetMetadata) {
|
||||
const outputsToDisplay = metadata.allOutputs ?? []
|
||||
if (!shouldLoadFullOutputs(metadata.outputCount, outputsToDisplay.length)) {
|
||||
return outputsToDisplay
|
||||
}
|
||||
|
||||
try {
|
||||
const jobDetail = await getJobDetail(metadata.promptId)
|
||||
const previewableOutputs = getPreviewableOutputsFromJobDetail(jobDetail)
|
||||
return previewableOutputs.length ? previewableOutputs : outputsToDisplay
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch job detail for stack children:', error)
|
||||
return outputsToDisplay
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveStackChildren(asset: AssetItem): Promise<AssetItem[]> {
|
||||
const metadata = getOutputAssetMetadata(asset.user_metadata)
|
||||
if (!metadata) {
|
||||
return []
|
||||
}
|
||||
|
||||
const outputsToDisplay = await resolveStackOutputs(metadata)
|
||||
if (!outputsToDisplay.length) {
|
||||
return []
|
||||
}
|
||||
|
||||
return mapOutputsToAssetItems({
|
||||
promptId: metadata.promptId,
|
||||
outputs: outputsToDisplay,
|
||||
createdAt: asset.created_at,
|
||||
executionTimeInSeconds: metadata.executionTimeInSeconds,
|
||||
workflow: metadata.workflow,
|
||||
excludeFilename: asset.name
|
||||
})
|
||||
}
|
||||
|
||||
function getAssetCardClass(selected: boolean): string {
|
||||
return cn(
|
||||
'w-full text-text-primary transition-colors hover:bg-secondary-background-hover',
|
||||
|
||||
@@ -102,6 +102,7 @@
|
||||
:is-selected="isSelected"
|
||||
:asset-type="activeTab"
|
||||
@select-asset="handleAssetSelect"
|
||||
@assets-change="handleListViewAssetsChange"
|
||||
@context-menu="handleAssetContextMenu"
|
||||
@approach-end="handleApproachEnd"
|
||||
/>
|
||||
@@ -225,12 +226,20 @@ import { useMediaAssets } from '@/platform/assets/composables/media/useMediaAsse
|
||||
import { useAssetSelection } from '@/platform/assets/composables/useAssetSelection'
|
||||
import { useMediaAssetActions } from '@/platform/assets/composables/useMediaAssetActions'
|
||||
import { useMediaAssetFiltering } from '@/platform/assets/composables/useMediaAssetFiltering'
|
||||
import { getOutputAssetMetadata } from '@/platform/assets/schemas/assetMetadataSchema'
|
||||
import { getOutputAssetMetadata } from '@/platform/assets/schemas/assetMetadataSchema';
|
||||
import type { OutputAssetMetadata } from '@/platform/assets/schemas/assetMetadataSchema';
|
||||
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
|
||||
import type { MediaKind } from '@/platform/assets/schemas/mediaAssetSchema'
|
||||
import {
|
||||
mapOutputsToAssetItems,
|
||||
shouldLoadFullOutputs
|
||||
} from '@/platform/assets/utils/outputAssetUtil'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { getJobDetail } from '@/services/jobOutputCache'
|
||||
import {
|
||||
getJobDetail,
|
||||
getPreviewableOutputsFromJobDetail
|
||||
} from '@/services/jobOutputCache'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
@@ -238,12 +247,6 @@ import { ResultItemImpl, useQueueStore } from '@/stores/queueStore'
|
||||
import { formatDuration, getMediaTypeFromFilename } from '@/utils/formatUtil'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
interface JobOutputItem {
|
||||
filename: string
|
||||
subfolder: string
|
||||
type: string
|
||||
}
|
||||
|
||||
const { t, n } = useI18n()
|
||||
const commandStore = useCommandStore()
|
||||
const queueStore = useQueueStore()
|
||||
@@ -384,7 +387,13 @@ const displayAssets = computed(() => {
|
||||
return filteredAssets.value
|
||||
})
|
||||
|
||||
const selectedAssets = computed(() => getSelectedAssets(displayAssets.value))
|
||||
const listViewAssets = ref<AssetItem[]>([])
|
||||
|
||||
const selectionAssets = computed(() =>
|
||||
isListView.value ? listViewAssets.value : displayAssets.value
|
||||
)
|
||||
|
||||
const selectedAssets = computed(() => getSelectedAssets(selectionAssets.value))
|
||||
|
||||
const isBulkMode = computed(
|
||||
() => hasSelection.value && selectedAssets.value.length > 1
|
||||
@@ -462,9 +471,14 @@ watch(
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
const handleAssetSelect = (asset: AssetItem) => {
|
||||
const index = displayAssets.value.findIndex((a) => a.id === asset.id)
|
||||
handleAssetClick(asset, index, displayAssets.value)
|
||||
const handleAssetSelect = (asset: AssetItem, assets?: AssetItem[]) => {
|
||||
const assetList = assets ?? selectionAssets.value
|
||||
const index = assetList.findIndex((a) => a.id === asset.id)
|
||||
handleAssetClick(asset, index, assetList)
|
||||
}
|
||||
|
||||
const handleListViewAssetsChange = (assets: AssetItem[]) => {
|
||||
listViewAssets.value = assets
|
||||
}
|
||||
|
||||
function handleAssetContextMenu(event: MouseEvent, asset: AssetItem) {
|
||||
@@ -543,6 +557,17 @@ const handleZoomClick = (asset: AssetItem) => {
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveFolderOutputs(metadata: OutputAssetMetadata) {
|
||||
const outputsToDisplay = metadata.allOutputs ?? []
|
||||
if (!shouldLoadFullOutputs(metadata.outputCount, outputsToDisplay.length)) {
|
||||
return outputsToDisplay
|
||||
}
|
||||
|
||||
const jobDetail = await getJobDetail(metadata.promptId)
|
||||
const previewableOutputs = getPreviewableOutputsFromJobDetail(jobDetail)
|
||||
return previewableOutputs.length ? previewableOutputs : outputsToDisplay
|
||||
}
|
||||
|
||||
const enterFolderView = async (asset: AssetItem) => {
|
||||
const metadata = getOutputAssetMetadata(asset.user_metadata)
|
||||
if (!metadata) {
|
||||
@@ -550,7 +575,7 @@ const enterFolderView = async (asset: AssetItem) => {
|
||||
return
|
||||
}
|
||||
|
||||
const { promptId, allOutputs, executionTimeInSeconds, outputCount } = metadata
|
||||
const { promptId, executionTimeInSeconds } = metadata
|
||||
|
||||
if (!promptId) {
|
||||
console.warn('Missing required folder view data')
|
||||
@@ -560,62 +585,20 @@ const enterFolderView = async (asset: AssetItem) => {
|
||||
folderPromptId.value = promptId
|
||||
folderExecutionTime.value = executionTimeInSeconds
|
||||
|
||||
// Determine which outputs to display
|
||||
let outputsToDisplay = allOutputs ?? []
|
||||
|
||||
// If outputCount indicates more outputs than we have, fetch full outputs
|
||||
const needsFullOutputs =
|
||||
typeof outputCount === 'number' &&
|
||||
outputCount > 1 &&
|
||||
outputsToDisplay.length < outputCount
|
||||
|
||||
if (needsFullOutputs) {
|
||||
try {
|
||||
const jobDetail = await getJobDetail(promptId)
|
||||
if (jobDetail?.outputs) {
|
||||
// Convert job outputs to ResultItemImpl array
|
||||
outputsToDisplay = Object.entries(jobDetail.outputs).flatMap(
|
||||
([nodeId, nodeOutputs]) =>
|
||||
Object.entries(nodeOutputs).flatMap(([mediaType, items]) =>
|
||||
(items as JobOutputItem[])
|
||||
.map(
|
||||
(item) =>
|
||||
new ResultItemImpl({
|
||||
...item,
|
||||
nodeId,
|
||||
mediaType
|
||||
})
|
||||
)
|
||||
.filter((r) => r.supportsPreview)
|
||||
)
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch job detail for folder view:', error)
|
||||
outputsToDisplay = []
|
||||
}
|
||||
}
|
||||
const outputsToDisplay = await resolveFolderOutputs(metadata)
|
||||
|
||||
if (outputsToDisplay.length === 0) {
|
||||
console.warn('No outputs available for folder view')
|
||||
return
|
||||
}
|
||||
|
||||
folderAssets.value = outputsToDisplay.map((output) => ({
|
||||
id: `${output.nodeId}-${output.filename}`,
|
||||
name: output.filename,
|
||||
size: 0,
|
||||
created_at: asset.created_at,
|
||||
tags: ['output'],
|
||||
preview_url: output.url,
|
||||
user_metadata: {
|
||||
promptId,
|
||||
nodeId: output.nodeId,
|
||||
subfolder: output.subfolder,
|
||||
executionTimeInSeconds,
|
||||
workflow: metadata.workflow
|
||||
}
|
||||
}))
|
||||
folderAssets.value = mapOutputsToAssetItems({
|
||||
promptId,
|
||||
outputs: outputsToDisplay,
|
||||
createdAt: asset.created_at,
|
||||
executionTimeInSeconds,
|
||||
workflow: metadata.workflow
|
||||
})
|
||||
}
|
||||
|
||||
const exitFolderView = () => {
|
||||
|
||||
@@ -84,10 +84,21 @@
|
||||
size="md"
|
||||
class="gap-1 font-bold"
|
||||
:aria-label="stackIndicatorLabel || undefined"
|
||||
@click.stop="emit('stack-toggle')"
|
||||
>
|
||||
<i aria-hidden="true" class="icon-[lucide--layers] size-4" />
|
||||
<span class="text-xs leading-none">{{ stackCount }}</span>
|
||||
<i aria-hidden="true" class="icon-[lucide--chevron-down] size-3" />
|
||||
<i
|
||||
aria-hidden="true"
|
||||
:class="
|
||||
cn(
|
||||
stackExpanded
|
||||
? 'icon-[lucide--chevron-down]'
|
||||
: 'icon-[lucide--chevron-right]',
|
||||
'size-3'
|
||||
)
|
||||
"
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -98,6 +109,10 @@ import { useProgressBarBackground } from '@/composables/useProgressBarBackground
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
const emit = defineEmits<{
|
||||
'stack-toggle': []
|
||||
}>()
|
||||
|
||||
const {
|
||||
previewUrl,
|
||||
previewAlt = '',
|
||||
@@ -109,6 +124,7 @@ const {
|
||||
secondaryText,
|
||||
stackCount,
|
||||
stackIndicatorLabel,
|
||||
stackExpanded = false,
|
||||
progressTotalPercent,
|
||||
progressCurrentPercent
|
||||
} = defineProps<{
|
||||
@@ -122,6 +138,7 @@ const {
|
||||
secondaryText?: string
|
||||
stackCount?: number
|
||||
stackIndicatorLabel?: string
|
||||
stackExpanded?: boolean
|
||||
progressTotalPercent?: number
|
||||
progressCurrentPercent?: number
|
||||
}>()
|
||||
|
||||
@@ -45,7 +45,7 @@ export function useMediaAssetActions() {
|
||||
): Promise<void> => {
|
||||
if (assetType === 'output') {
|
||||
const promptId =
|
||||
asset.id || getOutputAssetMetadata(asset.user_metadata)?.promptId
|
||||
getOutputAssetMetadata(asset.user_metadata)?.promptId || asset.id
|
||||
if (!promptId) {
|
||||
throw new Error('Unable to extract prompt ID from asset')
|
||||
}
|
||||
@@ -203,9 +203,10 @@ export function useMediaAssetActions() {
|
||||
const targetAsset = asset ?? mediaContext?.asset.value
|
||||
if (!targetAsset) return
|
||||
|
||||
// Try asset.id first (OSS), then fall back to metadata (Cloud)
|
||||
const metadata = getOutputAssetMetadata(targetAsset.user_metadata)
|
||||
const promptId = targetAsset.id || metadata?.promptId
|
||||
const promptId =
|
||||
metadata?.promptId ||
|
||||
(getAssetType(targetAsset) === 'output' ? targetAsset.id : undefined)
|
||||
|
||||
if (!promptId) {
|
||||
toast.add({
|
||||
|
||||
52
src/platform/assets/utils/outputAssetUtil.ts
Normal file
52
src/platform/assets/utils/outputAssetUtil.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import type { OutputAssetMetadata } from '@/platform/assets/schemas/assetMetadataSchema'
|
||||
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
|
||||
import type { ResultItemImpl } from '@/stores/queueStore'
|
||||
|
||||
type OutputAssetMapOptions = {
|
||||
promptId: string
|
||||
outputs: readonly ResultItemImpl[]
|
||||
createdAt?: string
|
||||
executionTimeInSeconds?: number
|
||||
workflow?: OutputAssetMetadata['workflow']
|
||||
excludeFilename?: string
|
||||
}
|
||||
|
||||
export function shouldLoadFullOutputs(
|
||||
outputCount: OutputAssetMetadata['outputCount'],
|
||||
outputsLength: number
|
||||
): boolean {
|
||||
return (
|
||||
typeof outputCount === 'number' &&
|
||||
outputCount > 1 &&
|
||||
outputsLength < outputCount
|
||||
)
|
||||
}
|
||||
|
||||
export function mapOutputsToAssetItems({
|
||||
promptId,
|
||||
outputs,
|
||||
createdAt,
|
||||
executionTimeInSeconds,
|
||||
workflow,
|
||||
excludeFilename
|
||||
}: OutputAssetMapOptions): AssetItem[] {
|
||||
const createdAtValue = createdAt ?? new Date().toISOString()
|
||||
|
||||
return outputs
|
||||
.filter((output) => output.filename && output.filename !== excludeFilename)
|
||||
.map((output) => ({
|
||||
id: `${promptId}-${output.nodeId}-${output.filename}`,
|
||||
name: output.filename,
|
||||
size: 0,
|
||||
created_at: createdAtValue,
|
||||
tags: ['output'],
|
||||
preview_url: output.url,
|
||||
user_metadata: {
|
||||
promptId,
|
||||
nodeId: output.nodeId,
|
||||
subfolder: output.subfolder,
|
||||
executionTimeInSeconds,
|
||||
workflow
|
||||
}
|
||||
}))
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import QuickLRU from '@alloc/quick-lru'
|
||||
import type { JobDetail } from '@/platform/remote/comfyui/jobs/jobTypes'
|
||||
import { extractWorkflow } from '@/platform/remote/comfyui/jobs/fetchJobs'
|
||||
import type { ComfyWorkflowJSON } from '@/platform/workflow/validation/schemas/workflowSchema'
|
||||
import type { ResultItem, TaskOutput } from '@/schemas/apiSchema'
|
||||
import { api } from '@/scripts/api'
|
||||
import { ResultItemImpl } from '@/stores/queueStore'
|
||||
import type { TaskItemImpl } from '@/stores/queueStore'
|
||||
@@ -75,6 +76,42 @@ export async function getOutputsForTask(
|
||||
}
|
||||
}
|
||||
|
||||
function mapTaskOutputToResultItems(outputs: TaskOutput): ResultItemImpl[] {
|
||||
return Object.entries(outputs).flatMap(([nodeId, nodeOutputs]) =>
|
||||
Object.entries(nodeOutputs)
|
||||
.filter(([mediaType, items]) => mediaType !== 'animated' && items)
|
||||
.flatMap(([mediaType, items]) => {
|
||||
if (!Array.isArray(items)) {
|
||||
return []
|
||||
}
|
||||
|
||||
return (items as ResultItem[])
|
||||
.filter((item) => typeof item === 'object' && item !== null)
|
||||
.map(
|
||||
(item) =>
|
||||
new ResultItemImpl({
|
||||
...item,
|
||||
nodeId,
|
||||
mediaType
|
||||
})
|
||||
)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
function getPreviewableOutputsFromTaskOutput(
|
||||
outputs?: TaskOutput
|
||||
): ResultItemImpl[] {
|
||||
if (!outputs) return []
|
||||
return ResultItemImpl.filterPreviewable(mapTaskOutputToResultItems(outputs))
|
||||
}
|
||||
|
||||
export function getPreviewableOutputsFromJobDetail(
|
||||
jobDetail?: JobDetail
|
||||
): ResultItemImpl[] {
|
||||
return getPreviewableOutputsFromTaskOutput(jobDetail?.outputs)
|
||||
}
|
||||
|
||||
// ===== Job Detail Caching =====
|
||||
|
||||
export async function getJobDetail(
|
||||
|
||||
Reference in New Issue
Block a user