fix: display active jobs in oldest-first order in media assets panel (#8561)

## Summary

Active jobs (pending/running) in the media assets panel now display in
FIFO order - oldest jobs first (first to be processed at top). This
matches the queue processing order and the old queue panel behavior.

## Changes

- **AssetsSidebarListView.vue**: Add `.toReversed()` to `activeJobItems`
computed to reverse job order for display
- **AssetsSidebarGridView.vue**: Same change for grid view consistency
- **AssetsSidebarListView.test.ts**: Unit test verifying oldest-first
ordering

## Root Cause

PR #8225 changed sorting from `queueIndex` to `createTime` descending in
`useJobList.ts`, which placed newest jobs first. For active jobs, users
expect oldest first (FIFO - first to be processed appears at top).

## Solution

Rather than modifying the shared `useJobList` composable (which serves
both the assets panel and queue overlay), the fix applies
`.toReversed()` at the view layer for the active jobs section only.
This:
- Preserves the newest-first order for completed/history jobs
- Displays active jobs in oldest-first order
- Maintains backward compatibility with the queue overlay

## Testing

- Unit test added to verify FIFO ordering of active jobs
- All existing `useJobList` tests pass

Fixes COM-14151

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Corrected the display order of active jobs in the sidebar to show jobs
in oldest-first (FIFO) sequence.

* **Tests**
* Added unit tests for the assets sidebar list view to verify job
ordering and filtering behavior.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8561-fix-display-active-jobs-in-oldest-first-order-in-media-assets-panel-2fc6d73d365081c6bf31cb076a8d6014)
by [Unito](https://www.unito.io)
This commit is contained in:
Christian Byrne
2026-02-02 18:57:42 -08:00
committed by GitHub
parent 15f4f11ee2
commit 22755d2cb2
3 changed files with 152 additions and 2 deletions

View File

@@ -102,7 +102,7 @@ const isQueuePanelV2Enabled = computed(() =>
type AssetGridItem = { key: string; asset: AssetItem }
const activeJobItems = computed(() =>
jobItems.value.filter((item) => isActiveJobState(item.state))
jobItems.value.filter((item) => isActiveJobState(item.state)).toReversed()
)
const assetItems = computed<AssetGridItem[]>(() =>

View File

@@ -0,0 +1,150 @@
import { mount } from '@vue/test-utils'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { ref } from 'vue'
import AssetsSidebarListView from './AssetsSidebarListView.vue'
vi.mock('vue-i18n', () => ({
useI18n: () => ({
t: (key: string) => key
})
}))
vi.mock('@/composables/queue/useJobActions', () => ({
useJobActions: () => ({
cancelAction: { variant: 'ghost', label: 'Cancel', icon: 'pi pi-times' },
canCancelJob: ref(false),
runCancelJob: vi.fn()
})
}))
const mockJobItems = ref<
Array<{
id: string
title: string
meta: string
state: string
createTime?: number
}>
>([])
vi.mock('@/composables/queue/useJobList', () => ({
useJobList: () => ({
jobItems: mockJobItems
})
}))
vi.mock('@/stores/assetsStore', () => ({
useAssetsStore: () => ({
isAssetDeleting: () => false
})
}))
vi.mock('@/platform/settings/settingStore', () => ({
useSettingStore: () => ({
get: (key: string) => key === 'Comfy.Queue.QPOV2'
})
}))
vi.mock('@/utils/queueUtil', () => ({
isActiveJobState: (state: string) =>
state === 'pending' || state === 'running'
}))
vi.mock('@/utils/queueDisplay', () => ({
iconForJobState: () => 'pi pi-spinner'
}))
vi.mock('@/platform/assets/schemas/assetMetadataSchema', () => ({
getOutputAssetMetadata: () => undefined
}))
vi.mock('@/platform/assets/utils/mediaIconUtil', () => ({
iconForMediaType: () => 'pi pi-file'
}))
vi.mock('@/utils/formatUtil', () => ({
formatDuration: (d: number) => `${d}s`,
formatSize: (s: number) => `${s}B`,
getMediaTypeFromFilename: () => 'image',
truncateFilename: (name: string) => name
}))
describe('AssetsSidebarListView', () => {
beforeEach(() => {
vi.clearAllMocks()
mockJobItems.value = []
})
const defaultProps = {
assetItems: [],
selectableAssets: [],
isSelected: () => false,
isStackExpanded: () => false,
toggleStack: async () => {}
}
it('displays active jobs in oldest-first order (FIFO)', () => {
mockJobItems.value = [
{
id: 'newest',
title: 'Newest Job',
meta: '',
state: 'pending',
createTime: 3000
},
{
id: 'middle',
title: 'Middle Job',
meta: '',
state: 'running',
createTime: 2000
},
{
id: 'oldest',
title: 'Oldest Job',
meta: '',
state: 'pending',
createTime: 1000
}
]
const wrapper = mount(AssetsSidebarListView, {
props: defaultProps,
shallow: true
})
const jobListItems = wrapper.findAllComponents({ name: 'AssetsListItem' })
expect(jobListItems).toHaveLength(3)
const displayedTitles = jobListItems.map((item) =>
item.props('primaryText')
)
expect(displayedTitles).toEqual(['Oldest Job', 'Middle Job', 'Newest Job'])
})
it('excludes completed and failed jobs from active jobs section', () => {
mockJobItems.value = [
{ id: 'pending', title: 'Pending', meta: '', state: 'pending' },
{ id: 'completed', title: 'Completed', meta: '', state: 'completed' },
{ id: 'failed', title: 'Failed', meta: '', state: 'failed' },
{ id: 'running', title: 'Running', meta: '', state: 'running' }
]
const wrapper = mount(AssetsSidebarListView, {
props: defaultProps,
shallow: true
})
const jobListItems = wrapper.findAllComponents({ name: 'AssetsListItem' })
expect(jobListItems).toHaveLength(2)
const displayedTitles = jobListItems.map((item) =>
item.props('primaryText')
)
expect(displayedTitles).toContain('Running')
expect(displayedTitles).toContain('Pending')
expect(displayedTitles).not.toContain('Completed')
expect(displayedTitles).not.toContain('Failed')
})
})

View File

@@ -179,7 +179,7 @@ const isQueuePanelV2Enabled = computed(() =>
const hoveredJobId = ref<string | null>(null)
const hoveredAssetId = ref<string | null>(null)
const activeJobItems = computed(() =>
jobItems.value.filter((item) => isActiveJobState(item.state))
jobItems.value.filter((item) => isActiveJobState(item.state)).toReversed()
)
const hoveredJob = computed(() =>
hoveredJobId.value