mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-20 06:20:11 +00:00
fix: separate inspectable outputs from surfaced outputs
This commit is contained in:
@@ -257,6 +257,10 @@ const focusAssetInSidebar = async (item: JobListItem) => {
|
||||
|
||||
const inspectJobAsset = wrapWithErrorHandlingAsync(
|
||||
async (item: JobListItem) => {
|
||||
if (!item.taskRef?.inspectableOutput) {
|
||||
return
|
||||
}
|
||||
|
||||
await openResultGallery(item)
|
||||
await focusAssetInSidebar(item)
|
||||
}
|
||||
|
||||
@@ -74,11 +74,13 @@ type TestPreviewOutput = {
|
||||
url: string
|
||||
isImage: boolean
|
||||
isVideo: boolean
|
||||
is3D: boolean
|
||||
}
|
||||
|
||||
type TestTaskRef = {
|
||||
workflowId?: string
|
||||
previewOutput?: TestPreviewOutput
|
||||
inspectableOutput?: TestPreviewOutput
|
||||
}
|
||||
|
||||
type TestJobListItem = Omit<JobListItem, 'taskRef'> & {
|
||||
@@ -97,14 +99,24 @@ const createPreviewOutput = (
|
||||
return {
|
||||
url,
|
||||
isImage: mediaType === 'images',
|
||||
isVideo: mediaType === 'video'
|
||||
isVideo: mediaType === 'video',
|
||||
is3D: mediaType === 'model'
|
||||
}
|
||||
}
|
||||
|
||||
const createTaskRef = (preview?: TestPreviewOutput): TestTaskRef => ({
|
||||
workflowId: 'workflow-1',
|
||||
...(preview && { previewOutput: preview })
|
||||
})
|
||||
const createTaskRef = (
|
||||
preview?: TestPreviewOutput,
|
||||
inspectable?: TestPreviewOutput | null
|
||||
): TestTaskRef => {
|
||||
const resolvedInspectable =
|
||||
inspectable === undefined ? preview : (inspectable ?? undefined)
|
||||
|
||||
return {
|
||||
workflowId: 'workflow-1',
|
||||
...(preview && { previewOutput: preview }),
|
||||
...(resolvedInspectable && { inspectableOutput: resolvedInspectable })
|
||||
}
|
||||
}
|
||||
|
||||
const buildJob = (
|
||||
overrides: Partial<TestJobListItem> = {}
|
||||
@@ -285,12 +297,13 @@ describe('JobAssetsList', () => {
|
||||
})
|
||||
|
||||
it('emits viewItem on icon click for completed PLY jobs without preview tile', async () => {
|
||||
const previewOutput = createPreviewOutput('job-1.ply', 'model')
|
||||
const job = buildJob({
|
||||
iconImageUrl: undefined,
|
||||
taskRef: createTaskRef(createResultItem('job-1.ply', 'model'))
|
||||
taskRef: createTaskRef(previewOutput)
|
||||
})
|
||||
const onViewItem = vi.fn()
|
||||
const { container, user } = renderJobAssetsList([job], { onViewItem })
|
||||
const { container, user } = renderJobAssetsList({ jobs: [job], onViewItem })
|
||||
|
||||
const icon = container.querySelector('.assets-list-item-stub i')!
|
||||
await user.click(icon)
|
||||
@@ -298,6 +311,21 @@ describe('JobAssetsList', () => {
|
||||
expect(onViewItem).toHaveBeenCalledWith(job)
|
||||
})
|
||||
|
||||
it('does not emit viewItem on icon click for completed USDZ jobs without inspectable output', async () => {
|
||||
const previewOutput = createPreviewOutput('job-1.usdz', 'model')
|
||||
const job = buildJob({
|
||||
iconImageUrl: undefined,
|
||||
taskRef: createTaskRef(previewOutput, null)
|
||||
})
|
||||
const onViewItem = vi.fn()
|
||||
const { container, user } = renderJobAssetsList({ jobs: [job], onViewItem })
|
||||
|
||||
const icon = container.querySelector('.assets-list-item-stub i')!
|
||||
await user.click(icon)
|
||||
|
||||
expect(onViewItem).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('does not emit viewItem on double-click for non-completed jobs', async () => {
|
||||
const job = buildJob({
|
||||
state: 'running',
|
||||
@@ -312,21 +340,18 @@ describe('JobAssetsList', () => {
|
||||
expect(onViewItem).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('emits viewItem from the View button for completed jobs without preview output', async () => {
|
||||
it('does not show the View button for completed jobs without inspectable output', async () => {
|
||||
const job = buildJob({
|
||||
iconImageUrl: undefined,
|
||||
taskRef: createTaskRef()
|
||||
taskRef: createTaskRef(undefined, null)
|
||||
})
|
||||
const onViewItem = vi.fn()
|
||||
const { container } = renderJobAssetsList({ jobs: [job], onViewItem })
|
||||
const { container } = renderJobAssetsList({ jobs: [job] })
|
||||
|
||||
const jobRow = container.querySelector(`[data-job-id="${job.id}"]`)!
|
||||
await fireEvent.mouseEnter(jobRow)
|
||||
|
||||
await fireEvent.click(screen.getByText('menuLabels.View'))
|
||||
await nextTick()
|
||||
|
||||
expect(onViewItem).toHaveBeenCalledWith(job)
|
||||
expect(screen.queryByText('menuLabels.View')).toBeNull()
|
||||
})
|
||||
|
||||
it('shows and hides the job details popover with hover delays', async () => {
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
<i class="icon-[lucide--trash-2] size-4" />
|
||||
</Button>
|
||||
<Button
|
||||
v-else-if="row.job.state === 'completed'"
|
||||
v-else-if="hasInspectableCompletedAsset(row.job)"
|
||||
variant="textonly"
|
||||
size="sm"
|
||||
@click.stop="emitCompletedViewItem(row.job)"
|
||||
@@ -284,6 +284,10 @@ function getPreviewOutput(job: JobListItem) {
|
||||
return job.taskRef?.previewOutput
|
||||
}
|
||||
|
||||
function getInspectableOutput(job: JobListItem) {
|
||||
return job.taskRef?.inspectableOutput
|
||||
}
|
||||
|
||||
function getJobPreviewUrl(job: JobListItem) {
|
||||
const preview = getPreviewOutput(job)
|
||||
if (preview?.isImage || preview?.isVideo) {
|
||||
@@ -296,12 +300,12 @@ function isVideoPreviewJob(job: JobListItem) {
|
||||
return job.state === 'completed' && !!getPreviewOutput(job)?.isVideo
|
||||
}
|
||||
|
||||
function isPreviewableCompletedJob(job: JobListItem) {
|
||||
return job.state === 'completed' && !!getPreviewOutput(job)
|
||||
function hasInspectableCompletedAsset(job: JobListItem) {
|
||||
return job.state === 'completed' && !!getInspectableOutput(job)
|
||||
}
|
||||
|
||||
function emitViewItem(job: JobListItem) {
|
||||
if (isPreviewableCompletedJob(job)) {
|
||||
if (hasInspectableCompletedAsset(job)) {
|
||||
resetActiveDetails()
|
||||
emit('viewItem', job)
|
||||
}
|
||||
|
||||
@@ -160,15 +160,15 @@ const {
|
||||
} = useResultGallery(() => filteredTasks.value)
|
||||
|
||||
const onViewItem = wrapWithErrorHandlingAsync(async (item: JobListItem) => {
|
||||
const previewOutput = item.taskRef?.previewOutput
|
||||
const inspectableOutput = item.taskRef?.inspectableOutput
|
||||
|
||||
if (previewOutput?.is3D) {
|
||||
if (inspectableOutput?.is3D) {
|
||||
dialogStore.showDialog({
|
||||
key: 'asset-3d-viewer',
|
||||
title: item.title,
|
||||
component: Load3dViewerContent,
|
||||
props: {
|
||||
modelUrl: previewOutput.url || ''
|
||||
modelUrl: inspectableOutput.url || ''
|
||||
},
|
||||
dialogComponentProps: {
|
||||
style: 'width: 80vw; height: 80vh;',
|
||||
@@ -178,6 +178,10 @@ const onViewItem = wrapWithErrorHandlingAsync(async (item: JobListItem) => {
|
||||
return
|
||||
}
|
||||
|
||||
if (!inspectableOutput) {
|
||||
return
|
||||
}
|
||||
|
||||
await openResultGallery(item)
|
||||
})
|
||||
|
||||
|
||||
@@ -725,7 +725,7 @@ describe('useJobMenu', () => {
|
||||
setCurrentItem(
|
||||
createJobItem({
|
||||
state: 'completed',
|
||||
taskRef: { previewOutput: {} }
|
||||
taskRef: { previewOutput: {}, inspectableOutput: {} }
|
||||
})
|
||||
)
|
||||
|
||||
@@ -763,7 +763,7 @@ describe('useJobMenu', () => {
|
||||
setCurrentItem(
|
||||
createJobItem({
|
||||
state: 'completed',
|
||||
taskRef: { previewOutput: {} }
|
||||
taskRef: { previewOutput: {}, inspectableOutput: {} }
|
||||
})
|
||||
)
|
||||
|
||||
@@ -792,6 +792,35 @@ describe('useJobMenu', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('disables inspect when asset is surfaced but not inspectable', async () => {
|
||||
const { jobMenuEntries } = mountJobMenu(vi.fn())
|
||||
setCurrentItem(
|
||||
createJobItem({
|
||||
state: 'completed',
|
||||
taskRef: {
|
||||
previewOutput: {
|
||||
filename: 'mesh.usdz',
|
||||
subfolder: 'models',
|
||||
type: 'output',
|
||||
url: 'https://asset'
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
await nextTick()
|
||||
|
||||
expect(
|
||||
findActionEntry(jobMenuEntries.value, 'inspect-asset')?.disabled
|
||||
).toBe(true)
|
||||
expect(findActionEntry(jobMenuEntries.value, 'download')?.disabled).toBe(
|
||||
false
|
||||
)
|
||||
expect(jobMenuEntries.value.some((entry) => entry.key === 'delete')).toBe(
|
||||
true
|
||||
)
|
||||
})
|
||||
|
||||
it('returns failed menu entries with error actions', async () => {
|
||||
const { jobMenuEntries } = mountJobMenu()
|
||||
setCurrentItem(
|
||||
|
||||
@@ -242,13 +242,14 @@ export function useJobMenu(
|
||||
const state = item?.state
|
||||
if (!state) return []
|
||||
const hasPreviewAsset = !!item?.taskRef?.previewOutput
|
||||
const hasInspectableAsset = !!item?.taskRef?.inspectableOutput
|
||||
if (state === 'completed') {
|
||||
return [
|
||||
{
|
||||
key: 'inspect-asset',
|
||||
label: st('queue.jobMenu.inspectAsset', 'Inspect asset'),
|
||||
icon: 'icon-[lucide--zoom-in]',
|
||||
disabled: !hasPreviewAsset || !onInspectAsset,
|
||||
disabled: !hasInspectableAsset || !onInspectAsset,
|
||||
onClick: onInspectAsset
|
||||
? () => {
|
||||
const item = currentMenuItem()
|
||||
|
||||
@@ -8,7 +8,13 @@ import { ResultItemImpl, TaskItemImpl } from '@/stores/queueStore'
|
||||
|
||||
const createResultItem = (
|
||||
url: string,
|
||||
supportsPreview = true
|
||||
{
|
||||
supportsPreview = true,
|
||||
supportsInspection = supportsPreview
|
||||
}: {
|
||||
supportsPreview?: boolean
|
||||
supportsInspection?: boolean
|
||||
} = {}
|
||||
): ResultItemImpl => {
|
||||
const item = new ResultItemImpl({
|
||||
filename: url,
|
||||
@@ -20,6 +26,9 @@ const createResultItem = (
|
||||
// Override url getter for test matching
|
||||
Object.defineProperty(item, 'url', { get: () => url })
|
||||
Object.defineProperty(item, 'supportsPreview', { get: () => supportsPreview })
|
||||
Object.defineProperty(item, 'supportsInspection', {
|
||||
get: () => supportsInspection
|
||||
})
|
||||
return item
|
||||
}
|
||||
|
||||
@@ -63,12 +72,15 @@ describe('useResultGallery', () => {
|
||||
setActivePinia(createPinia())
|
||||
})
|
||||
|
||||
it('collects only previewable outputs and preserves their order', async () => {
|
||||
it('collects only inspectable outputs and preserves their order', async () => {
|
||||
const previewable = [createResultItem('p-1'), createResultItem('p-2')]
|
||||
const nonPreviewable = createResultItem('skip-me', false)
|
||||
const nonInspectable = createResultItem('skip-me', {
|
||||
supportsPreview: true,
|
||||
supportsInspection: false
|
||||
})
|
||||
const tasks = [
|
||||
createTask(previewable[0]),
|
||||
createTask(nonPreviewable),
|
||||
createTask(nonInspectable),
|
||||
createTask(previewable[1]),
|
||||
createTask()
|
||||
]
|
||||
@@ -83,7 +95,7 @@ describe('useResultGallery', () => {
|
||||
expect(galleryActiveIndex.value).toBe(0)
|
||||
})
|
||||
|
||||
it('does not change state when there are no previewable tasks', async () => {
|
||||
it('does not change state when there are no inspectable tasks', async () => {
|
||||
const { galleryItems, galleryActiveIndex, onViewItem } = useResultGallery(
|
||||
() => []
|
||||
)
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { ref, shallowRef } from 'vue'
|
||||
|
||||
import type { JobListItem } from '@/composables/queue/useJobList'
|
||||
import { findActiveIndex, getOutputsForTask } from '@/services/jobOutputCache'
|
||||
import {
|
||||
findActiveIndex,
|
||||
getInspectableOutputsForTask
|
||||
} from '@/services/jobOutputCache'
|
||||
import type { ResultItemImpl, TaskItemImpl } from '@/stores/queueStore'
|
||||
|
||||
/**
|
||||
@@ -17,7 +20,7 @@ export function useResultGallery(getFilteredTasks: () => TaskItemImpl[]) {
|
||||
|
||||
const targetTask = item.taskRef
|
||||
const targetOutputs = targetTask
|
||||
? await getOutputsForTask(targetTask)
|
||||
? await getInspectableOutputsForTask(targetTask)
|
||||
: null
|
||||
|
||||
// Request was superseded by a newer one
|
||||
@@ -27,7 +30,7 @@ export function useResultGallery(getFilteredTasks: () => TaskItemImpl[]) {
|
||||
const items = targetOutputs?.length
|
||||
? targetOutputs
|
||||
: tasks
|
||||
.map((t) => t.previewOutput)
|
||||
.map((t) => t.inspectableOutput)
|
||||
.filter((o): o is ResultItemImpl => !!o)
|
||||
|
||||
if (!items.length) return
|
||||
@@ -35,7 +38,7 @@ export function useResultGallery(getFilteredTasks: () => TaskItemImpl[]) {
|
||||
galleryItems.value = items
|
||||
galleryActiveIndex.value = findActiveIndex(
|
||||
items,
|
||||
item.taskRef?.previewOutput?.url
|
||||
item.taskRef?.inspectableOutput?.url
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
findActiveIndex,
|
||||
getJobDetail,
|
||||
getJobWorkflow,
|
||||
getInspectableOutputsForTask,
|
||||
getOutputsForTask
|
||||
} from '@/services/jobOutputCache'
|
||||
import { ResultItemImpl, TaskItemImpl } from '@/stores/queueStore'
|
||||
@@ -29,7 +30,16 @@ vi.mock('@/scripts/api', () => ({
|
||||
}
|
||||
}))
|
||||
|
||||
function createResultItem(url: string, supportsPreview = true): ResultItemImpl {
|
||||
function createResultItem(
|
||||
url: string,
|
||||
{
|
||||
supportsPreview = true,
|
||||
supportsInspection = supportsPreview
|
||||
}: {
|
||||
supportsPreview?: boolean
|
||||
supportsInspection?: boolean
|
||||
} = {}
|
||||
): ResultItemImpl {
|
||||
const item = new ResultItemImpl({
|
||||
filename: url,
|
||||
subfolder: '',
|
||||
@@ -39,6 +49,9 @@ function createResultItem(url: string, supportsPreview = true): ResultItemImpl {
|
||||
})
|
||||
Object.defineProperty(item, 'url', { get: () => url })
|
||||
Object.defineProperty(item, 'supportsPreview', { get: () => supportsPreview })
|
||||
Object.defineProperty(item, 'supportsInspection', {
|
||||
get: () => supportsInspection
|
||||
})
|
||||
return item
|
||||
}
|
||||
|
||||
@@ -101,16 +114,30 @@ describe('jobOutputCache', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('getOutputsForTask', () => {
|
||||
it('returns previewable outputs directly when no lazy load needed', async () => {
|
||||
describe('getInspectableOutputsForTask', () => {
|
||||
it('returns inspectable outputs directly when no lazy load needed', async () => {
|
||||
const outputs = [createResultItem('p-1'), createResultItem('p-2')]
|
||||
const task = createTask(undefined, outputs, 1)
|
||||
|
||||
const result = await getOutputsForTask(task)
|
||||
const result = await getInspectableOutputsForTask(task)
|
||||
|
||||
expect(result).toEqual(outputs)
|
||||
})
|
||||
|
||||
it('filters out surfaced outputs that are not inspectable', async () => {
|
||||
const outputs = [
|
||||
createResultItem('mesh.usdz', {
|
||||
supportsPreview: true,
|
||||
supportsInspection: false
|
||||
})
|
||||
]
|
||||
const task = createTask(undefined, outputs, 1)
|
||||
|
||||
const result = await getInspectableOutputsForTask(task)
|
||||
|
||||
expect(result).toEqual([])
|
||||
})
|
||||
|
||||
it('lazy loads when outputsCount > 1', async () => {
|
||||
const previewOutput = createResultItem('preview')
|
||||
const fullOutputs = [
|
||||
@@ -123,7 +150,7 @@ describe('jobOutputCache', () => {
|
||||
const loadedTask = new TaskItemImpl(job, {}, fullOutputs)
|
||||
task.loadFullOutputs = vi.fn().mockResolvedValue(loadedTask)
|
||||
|
||||
const result = await getOutputsForTask(task)
|
||||
const result = await getInspectableOutputsForTask(task)
|
||||
|
||||
expect(result).toEqual(fullOutputs)
|
||||
expect(task.loadFullOutputs).toHaveBeenCalled()
|
||||
@@ -138,11 +165,11 @@ describe('jobOutputCache', () => {
|
||||
task.loadFullOutputs = vi.fn().mockResolvedValue(loadedTask)
|
||||
|
||||
// First call should load
|
||||
await getOutputsForTask(task)
|
||||
await getInspectableOutputsForTask(task)
|
||||
expect(task.loadFullOutputs).toHaveBeenCalledTimes(1)
|
||||
|
||||
// Second call should use cache
|
||||
await getOutputsForTask(task)
|
||||
await getInspectableOutputsForTask(task)
|
||||
expect(task.loadFullOutputs).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
@@ -155,7 +182,7 @@ describe('jobOutputCache', () => {
|
||||
.fn()
|
||||
.mockRejectedValue(new Error('Network error'))
|
||||
|
||||
const result = await getOutputsForTask(task)
|
||||
const result = await getInspectableOutputsForTask(task)
|
||||
|
||||
expect(result).toEqual([previewOutput])
|
||||
})
|
||||
@@ -184,8 +211,8 @@ describe('jobOutputCache', () => {
|
||||
task2.loadFullOutputs = vi.fn().mockResolvedValue(loadedTask2)
|
||||
|
||||
// Start task1, then immediately start task2
|
||||
const promise1 = getOutputsForTask(task1)
|
||||
const promise2 = getOutputsForTask(task2)
|
||||
const promise1 = getInspectableOutputsForTask(task1)
|
||||
const promise2 = getInspectableOutputsForTask(task2)
|
||||
|
||||
const [result1, result2] = await Promise.all([promise1, promise2])
|
||||
|
||||
@@ -195,6 +222,17 @@ describe('jobOutputCache', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('getOutputsForTask', () => {
|
||||
it('aliases getInspectableOutputsForTask', async () => {
|
||||
const outputs = [createResultItem('p-1')]
|
||||
const task = createTask(undefined, outputs, 1)
|
||||
|
||||
const result = await getOutputsForTask(task)
|
||||
|
||||
expect(result).toEqual(outputs)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getPreviewableOutputsFromJobDetail', () => {
|
||||
it('returns empty array when job detail or outputs are missing', async () => {
|
||||
const { getPreviewableOutputsFromJobDetail } =
|
||||
|
||||
@@ -40,10 +40,10 @@ export function findActiveIndex(
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets previewable outputs for a task, with lazy loading, caching, and request deduping.
|
||||
* Gets inspectable outputs for a task, with lazy loading, caching, and request deduping.
|
||||
* Returns null if a newer request superseded this one while loading.
|
||||
*/
|
||||
export async function getOutputsForTask(
|
||||
export async function getInspectableOutputsForTask(
|
||||
task: TaskItemImpl
|
||||
): Promise<ResultItemImpl[] | null> {
|
||||
const requestId = String(task.jobId)
|
||||
@@ -53,12 +53,12 @@ export async function getOutputsForTask(
|
||||
const needsLazyLoad = outputsCount > 1
|
||||
|
||||
if (!needsLazyLoad) {
|
||||
return [...task.previewableOutputs]
|
||||
return [...task.inspectableOutputs]
|
||||
}
|
||||
|
||||
const cached = taskCache.get(requestId)
|
||||
if (cached) {
|
||||
return [...cached.previewableOutputs]
|
||||
return [...cached.inspectableOutputs]
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -70,13 +70,22 @@ export async function getOutputsForTask(
|
||||
}
|
||||
|
||||
taskCache.set(requestId, loadedTask)
|
||||
return [...loadedTask.previewableOutputs]
|
||||
return [...loadedTask.inspectableOutputs]
|
||||
} catch (error) {
|
||||
console.warn('Failed to load full outputs, using preview:', error)
|
||||
return [...task.previewableOutputs]
|
||||
console.warn(
|
||||
'Failed to load full outputs, using inspectable outputs:',
|
||||
error
|
||||
)
|
||||
return [...task.inspectableOutputs]
|
||||
}
|
||||
}
|
||||
|
||||
export async function getOutputsForTask(
|
||||
task: TaskItemImpl
|
||||
): Promise<ResultItemImpl[] | null> {
|
||||
return getInspectableOutputsForTask(task)
|
||||
}
|
||||
|
||||
function getPreviewableOutputs(outputs?: TaskOutput): ResultItemImpl[] {
|
||||
if (!outputs) return []
|
||||
return ResultItemImpl.filterPreviewable(parseTaskOutput(outputs))
|
||||
|
||||
@@ -223,12 +223,15 @@ describe('TaskItemImpl', () => {
|
||||
const output = task.previewOutput
|
||||
|
||||
expect(task.previewableOutputs).toHaveLength(1)
|
||||
expect(task.inspectableOutputs).toHaveLength(1)
|
||||
expect(output?.filename).toBe('mesh-output.ply')
|
||||
expect(task.inspectableOutput?.filename).toBe('mesh-output.ply')
|
||||
expect(output?.is3D).toBe(true)
|
||||
expect(output?.supportsPreview).toBe(true)
|
||||
expect(output?.supportsInspection).toBe(true)
|
||||
})
|
||||
|
||||
it('should not treat USDZ output as previewable 3D media', () => {
|
||||
it('should surface USDZ output without treating it as inspectable 3D media', () => {
|
||||
const job: JobListItem = {
|
||||
...createHistoryJob(0, 'usdz-job'),
|
||||
preview_output: {
|
||||
@@ -242,8 +245,13 @@ describe('TaskItemImpl', () => {
|
||||
|
||||
const task = new TaskItemImpl(job)
|
||||
|
||||
expect(task.previewableOutputs).toHaveLength(0)
|
||||
expect(task.previewOutput).toBeUndefined()
|
||||
expect(task.previewableOutputs).toHaveLength(1)
|
||||
expect(task.previewOutput?.filename).toBe('mesh-output.usdz')
|
||||
expect(task.previewOutput?.is3D).toBe(true)
|
||||
expect(task.previewOutput?.supportsPreview).toBe(true)
|
||||
expect(task.previewOutput?.supportsInspection).toBe(false)
|
||||
expect(task.inspectableOutputs).toHaveLength(0)
|
||||
expect(task.inspectableOutput).toBeUndefined()
|
||||
})
|
||||
|
||||
describe('error extraction getters', () => {
|
||||
|
||||
@@ -223,15 +223,22 @@ export class ResultItemImpl {
|
||||
}
|
||||
|
||||
get is3D(): boolean {
|
||||
return (
|
||||
getMediaTypeFromFilename(this.filename) === '3D' &&
|
||||
isPreviewableMediaFilename(this.filename)
|
||||
)
|
||||
return getMediaTypeFromFilename(this.filename) === '3D'
|
||||
}
|
||||
|
||||
get isText(): boolean {
|
||||
return this.mediaType === 'text'
|
||||
}
|
||||
|
||||
get supportsInspection(): boolean {
|
||||
return (
|
||||
this.isImage ||
|
||||
this.isVideo ||
|
||||
this.isAudio ||
|
||||
(this.is3D && isPreviewableMediaFilename(this.filename))
|
||||
)
|
||||
}
|
||||
|
||||
get supportsPreview(): boolean {
|
||||
return (
|
||||
this.isImage || this.isVideo || this.isAudio || this.is3D || this.isText
|
||||
@@ -244,6 +251,12 @@ export class ResultItemImpl {
|
||||
return outputs.filter((o) => o.supportsPreview)
|
||||
}
|
||||
|
||||
static filterInspectable(
|
||||
outputs: readonly ResultItemImpl[]
|
||||
): ResultItemImpl[] {
|
||||
return outputs.filter((o) => o.supportsInspection)
|
||||
}
|
||||
|
||||
static findByUrl(items: readonly ResultItemImpl[], url?: string): number {
|
||||
if (!url) return 0
|
||||
const idx = items.findIndex((o) => o.url === url)
|
||||
@@ -284,11 +297,16 @@ export class TaskItemImpl {
|
||||
return parseTaskOutput(this.outputs)
|
||||
}
|
||||
|
||||
/** All outputs that support preview (images, videos, audio, 3D, text) */
|
||||
/** All surfaced outputs (images, videos, audio, 3D, text) */
|
||||
get previewableOutputs(): readonly ResultItemImpl[] {
|
||||
return ResultItemImpl.filterPreviewable(this.flatOutputs)
|
||||
}
|
||||
|
||||
/** Outputs the UI can inspect directly in the viewer/lightbox. */
|
||||
get inspectableOutputs(): readonly ResultItemImpl[] {
|
||||
return ResultItemImpl.filterInspectable(this.flatOutputs)
|
||||
}
|
||||
|
||||
get previewOutput(): ResultItemImpl | undefined {
|
||||
const previewable = this.previewableOutputs
|
||||
// Prefer the last saved media file (most recent result) over temp previews
|
||||
@@ -298,6 +316,14 @@ export class TaskItemImpl {
|
||||
)
|
||||
}
|
||||
|
||||
get inspectableOutput(): ResultItemImpl | undefined {
|
||||
const inspectable = this.inspectableOutputs
|
||||
return (
|
||||
inspectable.findLast((output) => output.type === 'output') ??
|
||||
inspectable.at(-1)
|
||||
)
|
||||
}
|
||||
|
||||
// Derive taskType from job status
|
||||
get taskType(): TaskType {
|
||||
switch (this.job.status) {
|
||||
|
||||
Reference in New Issue
Block a user