diff --git a/src/components/queue/JobHistoryActionsMenu.vue b/src/components/queue/JobHistoryActionsMenu.vue new file mode 100644 index 000000000..1d9d8168a --- /dev/null +++ b/src/components/queue/JobHistoryActionsMenu.vue @@ -0,0 +1,104 @@ + + + diff --git a/src/components/queue/QueueOverlayExpanded.vue b/src/components/queue/QueueOverlayExpanded.vue index 8e1d5d3e0..f7a5be55d 100644 --- a/src/components/queue/QueueOverlayExpanded.vue +++ b/src/components/queue/QueueOverlayExpanded.vue @@ -51,6 +51,7 @@ import type { } from '@/composables/queue/useJobList' import type { MenuEntry } from '@/composables/queue/useJobMenu' import { useJobMenu } from '@/composables/queue/useJobMenu' +import { useErrorHandling } from '@/composables/useErrorHandling' import QueueOverlayHeader from './QueueOverlayHeader.vue' import JobContextMenu from './job/JobContextMenu.vue' @@ -83,6 +84,7 @@ const emit = defineEmits<{ const currentMenuItem = ref(null) const jobContextMenuRef = ref | null>(null) +const { wrapWithErrorHandlingAsync } = useErrorHandling() const { jobMenuEntries } = useJobMenu( () => currentMenuItem.value, @@ -102,9 +104,9 @@ const onMenuItem = (item: JobListItem, event: Event) => { jobContextMenuRef.value?.open(event) } -const onJobMenuAction = async (entry: MenuEntry) => { +const onJobMenuAction = wrapWithErrorHandlingAsync(async (entry: MenuEntry) => { if (entry.kind === 'divider') return if (entry.onClick) await entry.onClick() jobContextMenuRef.value?.hide() -} +}) diff --git a/src/components/queue/QueueOverlayHeader.test.ts b/src/components/queue/QueueOverlayHeader.test.ts index 97da08684..b902fce6c 100644 --- a/src/components/queue/QueueOverlayHeader.test.ts +++ b/src/components/queue/QueueOverlayHeader.test.ts @@ -1,28 +1,40 @@ import { mount } from '@vue/test-utils' -import { describe, expect, it, vi } from 'vitest' +import { beforeEach, describe, expect, it, vi } from 'vitest' import { createI18n } from 'vue-i18n' -import { defineComponent } from 'vue' +import { defineComponent, h } from 'vue' -const popoverToggleSpy = vi.fn() -const popoverHideSpy = vi.fn() +const popoverCloseSpy = vi.fn() -vi.mock('primevue/popover', () => { +vi.mock('@/components/ui/Popover.vue', () => { const PopoverStub = defineComponent({ name: 'Popover', - setup(_, { slots, expose }) { - const toggle = (event: Event) => { - popoverToggleSpy(event) - } - const hide = () => { - popoverHideSpy() - } - expose({ toggle, hide }) - return () => slots.default?.() + setup(_, { slots }) { + return () => + h('div', [ + slots.button?.(), + slots.default?.({ + close: () => { + popoverCloseSpy() + } + }) + ]) } }) return { default: PopoverStub } }) +const mockGetSetting = vi.fn((key: string) => + key === 'Comfy.Queue.QPOV2' ? true : undefined +) +const mockSetSetting = vi.fn() + +vi.mock('@/platform/settings/settingStore', () => ({ + useSettingStore: () => ({ + get: mockGetSetting, + set: mockSetSetting + }) +})) + import QueueOverlayHeader from './QueueOverlayHeader.vue' import * as tooltipConfig from '@/composables/useTooltipConfig' @@ -43,7 +55,8 @@ const i18n = createI18n({ queuedSuffix: 'queued', clearQueued: 'Clear queued', moreOptions: 'More options', - clearHistory: 'Clear history' + clearHistory: 'Clear history', + dockedJobHistory: 'Docked Job History' } } } @@ -66,6 +79,11 @@ const mountHeader = (props = {}) => }) describe('QueueOverlayHeader', () => { + beforeEach(() => { + popoverCloseSpy.mockClear() + mockSetSetting.mockClear() + }) + it('renders header title and concurrent indicator when enabled', () => { const wrapper = mountHeader({ concurrentWorkflowCount: 3 }) @@ -102,19 +120,33 @@ describe('QueueOverlayHeader', () => { ) }) - it('toggles popover and emits clear history', async () => { + it('emits clear history from the menu', async () => { const spy = vi.spyOn(tooltipConfig, 'buildTooltipConfig') const wrapper = mountHeader() - const moreButton = wrapper.get('button[aria-label="More options"]') - await moreButton.trigger('click') - expect(popoverToggleSpy).toHaveBeenCalledTimes(1) + expect(wrapper.find('button[aria-label="More options"]').exists()).toBe( + true + ) expect(spy).toHaveBeenCalledWith('More') - const clearHistoryButton = wrapper.get('button[aria-label="Clear history"]') + const clearHistoryButton = wrapper.get( + '[data-testid="clear-history-action"]' + ) await clearHistoryButton.trigger('click') - expect(popoverHideSpy).toHaveBeenCalledTimes(1) + expect(popoverCloseSpy).toHaveBeenCalledTimes(1) expect(wrapper.emitted('clearHistory')).toHaveLength(1) }) + + it('toggles docked job history setting from the menu', async () => { + const wrapper = mountHeader() + + const dockedJobHistoryButton = wrapper.get( + '[data-testid="docked-job-history-action"]' + ) + await dockedJobHistoryButton.trigger('click') + + expect(mockSetSetting).toHaveBeenCalledTimes(1) + expect(mockSetSetting).toHaveBeenCalledWith('Comfy.Queue.QPOV2', false) + }) }) diff --git a/src/components/queue/QueueOverlayHeader.vue b/src/components/queue/QueueOverlayHeader.vue index 658039248..e84256713 100644 --- a/src/components/queue/QueueOverlayHeader.vue +++ b/src/components/queue/QueueOverlayHeader.vue @@ -37,62 +37,17 @@ -
- - -
- -
-
-
+ diff --git a/src/components/queue/QueueProgressOverlay.test.ts b/src/components/queue/QueueProgressOverlay.test.ts index 801aacfdc..3ed240c89 100644 --- a/src/components/queue/QueueProgressOverlay.test.ts +++ b/src/components/queue/QueueProgressOverlay.test.ts @@ -7,6 +7,7 @@ import QueueProgressOverlay from '@/components/queue/QueueProgressOverlay.vue' import { i18n } from '@/i18n' import type { JobStatus } from '@/platform/remote/comfyui/jobs/jobTypes' import { TaskItemImpl, useQueueStore } from '@/stores/queueStore' +import { useSidebarTabStore } from '@/stores/workspace/sidebarTabStore' vi.mock('@/platform/distribution/types', () => ({ isCloud: false @@ -20,7 +21,12 @@ const QueueOverlayExpandedStub = defineComponent({ required: true } }, - template: '
{{ headerTitle }}
' + template: ` +
+
{{ headerTitle }}
+
+ ` }) function createTask(id: string, status: JobStatus): TaskItemImpl { @@ -41,10 +47,11 @@ const mountComponent = ( stubActions: false }) const queueStore = useQueueStore(pinia) + const sidebarTabStore = useSidebarTabStore(pinia) queueStore.runningTasks = runningTasks queueStore.pendingTasks = pendingTasks - return mount(QueueProgressOverlay, { + const wrapper = mount(QueueProgressOverlay, { props: { expanded: true }, @@ -60,6 +67,8 @@ const mountComponent = ( } } }) + + return { wrapper, sidebarTabStore } } describe('QueueProgressOverlay', () => { @@ -68,7 +77,7 @@ describe('QueueProgressOverlay', () => { }) it('shows expanded header with running and queued labels', () => { - const wrapper = mountComponent( + const { wrapper } = mountComponent( [ createTask('running-1', 'in_progress'), createTask('running-2', 'in_progress') @@ -82,7 +91,10 @@ describe('QueueProgressOverlay', () => { }) it('shows only running label when queued count is zero', () => { - const wrapper = mountComponent([createTask('running-1', 'in_progress')], []) + const { wrapper } = mountComponent( + [createTask('running-1', 'in_progress')], + [] + ) expect(wrapper.get('[data-testid="expanded-title"]').text()).toBe( '1 running' @@ -90,10 +102,22 @@ describe('QueueProgressOverlay', () => { }) it('shows job queue title when there are no active jobs', () => { - const wrapper = mountComponent([], []) + const { wrapper } = mountComponent([], []) expect(wrapper.get('[data-testid="expanded-title"]').text()).toBe( 'Job Queue' ) }) + + it('toggles the assets sidebar tab when show-assets is clicked', async () => { + const { wrapper, sidebarTabStore } = mountComponent([], []) + + expect(sidebarTabStore.activeSidebarTabId).toBe(null) + + await wrapper.get('[data-testid="show-assets-button"]').trigger('click') + expect(sidebarTabStore.activeSidebarTabId).toBe('assets') + + await wrapper.get('[data-testid="show-assets-button"]').trigger('click') + expect(sidebarTabStore.activeSidebarTabId).toBe(null) + }) }) diff --git a/src/components/queue/QueueProgressOverlay.vue b/src/components/queue/QueueProgressOverlay.vue index 6281589c6..d6eca5ed4 100644 --- a/src/components/queue/QueueProgressOverlay.vue +++ b/src/components/queue/QueueProgressOverlay.vue @@ -22,7 +22,7 @@ :queued-count="queuedCount" :displayed-job-groups="displayedJobGroups" :has-failed-jobs="hasFailedJobs" - @show-assets="openAssetsSidebar" + @show-assets="toggleAssetsSidebar" @clear-history="onClearHistoryFromMenu" @clear-queued="cancelQueuedWorkflows" @cancel-item="onCancelItem" @@ -59,10 +59,10 @@ import { useI18n } from 'vue-i18n' import QueueOverlayActive from '@/components/queue/QueueOverlayActive.vue' import QueueOverlayExpanded from '@/components/queue/QueueOverlayExpanded.vue' -import QueueClearHistoryDialog from '@/components/queue/dialogs/QueueClearHistoryDialog.vue' import ResultGallery from '@/components/sidebar/tabs/queue/ResultGallery.vue' import { useJobList } from '@/composables/queue/useJobList' import type { JobListItem } from '@/composables/queue/useJobList' +import { useQueueClearHistoryDialog } from '@/composables/queue/useQueueClearHistoryDialog' import { useQueueProgress } from '@/composables/queue/useQueueProgress' import { useResultGallery } from '@/composables/queue/useResultGallery' import { useErrorHandling } from '@/composables/useErrorHandling' @@ -71,7 +71,6 @@ import { isCloud } from '@/platform/distribution/types' import { api } from '@/scripts/api' import { useAssetsStore } from '@/stores/assetsStore' import { useCommandStore } from '@/stores/commandStore' -import { useDialogStore } from '@/stores/dialogStore' import { useExecutionStore } from '@/stores/executionStore' import { useQueueStore } from '@/stores/queueStore' import { useSidebarTabStore } from '@/stores/workspace/sidebarTabStore' @@ -97,9 +96,9 @@ const queueStore = useQueueStore() const commandStore = useCommandStore() const executionStore = useExecutionStore() const sidebarTabStore = useSidebarTabStore() -const dialogStore = useDialogStore() const assetsStore = useAssetsStore() const assetSelectionStore = useAssetSelectionStore() +const { showQueueClearHistoryDialog } = useQueueClearHistoryDialog() const { wrapWithErrorHandlingAsync } = useErrorHandling() const { @@ -243,6 +242,10 @@ const viewAllJobs = () => { setExpanded(true) } +const toggleAssetsSidebar = () => { + sidebarTabStore.toggleSidebarTab('assets') +} + const openAssetsSidebar = () => { sidebarTabStore.activeSidebarTabId = 'assets' } @@ -309,28 +312,7 @@ const interruptAll = wrapWithErrorHandlingAsync(async () => { await queueStore.update() }) -const showClearHistoryDialog = () => { - dialogStore.showDialog({ - key: 'queue-clear-history', - component: QueueClearHistoryDialog, - dialogComponentProps: { - headless: true, - closable: false, - closeOnEscape: true, - dismissableMask: true, - pt: { - root: { - class: 'max-w-[360px] w-auto bg-transparent border-none shadow-none' - }, - content: { - class: '!p-0 bg-transparent' - } - } - } - }) -} - const onClearHistoryFromMenu = () => { - showClearHistoryDialog() + showQueueClearHistoryDialog() } diff --git a/src/components/queue/job/JobAssetsList.vue b/src/components/queue/job/JobAssetsList.vue new file mode 100644 index 000000000..9def85052 --- /dev/null +++ b/src/components/queue/job/JobAssetsList.vue @@ -0,0 +1,110 @@ + + + diff --git a/src/components/queue/job/JobFilterActions.vue b/src/components/queue/job/JobFilterActions.vue new file mode 100644 index 000000000..fd9f8c273 --- /dev/null +++ b/src/components/queue/job/JobFilterActions.vue @@ -0,0 +1,199 @@ + + + diff --git a/src/components/queue/job/JobFilterTabs.vue b/src/components/queue/job/JobFilterTabs.vue new file mode 100644 index 000000000..3f7232170 --- /dev/null +++ b/src/components/queue/job/JobFilterTabs.vue @@ -0,0 +1,45 @@ + + + diff --git a/src/components/queue/job/JobFiltersBar.test.ts b/src/components/queue/job/JobFiltersBar.test.ts index 69e3b7901..c4043f534 100644 --- a/src/components/queue/job/JobFiltersBar.test.ts +++ b/src/components/queue/job/JobFiltersBar.test.ts @@ -76,4 +76,24 @@ describe('JobFiltersBar', () => { expect(wrapper.emitted('showAssets')).toHaveLength(1) }) + + it('hides the assets icon button when hideShowAssetsAction is true', () => { + const wrapper = mount(JobFiltersBar, { + props: { + selectedJobTab: 'All', + selectedWorkflowFilter: 'all', + selectedSortMode: 'mostRecent', + hasFailedJobs: false, + hideShowAssetsAction: true + }, + global: { + plugins: [i18n], + directives: { tooltip: () => undefined } + } + }) + + expect( + wrapper.find('button[aria-label="Show assets panel"]').exists() + ).toBe(false) + }) }) diff --git a/src/components/queue/job/JobFiltersBar.vue b/src/components/queue/job/JobFiltersBar.vue index 66b00195e..23366ce69 100644 --- a/src/components/queue/job/JobFiltersBar.vue +++ b/src/components/queue/job/JobFiltersBar.vue @@ -1,225 +1,46 @@ diff --git a/src/components/sidebar/tabs/AssetsSidebarGridView.vue b/src/components/sidebar/tabs/AssetsSidebarGridView.vue index 6e95359b0..124dea50d 100644 --- a/src/components/sidebar/tabs/AssetsSidebarGridView.vue +++ b/src/components/sidebar/tabs/AssetsSidebarGridView.vue @@ -1,23 +1,7 @@