mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-25 01:04:06 +00:00
feat: split job history into a dedicated sidebar tab (#8957)
## Summary Move queue job history into a dedicated sidebar tab (gated by `Comfy.Queue.QPOV2`) and remove mixed job-history UI from the Assets sidebar so assets and job controls are separated. ## Changes - **What**: - Added `JobHistorySidebarTab` with reusable job UI primitives: `JobFilterTabs`, `JobFilterActions`, `JobAssetsList`, and shared `JobHistoryActionsMenu`. - Added reactive `job-history` tab registration in `sidebarTabStore`; prepends above Assets when `Comfy.Queue.QPOV2` is enabled and unregisters cleanly when disabled. - Added debounced search to `useJobList` (filters by job title, metadata, and prompt id). - Extracted clear-history dialog logic to `useQueueClearHistoryDialog` and reused it from queue overlay and job history tab. - Removed active-job rendering and queue-clear controls from assets list/grid/tab views; assets sidebar now focuses on media assets only. - Removed the QPOV2 gate from `MediaAssetViewModeToggle` and updated queue/job localized copy. - Added and updated tests for queue overlay header actions, job filters, search filtering, sidebar tab registration, and assets sidebar behavior. ## Review Focus - Verify QPOV2 toggle behavior: - `Docked Job History` menu action toggles `Comfy.Queue.QPOV2`. - `job-history` tab insertion/removal order and active-tab reset on removal. - Verify behavior split between tabs: - Job controls (cancel/delete/view/filter/search/clear history/clear queue) live in Job History. - Assets sidebar loading/empty states and list/grid rendering remain correct after removing active jobs. ## Screenshots (if applicable) <img width="670" height="707" alt="image" src="https://github.com/user-attachments/assets/3a201fcb-d104-4e95-b5fe-49c4006a30a5" />
This commit is contained in:
@@ -421,6 +421,52 @@ describe('useJobList', () => {
|
||||
expect(instance.filteredTasks.value.map((t) => t.jobId)).toEqual(['wf-1'])
|
||||
})
|
||||
|
||||
it('filters jobs by search query', async () => {
|
||||
vi.useFakeTimers()
|
||||
queueStoreMock.historyTasks = [
|
||||
createTask({
|
||||
jobId: 'alpha',
|
||||
queueIndex: 2,
|
||||
mockState: 'completed',
|
||||
createTime: 2000,
|
||||
executionEndTimestamp: 2000
|
||||
}),
|
||||
createTask({
|
||||
jobId: 'beta',
|
||||
queueIndex: 1,
|
||||
mockState: 'failed',
|
||||
createTime: 1000,
|
||||
executionEndTimestamp: 1000
|
||||
})
|
||||
]
|
||||
|
||||
const instance = initComposable()
|
||||
await flush()
|
||||
expect(instance.filteredTasks.value.map((task) => task.jobId)).toEqual([
|
||||
'alpha',
|
||||
'beta'
|
||||
])
|
||||
|
||||
instance.searchQuery.value = 'beta'
|
||||
await vi.advanceTimersByTimeAsync(200)
|
||||
await flush()
|
||||
expect(instance.filteredTasks.value.map((task) => task.jobId)).toEqual([
|
||||
'beta'
|
||||
])
|
||||
|
||||
instance.searchQuery.value = 'failed meta'
|
||||
await vi.advanceTimersByTimeAsync(200)
|
||||
await flush()
|
||||
expect(instance.filteredTasks.value.map((task) => task.jobId)).toEqual([
|
||||
'beta'
|
||||
])
|
||||
|
||||
instance.searchQuery.value = 'does-not-exist'
|
||||
await vi.advanceTimersByTimeAsync(200)
|
||||
await flush()
|
||||
expect(instance.filteredTasks.value).toEqual([])
|
||||
})
|
||||
|
||||
it('hydrates job items with active progress and compute hours', async () => {
|
||||
queueStoreMock.runningTasks = [
|
||||
createTask({
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { refDebounced } from '@vueuse/core'
|
||||
import { orderBy } from 'es-toolkit/array'
|
||||
import { computed, onUnmounted, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
@@ -197,6 +198,8 @@ export function useJobList() {
|
||||
const selectedJobTab = ref<JobTab>('All')
|
||||
const selectedWorkflowFilter = ref<'all' | 'current'>('all')
|
||||
const selectedSortMode = ref<JobSortMode>('mostRecent')
|
||||
const searchQuery = ref('')
|
||||
const debouncedSearchQuery = refDebounced(searchQuery, 150)
|
||||
|
||||
const mostRecentTimestamp = (task: TaskItemImpl) => task.createTime ?? 0
|
||||
|
||||
@@ -248,8 +251,12 @@ export function useJobList() {
|
||||
return entries
|
||||
})
|
||||
|
||||
const normalizedSearchQuery = computed(() =>
|
||||
debouncedSearchQuery.value.trim().toLocaleLowerCase()
|
||||
)
|
||||
|
||||
const filteredTasks = computed<TaskItemImpl[]>(() =>
|
||||
filteredTaskEntries.value.map(({ task }) => task)
|
||||
searchableTaskEntries.value.map(({ task }) => task)
|
||||
)
|
||||
|
||||
const jobItems = computed<JobListItem[]>(() => {
|
||||
@@ -307,11 +314,31 @@ export function useJobList() {
|
||||
return m
|
||||
})
|
||||
|
||||
const searchableTaskEntries = computed<TaskWithState[]>(() => {
|
||||
if (!normalizedSearchQuery.value) return filteredTaskEntries.value
|
||||
|
||||
return filteredTaskEntries.value.filter(({ task }) => {
|
||||
const taskId = String(task.jobId ?? '').toLocaleLowerCase()
|
||||
const item = jobItemById.value.get(String(task.jobId))
|
||||
if (!item) {
|
||||
return taskId.includes(normalizedSearchQuery.value)
|
||||
}
|
||||
|
||||
const title = item.title.toLocaleLowerCase()
|
||||
const meta = item.meta.toLocaleLowerCase()
|
||||
return (
|
||||
title.includes(normalizedSearchQuery.value) ||
|
||||
meta.includes(normalizedSearchQuery.value) ||
|
||||
taskId.includes(normalizedSearchQuery.value)
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
const groupedJobItems = computed<JobGroup[]>(() => {
|
||||
const groups: JobGroup[] = []
|
||||
const index = new Map<string, number>()
|
||||
const localeValue = locale.value
|
||||
for (const { task, state } of filteredTaskEntries.value) {
|
||||
for (const { task, state } of searchableTaskEntries.value) {
|
||||
let ts: number | undefined
|
||||
if (state === 'completed' || state === 'failed') {
|
||||
ts = task.executionEndTimestamp
|
||||
@@ -356,6 +383,7 @@ export function useJobList() {
|
||||
selectedJobTab,
|
||||
selectedWorkflowFilter,
|
||||
selectedSortMode,
|
||||
searchQuery,
|
||||
hasFailedJobs,
|
||||
// data sources
|
||||
allTasksSorted,
|
||||
|
||||
32
src/composables/queue/useQueueClearHistoryDialog.ts
Normal file
32
src/composables/queue/useQueueClearHistoryDialog.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import QueueClearHistoryDialog from '@/components/queue/dialogs/QueueClearHistoryDialog.vue'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
|
||||
export const useQueueClearHistoryDialog = () => {
|
||||
const dialogStore = useDialogStore()
|
||||
|
||||
const showQueueClearHistoryDialog = () => {
|
||||
dialogStore.showDialog({
|
||||
key: 'queue-clear-history',
|
||||
component: QueueClearHistoryDialog,
|
||||
dialogComponentProps: {
|
||||
headless: true,
|
||||
closable: false,
|
||||
closeOnEscape: true,
|
||||
dismissableMask: true,
|
||||
pt: {
|
||||
root: {
|
||||
class: 'max-w-90 w-auto bg-transparent border-none shadow-none'
|
||||
},
|
||||
content: {
|
||||
class: 'bg-transparent',
|
||||
style: 'padding: 0'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
showQueueClearHistoryDialog
|
||||
}
|
||||
}
|
||||
16
src/composables/sidebarTabs/useJobHistorySidebarTab.ts
Normal file
16
src/composables/sidebarTabs/useJobHistorySidebarTab.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { markRaw } from 'vue'
|
||||
|
||||
import JobHistorySidebarTab from '@/components/sidebar/tabs/JobHistorySidebarTab.vue'
|
||||
import type { SidebarTabExtension } from '@/types/extensionTypes'
|
||||
|
||||
export const useJobHistorySidebarTab = (): SidebarTabExtension => {
|
||||
return {
|
||||
id: 'job-history',
|
||||
icon: 'icon-[lucide--history]',
|
||||
title: 'queue.jobHistory',
|
||||
tooltip: 'queue.jobHistory',
|
||||
label: 'queue.jobHistory',
|
||||
component: markRaw(JobHistorySidebarTab),
|
||||
type: 'vue'
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user