mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-30 11:11:53 +00:00
feat: add active jobs context menu
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
import { createTestingPinia } from '@pinia/testing'
|
import { createTestingPinia } from '@pinia/testing'
|
||||||
import { mount } from '@vue/test-utils'
|
import { mount } from '@vue/test-utils'
|
||||||
|
import type { MenuItem } from 'primevue/menuitem'
|
||||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
import { computed, nextTick } from 'vue'
|
import { computed, nextTick } from 'vue'
|
||||||
import { createI18n } from 'vue-i18n'
|
import { createI18n } from 'vue-i18n'
|
||||||
@@ -42,7 +43,8 @@ function createWrapper() {
|
|||||||
queueProgressOverlay: {
|
queueProgressOverlay: {
|
||||||
viewJobHistory: 'View job history',
|
viewJobHistory: 'View job history',
|
||||||
expandCollapsedQueue: 'Expand collapsed queue',
|
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,
|
SubgraphBreadcrumb: true,
|
||||||
QueueProgressOverlay: true,
|
QueueProgressOverlay: true,
|
||||||
CurrentUserButton: true,
|
CurrentUserButton: true,
|
||||||
LoginButton: true
|
LoginButton: true,
|
||||||
|
ContextMenu: {
|
||||||
|
name: 'ContextMenu',
|
||||||
|
props: ['model'],
|
||||||
|
template: '<div />'
|
||||||
|
}
|
||||||
},
|
},
|
||||||
directives: {
|
directives: {
|
||||||
tooltip: () => {}
|
tooltip: () => {}
|
||||||
@@ -134,4 +141,24 @@ describe('TopMenuSection', () => {
|
|||||||
const queueButton = wrapper.find('[data-testid="queue-overlay-toggle"]')
|
const queueButton = wrapper.find('[data-testid="queue-overlay-toggle"]')
|
||||||
expect(queueButton.text()).toContain('3 active')
|
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)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -44,6 +44,7 @@
|
|||||||
class="px-3"
|
class="px-3"
|
||||||
data-testid="queue-overlay-toggle"
|
data-testid="queue-overlay-toggle"
|
||||||
@click="toggleQueueOverlay"
|
@click="toggleQueueOverlay"
|
||||||
|
@contextmenu.stop.prevent="showQueueContextMenu"
|
||||||
>
|
>
|
||||||
<span class="text-sm font-normal tabular-nums">
|
<span class="text-sm font-normal tabular-nums">
|
||||||
{{ activeJobsLabel }}
|
{{ activeJobsLabel }}
|
||||||
@@ -52,6 +53,35 @@
|
|||||||
{{ t('sideToolbar.queueProgressOverlay.expandCollapsedQueue') }}
|
{{ t('sideToolbar.queueProgressOverlay.expandCollapsedQueue') }}
|
||||||
</span>
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
<ContextMenu
|
||||||
|
ref="queueContextMenu"
|
||||||
|
:model="queueContextMenuItems"
|
||||||
|
unstyled
|
||||||
|
:pt="{
|
||||||
|
root: {
|
||||||
|
class:
|
||||||
|
'rounded-lg border border-border-default bg-base-background p-2 shadow-[0px_2px_12px_0_rgba(0,0,0,0.1)] font-inter'
|
||||||
|
},
|
||||||
|
rootList: { class: 'm-0 flex list-none flex-col gap-1 p-0' },
|
||||||
|
item: { class: 'm-0 p-0' }
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<template #item="{ item, props }">
|
||||||
|
<a
|
||||||
|
v-bind="props.action"
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
'flex h-8 w-full items-center gap-2 rounded-sm px-2 text-sm font-normal',
|
||||||
|
item.class,
|
||||||
|
item.disabled && 'opacity-50'
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<i v-if="item.icon" :class="cn(item.icon, 'size-4')" />
|
||||||
|
<span>{{ item.label }}</span>
|
||||||
|
</a>
|
||||||
|
</template>
|
||||||
|
</ContextMenu>
|
||||||
<CurrentUserButton
|
<CurrentUserButton
|
||||||
v-if="isLoggedIn && !isIntegratedTabBar"
|
v-if="isLoggedIn && !isIntegratedTabBar"
|
||||||
class="shrink-0"
|
class="shrink-0"
|
||||||
@@ -76,6 +106,8 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { storeToRefs } from 'pinia'
|
import { storeToRefs } from 'pinia'
|
||||||
|
import ContextMenu from 'primevue/contextmenu'
|
||||||
|
import type { MenuItem } from 'primevue/menuitem'
|
||||||
import { computed, onMounted, ref } from 'vue'
|
import { computed, onMounted, ref } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
@@ -93,10 +125,12 @@ import { useSettingStore } from '@/platform/settings/settingStore'
|
|||||||
import { useReleaseStore } from '@/platform/updates/common/releaseStore'
|
import { useReleaseStore } from '@/platform/updates/common/releaseStore'
|
||||||
import { app } from '@/scripts/app'
|
import { app } from '@/scripts/app'
|
||||||
import { useCommandStore } from '@/stores/commandStore'
|
import { useCommandStore } from '@/stores/commandStore'
|
||||||
|
import { useExecutionStore } from '@/stores/executionStore'
|
||||||
import { useQueueStore, useQueueUIStore } from '@/stores/queueStore'
|
import { useQueueStore, useQueueUIStore } from '@/stores/queueStore'
|
||||||
import { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore'
|
import { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore'
|
||||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||||
import { isElectron } from '@/utils/envUtil'
|
import { isElectron } from '@/utils/envUtil'
|
||||||
|
import { cn } from '@/utils/tailwindUtil'
|
||||||
import { useConflictAcknowledgment } from '@/workbench/extensions/manager/composables/useConflictAcknowledgment'
|
import { useConflictAcknowledgment } from '@/workbench/extensions/manager/composables/useConflictAcknowledgment'
|
||||||
import { useManagerState } from '@/workbench/extensions/manager/composables/useManagerState'
|
import { useManagerState } from '@/workbench/extensions/manager/composables/useManagerState'
|
||||||
import { ManagerTab } from '@/workbench/extensions/manager/types/comfyManagerTypes'
|
import { ManagerTab } from '@/workbench/extensions/manager/types/comfyManagerTypes'
|
||||||
@@ -111,6 +145,7 @@ const { t, n } = useI18n()
|
|||||||
const { toastErrorHandler } = useErrorHandling()
|
const { toastErrorHandler } = useErrorHandling()
|
||||||
const commandStore = useCommandStore()
|
const commandStore = useCommandStore()
|
||||||
const queueStore = useQueueStore()
|
const queueStore = useQueueStore()
|
||||||
|
const executionStore = useExecutionStore()
|
||||||
const queueUIStore = useQueueUIStore()
|
const queueUIStore = useQueueUIStore()
|
||||||
const { activeJobsCount } = storeToRefs(queueStore)
|
const { activeJobsCount } = storeToRefs(queueStore)
|
||||||
const { isOverlayExpanded: isQueueOverlayExpanded } = storeToRefs(queueUIStore)
|
const { isOverlayExpanded: isQueueOverlayExpanded } = storeToRefs(queueUIStore)
|
||||||
@@ -135,6 +170,18 @@ const queueHistoryTooltipConfig = computed(() =>
|
|||||||
const customNodesManagerTooltipConfig = computed(() =>
|
const customNodesManagerTooltipConfig = computed(() =>
|
||||||
buildTooltipConfig(t('menu.customNodesManager'))
|
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]',
|
||||||
|
class: 'text-destructive-background',
|
||||||
|
disabled: queueStore.pendingTasks.length === 0,
|
||||||
|
command: () => {
|
||||||
|
void handleClearQueue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
// Use either release red dot or conflict red dot
|
// Use either release red dot or conflict red dot
|
||||||
const shouldShowRedDot = computed((): boolean => {
|
const shouldShowRedDot = computed((): boolean => {
|
||||||
@@ -161,6 +208,19 @@ const toggleQueueOverlay = () => {
|
|||||||
commandStore.execute('Comfy.Queue.ToggleOverlay')
|
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 () => {
|
const openCustomNodeManager = async () => {
|
||||||
try {
|
try {
|
||||||
await managerState.openManager({
|
await managerState.openManager({
|
||||||
|
|||||||
Reference in New Issue
Block a user