mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-05 07:30:11 +00:00
feat: centralize output asset resolution
This commit is contained in:
@@ -228,9 +228,9 @@ import { useMediaAssetFiltering } from '@/platform/assets/composables/useMediaAs
|
||||
import { getOutputAssetMetadata } from '@/platform/assets/schemas/assetMetadataSchema'
|
||||
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
|
||||
import type { MediaKind } from '@/platform/assets/schemas/mediaAssetSchema'
|
||||
import { resolveOutputAssetItems } from '@/platform/assets/utils/outputAssetUtil'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { getJobDetail } from '@/services/jobOutputCache'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
@@ -238,12 +238,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()
|
||||
@@ -550,7 +544,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 +554,16 @@ const enterFolderView = async (asset: AssetItem) => {
|
||||
folderPromptId.value = promptId
|
||||
folderExecutionTime.value = executionTimeInSeconds
|
||||
|
||||
// Determine which outputs to display
|
||||
let outputsToDisplay = allOutputs ?? []
|
||||
const folderItems = await resolveOutputAssetItems(metadata, {
|
||||
createdAt: asset.created_at
|
||||
})
|
||||
|
||||
// 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 = []
|
||||
}
|
||||
}
|
||||
|
||||
if (outputsToDisplay.length === 0) {
|
||||
if (folderItems.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 = folderItems
|
||||
}
|
||||
|
||||
const exitFolderView = () => {
|
||||
|
||||
123
src/platform/assets/utils/outputAssetUtil.test.ts
Normal file
123
src/platform/assets/utils/outputAssetUtil.test.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import type { OutputAssetMetadata } from '@/platform/assets/schemas/assetMetadataSchema'
|
||||
import type { ResultItemImpl } from '@/stores/queueStore'
|
||||
|
||||
import { resolveOutputAssetItems } from './outputAssetUtil'
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
getJobDetail: vi.fn(),
|
||||
getPreviewableOutputsFromJobDetail: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('@/services/jobOutputCache', () => ({
|
||||
getJobDetail: mocks.getJobDetail,
|
||||
getPreviewableOutputsFromJobDetail: mocks.getPreviewableOutputsFromJobDetail
|
||||
}))
|
||||
|
||||
type OutputOverrides = Partial<{
|
||||
filename: string
|
||||
subfolder: string
|
||||
nodeId: string
|
||||
url: string
|
||||
}>
|
||||
|
||||
function createOutput(overrides: OutputOverrides = {}): ResultItemImpl {
|
||||
return {
|
||||
filename: 'file.png',
|
||||
subfolder: 'sub',
|
||||
nodeId: '1',
|
||||
url: 'https://example.com/file.png',
|
||||
...overrides
|
||||
} as ResultItemImpl
|
||||
}
|
||||
|
||||
describe('resolveOutputAssetItems', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('maps outputs and excludes a filename', async () => {
|
||||
const outputA = createOutput({
|
||||
filename: 'a.png',
|
||||
nodeId: '1',
|
||||
url: 'https://example.com/a.png'
|
||||
})
|
||||
const outputB = createOutput({
|
||||
filename: 'b.png',
|
||||
nodeId: '2',
|
||||
url: 'https://example.com/b.png'
|
||||
})
|
||||
const metadata: OutputAssetMetadata = {
|
||||
promptId: 'prompt-1',
|
||||
nodeId: '1',
|
||||
subfolder: 'sub',
|
||||
executionTimeInSeconds: 12.5,
|
||||
outputCount: 2,
|
||||
allOutputs: [outputA, outputB]
|
||||
}
|
||||
|
||||
const results = await resolveOutputAssetItems(metadata, {
|
||||
createdAt: '2025-01-01T00:00:00.000Z',
|
||||
excludeOutputKey: 'b.png'
|
||||
})
|
||||
|
||||
expect(mocks.getJobDetail).not.toHaveBeenCalled()
|
||||
expect(results).toHaveLength(1)
|
||||
expect(results[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
id: 'prompt-1-1-a.png',
|
||||
name: 'a.png',
|
||||
created_at: '2025-01-01T00:00:00.000Z',
|
||||
tags: ['output'],
|
||||
preview_url: 'https://example.com/a.png'
|
||||
})
|
||||
)
|
||||
expect(results[0].user_metadata).toEqual(
|
||||
expect.objectContaining({
|
||||
promptId: 'prompt-1',
|
||||
nodeId: '1',
|
||||
subfolder: 'sub',
|
||||
executionTimeInSeconds: 12.5
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it('loads full outputs when metadata indicates more outputs', async () => {
|
||||
const previewOutput = createOutput({
|
||||
filename: 'preview.png',
|
||||
nodeId: '1',
|
||||
url: 'https://example.com/preview.png'
|
||||
})
|
||||
const fullOutput = createOutput({
|
||||
filename: 'full.png',
|
||||
nodeId: '2',
|
||||
url: 'https://example.com/full.png'
|
||||
})
|
||||
const metadata: OutputAssetMetadata = {
|
||||
promptId: 'prompt-2',
|
||||
nodeId: '1',
|
||||
subfolder: 'sub',
|
||||
outputCount: 3,
|
||||
allOutputs: [previewOutput]
|
||||
}
|
||||
const jobDetail = { id: 'job-1' }
|
||||
|
||||
mocks.getJobDetail.mockResolvedValue(jobDetail)
|
||||
mocks.getPreviewableOutputsFromJobDetail.mockReturnValue([
|
||||
fullOutput,
|
||||
previewOutput
|
||||
])
|
||||
|
||||
const results = await resolveOutputAssetItems(metadata)
|
||||
|
||||
expect(mocks.getJobDetail).toHaveBeenCalledWith('prompt-2')
|
||||
expect(mocks.getPreviewableOutputsFromJobDetail).toHaveBeenCalledWith(
|
||||
jobDetail
|
||||
)
|
||||
expect(results.map((asset) => asset.name)).toEqual([
|
||||
'full.png',
|
||||
'preview.png'
|
||||
])
|
||||
})
|
||||
})
|
||||
84
src/platform/assets/utils/outputAssetUtil.ts
Normal file
84
src/platform/assets/utils/outputAssetUtil.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import type { OutputAssetMetadata } from '@/platform/assets/schemas/assetMetadataSchema'
|
||||
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
|
||||
import {
|
||||
getJobDetail,
|
||||
getPreviewableOutputsFromJobDetail
|
||||
} from '@/services/jobOutputCache'
|
||||
import type { ResultItemImpl } from '@/stores/queueStore'
|
||||
|
||||
type OutputAssetMapOptions = {
|
||||
promptId: string
|
||||
outputs: readonly ResultItemImpl[]
|
||||
createdAt?: string
|
||||
executionTimeInSeconds?: number
|
||||
workflow?: OutputAssetMetadata['workflow']
|
||||
excludeFilename?: string
|
||||
}
|
||||
|
||||
type ResolveOutputAssetItemsOptions = {
|
||||
createdAt?: string
|
||||
excludeOutputKey?: string
|
||||
}
|
||||
|
||||
function shouldLoadFullOutputs(
|
||||
outputCount: OutputAssetMetadata['outputCount'],
|
||||
outputsLength: number
|
||||
): boolean {
|
||||
return (
|
||||
typeof outputCount === 'number' &&
|
||||
outputCount > 1 &&
|
||||
outputsLength < outputCount
|
||||
)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
export async function resolveOutputAssetItems(
|
||||
metadata: OutputAssetMetadata,
|
||||
{ createdAt, excludeOutputKey }: ResolveOutputAssetItemsOptions = {}
|
||||
): Promise<AssetItem[]> {
|
||||
let outputsToDisplay = metadata.allOutputs ?? []
|
||||
if (shouldLoadFullOutputs(metadata.outputCount, outputsToDisplay.length)) {
|
||||
const jobDetail = await getJobDetail(metadata.promptId)
|
||||
const previewableOutputs = getPreviewableOutputsFromJobDetail(jobDetail)
|
||||
if (previewableOutputs.length) {
|
||||
outputsToDisplay = previewableOutputs
|
||||
}
|
||||
}
|
||||
|
||||
return mapOutputsToAssetItems({
|
||||
promptId: metadata.promptId,
|
||||
outputs: outputsToDisplay,
|
||||
createdAt,
|
||||
executionTimeInSeconds: metadata.executionTimeInSeconds,
|
||||
workflow: metadata.workflow,
|
||||
excludeFilename: excludeOutputKey
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user