mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-13 09:00:16 +00:00
feat: add job history and assets sidebar badge behavior (#9050)
## Summary Add sidebar badge behavior for queue/asset visibility updates: - Job History tab icon shows active jobs count (`queued + running`) only when the Job History panel is closed. - Assets tab icon no longer mirrors active jobs; when QPO V2 is enabled it now shows the number of assets added since the last time Assets was opened. - Opening Assets clears the unseen added-assets badge count. ## Changes - Added `iconBadge` logic to Job History sidebar tab. - Replaced Assets sidebar badge source with new unseen-assets counter logic. - Added `assetsSidebarBadgeStore` to track unseen asset additions from history updates and reset on Assets open. - Added/updated unit tests for both sidebar tab composables and the new store behavior. https://github.com/user-attachments/assets/33588a2a-c607-4fcc-8221-e7f11c3d79cc ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-9050-fix-add-job-history-and-assets-sidebar-badge-behavior-30e6d73d365081c38297fe6aac9cd34c) by [Unito](https://www.unito.io) --------- Co-authored-by: Johnpaul Chiwetelu <49923152+Myestery@users.noreply.github.com> Co-authored-by: GitHub Action <action@github.com>
This commit is contained in:
@@ -535,6 +535,7 @@ describe('useQueueStore', () => {
|
||||
|
||||
await store.update()
|
||||
const initialTask = store.historyTasks[0]
|
||||
const initialHistoryTasks = store.historyTasks
|
||||
|
||||
// Same job with same outputs_count
|
||||
mockGetHistory.mockResolvedValue([{ ...job }])
|
||||
@@ -543,6 +544,8 @@ describe('useQueueStore', () => {
|
||||
|
||||
// Should reuse the same instance
|
||||
expect(store.historyTasks[0]).toBe(initialTask)
|
||||
// Should preserve array identity when history is unchanged
|
||||
expect(store.historyTasks).toBe(initialHistoryTasks)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -479,6 +479,7 @@ export const useQueueStore = defineStore('queue', () => {
|
||||
const runningTasks = shallowRef<TaskItemImpl[]>([])
|
||||
const pendingTasks = shallowRef<TaskItemImpl[]>([])
|
||||
const historyTasks = shallowRef<TaskItemImpl[]>([])
|
||||
const hasFetchedHistorySnapshot = ref(false)
|
||||
const maxHistoryItems = ref(64)
|
||||
const isLoading = ref(false)
|
||||
|
||||
@@ -557,7 +558,7 @@ export const useQueueStore = defineStore('queue', () => {
|
||||
currentHistory.map((impl) => [impl.jobId, impl])
|
||||
)
|
||||
|
||||
historyTasks.value = sortedHistory.map((job) => {
|
||||
const nextHistoryTasks = sortedHistory.map((job) => {
|
||||
const existing = existingByJobId.get(job.id)
|
||||
if (!existing) return new TaskItemImpl(job)
|
||||
// Recreate if outputs_count changed to ensure lazy loading works
|
||||
@@ -566,6 +567,15 @@ export const useQueueStore = defineStore('queue', () => {
|
||||
}
|
||||
return existing
|
||||
})
|
||||
|
||||
const isHistoryUnchanged =
|
||||
nextHistoryTasks.length === currentHistory.length &&
|
||||
nextHistoryTasks.every((task, index) => task === currentHistory[index])
|
||||
|
||||
if (!isHistoryUnchanged) {
|
||||
historyTasks.value = nextHistoryTasks
|
||||
}
|
||||
hasFetchedHistorySnapshot.value = true
|
||||
} finally {
|
||||
// Only clear loading if this is the latest request.
|
||||
// A stale request completing (success or error) should not touch loading state
|
||||
@@ -595,6 +605,7 @@ export const useQueueStore = defineStore('queue', () => {
|
||||
runningTasks,
|
||||
pendingTasks,
|
||||
historyTasks,
|
||||
hasFetchedHistorySnapshot,
|
||||
maxHistoryItems,
|
||||
isLoading,
|
||||
|
||||
|
||||
187
src/stores/workspace/assetsSidebarBadgeStore.test.ts
Normal file
187
src/stores/workspace/assetsSidebarBadgeStore.test.ts
Normal file
@@ -0,0 +1,187 @@
|
||||
import { createTestingPinia } from '@pinia/testing'
|
||||
import { setActivePinia } from 'pinia'
|
||||
import { nextTick } from 'vue'
|
||||
import { beforeEach, describe, expect, it } from 'vitest'
|
||||
|
||||
import type { JobListItem } from '@/platform/remote/comfyui/jobs/jobTypes'
|
||||
import { TaskItemImpl, useQueueStore } from '@/stores/queueStore'
|
||||
import { useAssetsSidebarBadgeStore } from '@/stores/workspace/assetsSidebarBadgeStore'
|
||||
import { useSidebarTabStore } from '@/stores/workspace/sidebarTabStore'
|
||||
|
||||
const createHistoryTask = ({
|
||||
id,
|
||||
outputsCount,
|
||||
hasPreview = true
|
||||
}: {
|
||||
id: string
|
||||
outputsCount?: number
|
||||
hasPreview?: boolean
|
||||
}) =>
|
||||
new TaskItemImpl({
|
||||
id,
|
||||
status: 'completed',
|
||||
create_time: Date.now(),
|
||||
priority: 1,
|
||||
outputs_count: outputsCount,
|
||||
preview_output: hasPreview
|
||||
? {
|
||||
filename: `${id}.png`,
|
||||
subfolder: '',
|
||||
type: 'output',
|
||||
nodeId: '1',
|
||||
mediaType: 'images'
|
||||
}
|
||||
: undefined
|
||||
} as JobListItem)
|
||||
|
||||
describe('useAssetsSidebarBadgeStore', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createTestingPinia({ stubActions: false }))
|
||||
})
|
||||
|
||||
it('does not count initial fetched history when store starts before hydration', async () => {
|
||||
const queueStore = useQueueStore()
|
||||
const assetsSidebarBadgeStore = useAssetsSidebarBadgeStore()
|
||||
|
||||
queueStore.historyTasks = [
|
||||
createHistoryTask({ id: 'job-1', outputsCount: 2 })
|
||||
]
|
||||
await nextTick()
|
||||
expect(assetsSidebarBadgeStore.unseenAddedAssetsCount).toBe(0)
|
||||
|
||||
queueStore.hasFetchedHistorySnapshot = true
|
||||
await nextTick()
|
||||
expect(assetsSidebarBadgeStore.unseenAddedAssetsCount).toBe(0)
|
||||
})
|
||||
|
||||
it('counts new history items after baseline hydration while assets tab is closed', async () => {
|
||||
const queueStore = useQueueStore()
|
||||
queueStore.historyTasks = [
|
||||
createHistoryTask({ id: 'job-1', outputsCount: 2 })
|
||||
]
|
||||
queueStore.hasFetchedHistorySnapshot = true
|
||||
|
||||
const assetsSidebarBadgeStore = useAssetsSidebarBadgeStore()
|
||||
await nextTick()
|
||||
|
||||
expect(assetsSidebarBadgeStore.unseenAddedAssetsCount).toBe(0)
|
||||
|
||||
queueStore.historyTasks = [
|
||||
createHistoryTask({ id: 'job-2', hasPreview: true }),
|
||||
...queueStore.historyTasks
|
||||
]
|
||||
await nextTick()
|
||||
|
||||
expect(assetsSidebarBadgeStore.unseenAddedAssetsCount).toBe(1)
|
||||
})
|
||||
|
||||
it('does not count preview fallback when server outputsCount is zero', async () => {
|
||||
const queueStore = useQueueStore()
|
||||
queueStore.historyTasks = [
|
||||
createHistoryTask({ id: 'job-1', outputsCount: 2 })
|
||||
]
|
||||
queueStore.hasFetchedHistorySnapshot = true
|
||||
|
||||
const assetsSidebarBadgeStore = useAssetsSidebarBadgeStore()
|
||||
await nextTick()
|
||||
|
||||
expect(assetsSidebarBadgeStore.unseenAddedAssetsCount).toBe(0)
|
||||
|
||||
queueStore.historyTasks = [
|
||||
createHistoryTask({ id: 'job-2', outputsCount: 0, hasPreview: true }),
|
||||
...queueStore.historyTasks
|
||||
]
|
||||
await nextTick()
|
||||
|
||||
expect(assetsSidebarBadgeStore.unseenAddedAssetsCount).toBe(0)
|
||||
})
|
||||
|
||||
it('adds only delta when a seen job gains more outputs', async () => {
|
||||
const queueStore = useQueueStore()
|
||||
const assetsSidebarBadgeStore = useAssetsSidebarBadgeStore()
|
||||
|
||||
queueStore.historyTasks = [
|
||||
createHistoryTask({ id: 'job-1', hasPreview: true })
|
||||
]
|
||||
queueStore.hasFetchedHistorySnapshot = true
|
||||
await nextTick()
|
||||
|
||||
expect(assetsSidebarBadgeStore.unseenAddedAssetsCount).toBe(0)
|
||||
|
||||
queueStore.historyTasks = [
|
||||
createHistoryTask({ id: 'job-1', outputsCount: 3 })
|
||||
]
|
||||
await nextTick()
|
||||
|
||||
expect(assetsSidebarBadgeStore.unseenAddedAssetsCount).toBe(2)
|
||||
})
|
||||
|
||||
it('treats a reappearing job as unseen after it aged out of history', async () => {
|
||||
const queueStore = useQueueStore()
|
||||
const assetsSidebarBadgeStore = useAssetsSidebarBadgeStore()
|
||||
|
||||
queueStore.historyTasks = [
|
||||
createHistoryTask({ id: 'job-1', outputsCount: 2 })
|
||||
]
|
||||
queueStore.hasFetchedHistorySnapshot = true
|
||||
await nextTick()
|
||||
|
||||
expect(assetsSidebarBadgeStore.unseenAddedAssetsCount).toBe(0)
|
||||
|
||||
queueStore.historyTasks = [
|
||||
createHistoryTask({ id: 'job-2', outputsCount: 1 })
|
||||
]
|
||||
await nextTick()
|
||||
|
||||
expect(assetsSidebarBadgeStore.unseenAddedAssetsCount).toBe(1)
|
||||
|
||||
queueStore.historyTasks = [
|
||||
createHistoryTask({ id: 'job-1', outputsCount: 1 }),
|
||||
createHistoryTask({ id: 'job-2', outputsCount: 1 })
|
||||
]
|
||||
await nextTick()
|
||||
|
||||
expect(assetsSidebarBadgeStore.unseenAddedAssetsCount).toBe(2)
|
||||
})
|
||||
|
||||
it('clears and suppresses count while assets tab is open', async () => {
|
||||
const queueStore = useQueueStore()
|
||||
const sidebarTabStore = useSidebarTabStore()
|
||||
const assetsSidebarBadgeStore = useAssetsSidebarBadgeStore()
|
||||
|
||||
queueStore.historyTasks = [
|
||||
createHistoryTask({ id: 'job-1', outputsCount: 2 })
|
||||
]
|
||||
queueStore.hasFetchedHistorySnapshot = true
|
||||
await nextTick()
|
||||
expect(assetsSidebarBadgeStore.unseenAddedAssetsCount).toBe(0)
|
||||
|
||||
queueStore.historyTasks = [
|
||||
createHistoryTask({ id: 'job-2', outputsCount: 4 }),
|
||||
...queueStore.historyTasks
|
||||
]
|
||||
await nextTick()
|
||||
expect(assetsSidebarBadgeStore.unseenAddedAssetsCount).toBe(4)
|
||||
|
||||
sidebarTabStore.activeSidebarTabId = 'assets'
|
||||
await nextTick()
|
||||
expect(assetsSidebarBadgeStore.unseenAddedAssetsCount).toBe(0)
|
||||
|
||||
queueStore.historyTasks = [
|
||||
createHistoryTask({ id: 'job-3', outputsCount: 4 }),
|
||||
...queueStore.historyTasks
|
||||
]
|
||||
await nextTick()
|
||||
expect(assetsSidebarBadgeStore.unseenAddedAssetsCount).toBe(0)
|
||||
|
||||
sidebarTabStore.activeSidebarTabId = 'node-library'
|
||||
await nextTick()
|
||||
|
||||
queueStore.historyTasks = [
|
||||
createHistoryTask({ id: 'job-4', outputsCount: 1 }),
|
||||
...queueStore.historyTasks
|
||||
]
|
||||
await nextTick()
|
||||
expect(assetsSidebarBadgeStore.unseenAddedAssetsCount).toBe(1)
|
||||
})
|
||||
})
|
||||
99
src/stores/workspace/assetsSidebarBadgeStore.ts
Normal file
99
src/stores/workspace/assetsSidebarBadgeStore.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, watch } from 'vue'
|
||||
|
||||
import type { TaskItemImpl } from '@/stores/queueStore'
|
||||
import { useQueueStore } from '@/stores/queueStore'
|
||||
import { useSidebarTabStore } from '@/stores/workspace/sidebarTabStore'
|
||||
|
||||
const getAddedAssetCount = (task: TaskItemImpl): number => {
|
||||
if (typeof task.outputsCount === 'number') {
|
||||
return Math.max(task.outputsCount, 0)
|
||||
}
|
||||
|
||||
return task.previewOutput ? 1 : 0
|
||||
}
|
||||
|
||||
export const useAssetsSidebarBadgeStore = defineStore(
|
||||
'assetsSidebarBadge',
|
||||
() => {
|
||||
const queueStore = useQueueStore()
|
||||
const sidebarTabStore = useSidebarTabStore()
|
||||
|
||||
const unseenAddedAssetsCount = ref(0)
|
||||
const countedHistoryAssetsByJobId = ref(new Map<string, number>())
|
||||
const hasInitializedHistory = ref(false)
|
||||
|
||||
const markCurrentHistoryAsSeen = () => {
|
||||
countedHistoryAssetsByJobId.value = new Map(
|
||||
queueStore.historyTasks.map((task) => [
|
||||
task.jobId,
|
||||
getAddedAssetCount(task)
|
||||
])
|
||||
)
|
||||
}
|
||||
|
||||
watch(
|
||||
() =>
|
||||
[
|
||||
queueStore.historyTasks,
|
||||
queueStore.hasFetchedHistorySnapshot
|
||||
] as const,
|
||||
([historyTasks, hasFetchedHistorySnapshot]) => {
|
||||
if (!hasFetchedHistorySnapshot) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!hasInitializedHistory.value) {
|
||||
hasInitializedHistory.value = true
|
||||
markCurrentHistoryAsSeen()
|
||||
return
|
||||
}
|
||||
|
||||
const isAssetsTabOpen = sidebarTabStore.activeSidebarTabId === 'assets'
|
||||
const previousCountedAssetsByJobId = countedHistoryAssetsByJobId.value
|
||||
const nextCountedAssetsByJobId = new Map<string, number>()
|
||||
|
||||
for (const task of historyTasks) {
|
||||
const jobId = task.jobId
|
||||
if (!jobId) {
|
||||
continue
|
||||
}
|
||||
|
||||
const countedAssets = previousCountedAssetsByJobId.get(jobId) ?? 0
|
||||
const currentAssets = getAddedAssetCount(task)
|
||||
const hasSeenJob = previousCountedAssetsByJobId.has(jobId)
|
||||
|
||||
if (!isAssetsTabOpen && !hasSeenJob) {
|
||||
unseenAddedAssetsCount.value += currentAssets
|
||||
} else if (!isAssetsTabOpen && currentAssets > countedAssets) {
|
||||
unseenAddedAssetsCount.value += currentAssets - countedAssets
|
||||
}
|
||||
|
||||
nextCountedAssetsByJobId.set(
|
||||
jobId,
|
||||
Math.max(countedAssets, currentAssets)
|
||||
)
|
||||
}
|
||||
|
||||
countedHistoryAssetsByJobId.value = nextCountedAssetsByJobId
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
watch(
|
||||
() => sidebarTabStore.activeSidebarTabId,
|
||||
(activeSidebarTabId) => {
|
||||
if (activeSidebarTabId !== 'assets') {
|
||||
return
|
||||
}
|
||||
|
||||
unseenAddedAssetsCount.value = 0
|
||||
markCurrentHistoryAsSeen()
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
unseenAddedAssetsCount
|
||||
}
|
||||
}
|
||||
)
|
||||
Reference in New Issue
Block a user