Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 29 additions & 2 deletions src/components/TopMenuSection.test.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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'
}
}
}
Expand All @@ -56,7 +58,12 @@ function createWrapper() {
SubgraphBreadcrumb: true,
QueueProgressOverlay: true,
CurrentUserButton: true,
LoginButton: true
LoginButton: true,
ContextMenu: {
name: 'ContextMenu',
props: ['model'],
template: '<div />'
}
},
directives: {
tooltip: () => {}
Expand Down Expand Up @@ -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)
})
Comment on lines +153 to +163
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The handleClearQueue function is missing test coverage. While the tests verify that the menu item is enabled/disabled correctly, there's no test that actually invokes the command when the user clicks the menu item. Add a test that verifies the command execution and the executionStore.clearInitializationByPromptIds call with the correct prompt IDs.

Copilot uses AI. Check for mistakes.
})
31 changes: 31 additions & 0 deletions src/components/TopMenuSection.vue
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
class="px-3"
data-testid="queue-overlay-toggle"
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The button that triggers the context menu should have the aria-haspopup attribute set to "menu" to indicate that it opens a context menu. This helps screen reader users understand the button's functionality.

Suggested change
data-testid="queue-overlay-toggle"
data-testid="queue-overlay-toggle"
aria-haspopup="menu"

Copilot uses AI. Check for mistakes.
@click="toggleQueueOverlay"
@contextmenu.stop.prevent="showQueueContextMenu"
>
<span class="text-sm font-normal tabular-nums">
{{ activeJobsLabel }}
Expand All @@ -57,6 +58,7 @@
{{ t('sideToolbar.queueProgressOverlay.expandCollapsedQueue') }}
</span>
</Button>
<ContextMenu ref="queueContextMenu" :model="queueContextMenuItems" />
<CurrentUserButton
v-if="isLoggedIn && !isIntegratedTabBar"
class="shrink-0"
Expand Down Expand Up @@ -84,6 +86,8 @@

<script setup lang="ts">
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'

Expand All @@ -101,6 +105,7 @@ 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'
Expand All @@ -119,6 +124,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)
Expand All @@ -144,6 +150,18 @@ const queueHistoryTooltipConfig = computed(() =>
const customNodesManagerTooltipConfig = computed(() =>
buildTooltipConfig(t('menu.customNodesManager'))
)
const queueContextMenu = ref<InstanceType<typeof ContextMenu> | null>(null)
const queueContextMenuItems = computed<MenuItem[]>(() => [
{
label: t('sideToolbar.queueProgressOverlay.clearQueueTooltip'),
icon: 'icon-[lucide--list-x] text-destructive-background',
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 => {
Expand All @@ -170,6 +188,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({
Expand Down