diff --git a/src/components/queue/job/JobContextMenu.test.ts b/src/components/queue/job/JobContextMenu.test.ts
new file mode 100644
index 0000000000..57c248e70d
--- /dev/null
+++ b/src/components/queue/job/JobContextMenu.test.ts
@@ -0,0 +1,81 @@
+import { mount } from '@vue/test-utils'
+import { describe, expect, it, vi } from 'vitest'
+
+import JobContextMenu from '@/components/queue/job/JobContextMenu.vue'
+import type { MenuEntry } from '@/composables/queue/useJobMenu'
+
+const buttonStub = {
+ props: {
+ disabled: {
+ type: Boolean,
+ default: false
+ }
+ },
+ template: `
+
+
+
+ `
+}
+
+const createEntries = (): MenuEntry[] => [
+ { key: 'enabled', label: 'Enabled action', onClick: vi.fn() },
+ {
+ key: 'disabled',
+ label: 'Disabled action',
+ disabled: true,
+ onClick: vi.fn()
+ },
+ { kind: 'divider', key: 'divider-1' }
+]
+
+const mountComponent = (entries: MenuEntry[]) =>
+ mount(JobContextMenu, {
+ props: { entries },
+ global: {
+ stubs: {
+ Popover: {
+ template: '
'
+ },
+ Button: buttonStub
+ }
+ }
+ })
+
+describe('JobContextMenu', () => {
+ it('passes disabled state to action buttons', () => {
+ const wrapper = mountComponent(createEntries())
+
+ const buttons = wrapper.findAll('.button-stub')
+ expect(buttons).toHaveLength(2)
+ expect(buttons[0].attributes('data-disabled')).toBe('false')
+ expect(buttons[1].attributes('data-disabled')).toBe('true')
+ })
+
+ it('emits action for enabled entries', async () => {
+ const entries = createEntries()
+ const wrapper = mountComponent(entries)
+
+ await wrapper.findAll('.button-stub')[0].trigger('click')
+
+ expect(wrapper.emitted('action')).toEqual([[entries[0]]])
+ })
+
+ it('does not emit action for disabled entries', async () => {
+ const wrapper = mountComponent([
+ {
+ key: 'disabled',
+ label: 'Disabled action',
+ disabled: true,
+ onClick: vi.fn()
+ }
+ ])
+
+ await wrapper.get('.button-stub').trigger('click')
+
+ expect(wrapper.emitted('action')).toBeUndefined()
+ })
+})
diff --git a/src/components/queue/job/JobContextMenu.vue b/src/components/queue/job/JobContextMenu.vue
index 5ed96ad9ec..c8c4e1bddf 100644
--- a/src/components/queue/job/JobContextMenu.vue
+++ b/src/components/queue/job/JobContextMenu.vue
@@ -26,6 +26,7 @@
variant="textonly"
size="sm"
:aria-label="entry.label"
+ :disabled="entry.disabled"
@click="onEntry(entry)"
>
{
'd3',
'delete'
])
+
+ expect(
+ findActionEntry(jobMenuEntries.value, 'inspect-asset')?.disabled
+ ).toBe(false)
+ expect(
+ findActionEntry(jobMenuEntries.value, 'add-to-current')?.disabled
+ ).toBe(false)
+ expect(findActionEntry(jobMenuEntries.value, 'download')?.disabled).toBe(
+ false
+ )
+
const inspectEntry = findActionEntry(jobMenuEntries.value, 'inspect-asset')
await inspectEntry?.onClick?.()
expect(inspectSpy).toHaveBeenCalledWith(currentItem.value)
@@ -760,6 +771,7 @@ describe('useJobMenu', () => {
await nextTick()
const inspectEntry = findActionEntry(jobMenuEntries.value, 'inspect-asset')
expect(inspectEntry?.onClick).toBeUndefined()
+ expect(inspectEntry?.disabled).toBe(true)
})
it('omits delete asset entry when no preview exists', async () => {
@@ -767,6 +779,15 @@ describe('useJobMenu', () => {
setCurrentItem(createJobItem({ state: 'completed', taskRef: {} }))
await nextTick()
+ expect(
+ findActionEntry(jobMenuEntries.value, 'inspect-asset')?.disabled
+ ).toBe(true)
+ expect(
+ findActionEntry(jobMenuEntries.value, 'add-to-current')?.disabled
+ ).toBe(true)
+ expect(findActionEntry(jobMenuEntries.value, 'download')?.disabled).toBe(
+ true
+ )
expect(jobMenuEntries.value.some((entry) => entry.key === 'delete')).toBe(
false
)
diff --git a/src/composables/queue/useJobMenu.ts b/src/composables/queue/useJobMenu.ts
index 0dd9908538..7c573657ea 100644
--- a/src/composables/queue/useJobMenu.ts
+++ b/src/composables/queue/useJobMenu.ts
@@ -29,6 +29,7 @@ export type MenuEntry =
key: string
label: string
icon?: string
+ disabled?: boolean
onClick?: () => void | Promise
}
| { kind: 'divider'; key: string }
@@ -239,13 +240,14 @@ export function useJobMenu(
const item = currentMenuItem()
const state = item?.state
if (!state) return []
- const hasDeletableAsset = !!item?.taskRef?.previewOutput
+ const hasPreviewAsset = !!item?.taskRef?.previewOutput
if (state === 'completed') {
return [
{
key: 'inspect-asset',
label: st('queue.jobMenu.inspectAsset', 'Inspect asset'),
icon: 'icon-[lucide--zoom-in]',
+ disabled: !hasPreviewAsset || !onInspectAsset,
onClick: onInspectAsset
? () => {
const item = currentMenuItem()
@@ -260,12 +262,14 @@ export function useJobMenu(
'Add to current workflow'
),
icon: 'icon-[comfy--node]',
+ disabled: !hasPreviewAsset,
onClick: addOutputLoaderNode
},
{
key: 'download',
label: st('queue.jobMenu.download', 'Download'),
icon: 'icon-[lucide--download]',
+ disabled: !hasPreviewAsset,
onClick: downloadPreviewAsset
},
{ kind: 'divider', key: 'd1' },
@@ -289,7 +293,7 @@ export function useJobMenu(
onClick: copyJobId
},
{ kind: 'divider', key: 'd3' },
- ...(hasDeletableAsset
+ ...(hasPreviewAsset
? [
{
key: 'delete',