mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-13 17:10: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:
151
src/stores/workspace/sidebarTabStore.test.ts
Normal file
151
src/stores/workspace/sidebarTabStore.test.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
import { createTestingPinia } from '@pinia/testing'
|
||||
import { setActivePinia } from 'pinia'
|
||||
import { nextTick, ref } from 'vue'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { useSidebarTabStore } from '@/stores/workspace/sidebarTabStore'
|
||||
|
||||
const { mockGetSetting, mockRegisterCommand, mockRegisterCommands } =
|
||||
vi.hoisted(() => ({
|
||||
mockGetSetting: vi.fn(),
|
||||
mockRegisterCommand: vi.fn(),
|
||||
mockRegisterCommands: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('@/platform/settings/settingStore', () => ({
|
||||
useSettingStore: () => ({
|
||||
get: mockGetSetting
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/commandStore', () => ({
|
||||
useCommandStore: () => ({
|
||||
registerCommand: mockRegisterCommand,
|
||||
commands: []
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/menuItemStore', () => ({
|
||||
useMenuItemStore: () => ({
|
||||
registerCommands: mockRegisterCommands
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/i18n', () => ({
|
||||
t: (key: string) => key,
|
||||
te: () => false
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/sidebarTabs/useAssetsSidebarTab', () => ({
|
||||
useAssetsSidebarTab: () => ({
|
||||
id: 'assets',
|
||||
title: 'assets',
|
||||
type: 'vue',
|
||||
component: {}
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/sidebarTabs/useJobHistorySidebarTab', () => ({
|
||||
useJobHistorySidebarTab: () => ({
|
||||
id: 'job-history',
|
||||
title: 'job-history',
|
||||
type: 'vue',
|
||||
component: {}
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/sidebarTabs/useNodeLibrarySidebarTab', () => ({
|
||||
useNodeLibrarySidebarTab: () => ({
|
||||
id: 'node-library',
|
||||
title: 'node-library',
|
||||
type: 'vue',
|
||||
component: {}
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/sidebarTabs/useModelLibrarySidebarTab', () => ({
|
||||
useModelLibrarySidebarTab: () => ({
|
||||
id: 'model-library',
|
||||
title: 'model-library',
|
||||
type: 'vue',
|
||||
component: {}
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock(
|
||||
'@/platform/workflow/management/composables/useWorkflowsSidebarTab',
|
||||
() => ({
|
||||
useWorkflowsSidebarTab: () => ({
|
||||
id: 'workflows',
|
||||
title: 'workflows',
|
||||
type: 'vue',
|
||||
component: {}
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
describe('useSidebarTabStore', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createTestingPinia({ stubActions: false }))
|
||||
mockGetSetting.mockReset()
|
||||
mockRegisterCommand.mockClear()
|
||||
mockRegisterCommands.mockClear()
|
||||
})
|
||||
|
||||
it('registers the job history tab when QPO V2 is enabled', () => {
|
||||
mockGetSetting.mockImplementation((key: string) =>
|
||||
key === 'Comfy.Queue.QPOV2' ? true : undefined
|
||||
)
|
||||
|
||||
const store = useSidebarTabStore()
|
||||
store.registerCoreSidebarTabs()
|
||||
|
||||
expect(store.sidebarTabs.map((tab) => tab.id)).toEqual([
|
||||
'job-history',
|
||||
'assets',
|
||||
'node-library',
|
||||
'model-library',
|
||||
'workflows'
|
||||
])
|
||||
expect(mockRegisterCommand).toHaveBeenCalledTimes(5)
|
||||
})
|
||||
|
||||
it('does not register the job history tab when QPO V2 is disabled', () => {
|
||||
mockGetSetting.mockImplementation((key: string) =>
|
||||
key === 'Comfy.Queue.QPOV2' ? false : undefined
|
||||
)
|
||||
|
||||
const store = useSidebarTabStore()
|
||||
store.registerCoreSidebarTabs()
|
||||
|
||||
expect(store.sidebarTabs.map((tab) => tab.id)).toEqual([
|
||||
'assets',
|
||||
'node-library',
|
||||
'model-library',
|
||||
'workflows'
|
||||
])
|
||||
expect(mockRegisterCommand).toHaveBeenCalledTimes(4)
|
||||
})
|
||||
|
||||
it('prepends the job history tab when QPO V2 is toggled on', async () => {
|
||||
const qpoV2Enabled = ref(false)
|
||||
mockGetSetting.mockImplementation((key: string) =>
|
||||
key === 'Comfy.Queue.QPOV2' ? qpoV2Enabled.value : undefined
|
||||
)
|
||||
|
||||
const store = useSidebarTabStore()
|
||||
store.registerCoreSidebarTabs()
|
||||
|
||||
qpoV2Enabled.value = true
|
||||
await nextTick()
|
||||
|
||||
expect(store.sidebarTabs.map((tab) => tab.id)).toEqual([
|
||||
'job-history',
|
||||
'assets',
|
||||
'node-library',
|
||||
'model-library',
|
||||
'workflows'
|
||||
])
|
||||
expect(mockRegisterCommand).toHaveBeenCalledTimes(5)
|
||||
})
|
||||
})
|
||||
@@ -1,7 +1,8 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, ref } from 'vue'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
|
||||
import { useAssetsSidebarTab } from '@/composables/sidebarTabs/useAssetsSidebarTab'
|
||||
import { useJobHistorySidebarTab } from '@/composables/sidebarTabs/useJobHistorySidebarTab'
|
||||
import { useModelLibrarySidebarTab } from '@/composables/sidebarTabs/useModelLibrarySidebarTab'
|
||||
import { useNodeLibrarySidebarTab } from '@/composables/sidebarTabs/useNodeLibrarySidebarTab'
|
||||
import { t, te } from '@/i18n'
|
||||
@@ -26,8 +27,13 @@ export const useSidebarTabStore = defineStore('sidebarTab', () => {
|
||||
activeSidebarTabId.value = activeSidebarTabId.value === tabId ? null : tabId
|
||||
}
|
||||
|
||||
const registerSidebarTab = (tab: SidebarTabExtension) => {
|
||||
sidebarTabs.value = [...sidebarTabs.value, tab]
|
||||
const registerSidebarTab = (
|
||||
tab: SidebarTabExtension,
|
||||
options?: { prepend?: boolean }
|
||||
) => {
|
||||
sidebarTabs.value = options?.prepend
|
||||
? [tab, ...sidebarTabs.value]
|
||||
: [...sidebarTabs.value, tab]
|
||||
|
||||
// Generate label in format "Toggle X Sidebar"
|
||||
const labelFunction = () => {
|
||||
@@ -45,7 +51,8 @@ export const useSidebarTabStore = defineStore('sidebarTab', () => {
|
||||
'node-library': 'sideToolbar.nodeLibrary',
|
||||
'model-library': 'sideToolbar.modelLibrary',
|
||||
workflows: 'sideToolbar.workflows',
|
||||
assets: 'sideToolbar.assets'
|
||||
assets: 'sideToolbar.assets',
|
||||
'job-history': 'queue.jobHistory'
|
||||
}
|
||||
|
||||
const key = menubarLabelKeys[tab.id]
|
||||
@@ -95,6 +102,9 @@ export const useSidebarTabStore = defineStore('sidebarTab', () => {
|
||||
const newSidebarTabs = [...sidebarTabs.value]
|
||||
newSidebarTabs.splice(index, 1)
|
||||
sidebarTabs.value = newSidebarTabs
|
||||
if (activeSidebarTabId.value === id) {
|
||||
activeSidebarTabId.value = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,6 +112,25 @@ export const useSidebarTabStore = defineStore('sidebarTab', () => {
|
||||
* Register the core sidebar tabs.
|
||||
*/
|
||||
const registerCoreSidebarTabs = () => {
|
||||
const settingStore = useSettingStore()
|
||||
const jobHistoryTabId = 'job-history'
|
||||
const syncJobHistoryTab = (enabled: boolean) => {
|
||||
const hasJobHistoryTab = sidebarTabs.value.some(
|
||||
(tab) => tab.id === jobHistoryTabId
|
||||
)
|
||||
if (enabled && !hasJobHistoryTab) {
|
||||
registerSidebarTab(useJobHistorySidebarTab(), { prepend: true })
|
||||
} else if (!enabled && hasJobHistoryTab) {
|
||||
unregisterSidebarTab(jobHistoryTabId)
|
||||
}
|
||||
}
|
||||
|
||||
syncJobHistoryTab(settingStore.get('Comfy.Queue.QPOV2'))
|
||||
watch(
|
||||
() => settingStore.get('Comfy.Queue.QPOV2'),
|
||||
(enabled) => syncJobHistoryTab(enabled)
|
||||
)
|
||||
|
||||
registerSidebarTab(useAssetsSidebarTab())
|
||||
registerSidebarTab(useNodeLibrarySidebarTab())
|
||||
registerSidebarTab(useModelLibrarySidebarTab())
|
||||
|
||||
Reference in New Issue
Block a user