From 258a1d5d75c51cc1d3ab80b7b316ba850d94eda5 Mon Sep 17 00:00:00 2001 From: Benjamin Lu Date: Thu, 22 Jan 2026 16:13:35 -0800 Subject: [PATCH] feat: add active jobs context menu --- src/components/TopMenuSection.test.ts | 31 +++++++++++++- src/components/TopMenuSection.vue | 60 +++++++++++++++++++++++++++ 2 files changed, 89 insertions(+), 2 deletions(-) diff --git a/src/components/TopMenuSection.test.ts b/src/components/TopMenuSection.test.ts index 6640f9b88..944ce714c 100644 --- a/src/components/TopMenuSection.test.ts +++ b/src/components/TopMenuSection.test.ts @@ -1,5 +1,6 @@ import { createTestingPinia } from '@pinia/testing' import { mount } from '@vue/test-utils' +import type { MenuItem } from 'primevue/menuitem' import { beforeEach, describe, expect, it, vi } from 'vitest' import { computed, nextTick } from 'vue' import { createI18n } from 'vue-i18n' @@ -42,7 +43,8 @@ function createWrapper() { queueProgressOverlay: { viewJobHistory: 'View job history', expandCollapsedQueue: 'Expand collapsed queue', - activeJobsShort: '{count} active | {count} active' + activeJobsShort: '{count} active | {count} active', + clearQueueTooltip: 'Clear queue' } } } @@ -56,7 +58,12 @@ function createWrapper() { SubgraphBreadcrumb: true, QueueProgressOverlay: true, CurrentUserButton: true, - LoginButton: true + LoginButton: true, + ContextMenu: { + name: 'ContextMenu', + props: ['model'], + template: '
' + } }, directives: { tooltip: () => {} @@ -134,4 +141,24 @@ describe('TopMenuSection', () => { const queueButton = wrapper.find('[data-testid="queue-overlay-toggle"]') expect(queueButton.text()).toContain('3 active') }) + + it('disables the clear queue context menu item when no queued jobs exist', () => { + const wrapper = createWrapper() + const menu = wrapper.findComponent({ name: 'ContextMenu' }) + const model = menu.props('model') as MenuItem[] + expect(model[0]?.label).toBe('Clear queue') + expect(model[0]?.disabled).toBe(true) + }) + + it('enables the clear queue context menu item when queued jobs exist', async () => { + const wrapper = createWrapper() + const queueStore = useQueueStore() + queueStore.pendingTasks = [createTask('pending-1', 'pending')] + + await nextTick() + + const menu = wrapper.findComponent({ name: 'ContextMenu' }) + const model = menu.props('model') as MenuItem[] + expect(model[0]?.disabled).toBe(false) + }) }) diff --git a/src/components/TopMenuSection.vue b/src/components/TopMenuSection.vue index 210c03feb..67bc544eb 100644 --- a/src/components/TopMenuSection.vue +++ b/src/components/TopMenuSection.vue @@ -44,6 +44,7 @@ class="px-3" data-testid="queue-overlay-toggle" @click="toggleQueueOverlay" + @contextmenu.stop.prevent="showQueueContextMenu" > {{ activeJobsLabel }} @@ -52,6 +53,35 @@ {{ t('sideToolbar.queueProgressOverlay.expandCollapsedQueue') }} + + + import { storeToRefs } from 'pinia' +import ContextMenu from 'primevue/contextmenu' +import type { MenuItem } from 'primevue/menuitem' import { computed, onMounted, ref } from 'vue' import { useI18n } from 'vue-i18n' @@ -93,10 +125,12 @@ import { useSettingStore } from '@/platform/settings/settingStore' import { useReleaseStore } from '@/platform/updates/common/releaseStore' import { app } from '@/scripts/app' import { useCommandStore } from '@/stores/commandStore' +import { useExecutionStore } from '@/stores/executionStore' import { useQueueStore, useQueueUIStore } from '@/stores/queueStore' import { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore' import { useWorkspaceStore } from '@/stores/workspaceStore' import { isElectron } from '@/utils/envUtil' +import { cn } from '@/utils/tailwindUtil' import { useConflictAcknowledgment } from '@/workbench/extensions/manager/composables/useConflictAcknowledgment' import { useManagerState } from '@/workbench/extensions/manager/composables/useManagerState' import { ManagerTab } from '@/workbench/extensions/manager/types/comfyManagerTypes' @@ -111,6 +145,7 @@ const { t, n } = useI18n() const { toastErrorHandler } = useErrorHandling() const commandStore = useCommandStore() const queueStore = useQueueStore() +const executionStore = useExecutionStore() const queueUIStore = useQueueUIStore() const { activeJobsCount } = storeToRefs(queueStore) const { isOverlayExpanded: isQueueOverlayExpanded } = storeToRefs(queueUIStore) @@ -135,6 +170,18 @@ const queueHistoryTooltipConfig = computed(() => const customNodesManagerTooltipConfig = computed(() => buildTooltipConfig(t('menu.customNodesManager')) ) +const queueContextMenu = ref | null>(null) +const queueContextMenuItems = computed(() => [ + { + label: t('sideToolbar.queueProgressOverlay.clearQueueTooltip'), + icon: 'icon-[lucide--list-x]', + class: 'text-destructive-background', + disabled: queueStore.pendingTasks.length === 0, + command: () => { + void handleClearQueue() + } + } +]) // Use either release red dot or conflict red dot const shouldShowRedDot = computed((): boolean => { @@ -161,6 +208,19 @@ const toggleQueueOverlay = () => { commandStore.execute('Comfy.Queue.ToggleOverlay') } +const showQueueContextMenu = (event: MouseEvent) => { + queueContextMenu.value?.show(event) +} + +const handleClearQueue = async () => { + const pendingPromptIds = queueStore.pendingTasks + .map((task) => task.promptId) + .filter((id): id is string => typeof id === 'string' && id.length > 0) + + await commandStore.execute('Comfy.ClearPendingTasks') + executionStore.clearInitializationByPromptIds(pendingPromptIds) +} + const openCustomNodeManager = async () => { try { await managerState.openManager({