From 22755d2cb289e66ab224fd411261f6bf40f3ae4f Mon Sep 17 00:00:00 2001 From: Christian Byrne Date: Mon, 2 Feb 2026 18:57:42 -0800 Subject: [PATCH] fix: display active jobs in oldest-first order in media assets panel (#8561) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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 ## 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. ┆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) --- .../sidebar/tabs/AssetsSidebarGridView.vue | 2 +- .../tabs/AssetsSidebarListView.test.ts | 150 ++++++++++++++++++ .../sidebar/tabs/AssetsSidebarListView.vue | 2 +- 3 files changed, 152 insertions(+), 2 deletions(-) create mode 100644 src/components/sidebar/tabs/AssetsSidebarListView.test.ts diff --git a/src/components/sidebar/tabs/AssetsSidebarGridView.vue b/src/components/sidebar/tabs/AssetsSidebarGridView.vue index 030f547c8..6e95359b0 100644 --- a/src/components/sidebar/tabs/AssetsSidebarGridView.vue +++ b/src/components/sidebar/tabs/AssetsSidebarGridView.vue @@ -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(() => diff --git a/src/components/sidebar/tabs/AssetsSidebarListView.test.ts b/src/components/sidebar/tabs/AssetsSidebarListView.test.ts new file mode 100644 index 000000000..57746e5fc --- /dev/null +++ b/src/components/sidebar/tabs/AssetsSidebarListView.test.ts @@ -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') + }) +}) diff --git a/src/components/sidebar/tabs/AssetsSidebarListView.vue b/src/components/sidebar/tabs/AssetsSidebarListView.vue index 209a744f2..0c0fb4944 100644 --- a/src/components/sidebar/tabs/AssetsSidebarListView.vue +++ b/src/components/sidebar/tabs/AssetsSidebarListView.vue @@ -179,7 +179,7 @@ const isQueuePanelV2Enabled = computed(() => const hoveredJobId = ref(null) const hoveredAssetId = ref(null) const activeJobItems = computed(() => - jobItems.value.filter((item) => isActiveJobState(item.state)) + jobItems.value.filter((item) => isActiveJobState(item.state)).toReversed() ) const hoveredJob = computed(() => hoveredJobId.value