mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-29 18:52:19 +00:00
fix: show stop state for active instant run button (#8917)
Switch the Run (Instant) actionbar button into a stop-state while instant auto-queue is actively running, so users can explicitly stop that mode from the same control. Figma context: https://www.figma.com/design/LVilZgHGk5RwWOkVN6yCEK/Queue-Progress-Modal?node-id=3381-6181&m=dev ## Screenshots (if applicable) https://github.com/user-attachments/assets/a4aca6ab-eb0c-41a2-9f05-3af7ecf2bedd
This commit is contained in:
197
src/components/actionbar/ComfyRunButton/ComfyQueueButton.test.ts
Normal file
197
src/components/actionbar/ComfyRunButton/ComfyQueueButton.test.ts
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
import { createTestingPinia } from '@pinia/testing'
|
||||||
|
import { mount } from '@vue/test-utils'
|
||||||
|
import { describe, expect, it, vi } from 'vitest'
|
||||||
|
import { defineComponent, nextTick } from 'vue'
|
||||||
|
import { createI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
import type {
|
||||||
|
JobListItem,
|
||||||
|
JobStatus
|
||||||
|
} from '@/platform/remote/comfyui/jobs/jobTypes'
|
||||||
|
import { useCommandStore } from '@/stores/commandStore'
|
||||||
|
import {
|
||||||
|
TaskItemImpl,
|
||||||
|
useQueueSettingsStore,
|
||||||
|
useQueueStore
|
||||||
|
} from '@/stores/queueStore'
|
||||||
|
|
||||||
|
import ComfyQueueButton from './ComfyQueueButton.vue'
|
||||||
|
|
||||||
|
vi.mock('@/platform/distribution/types', () => ({
|
||||||
|
isCloud: false
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/platform/telemetry', () => ({
|
||||||
|
useTelemetry: () => null
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/workbench/extensions/manager/utils/graphHasMissingNodes', () => ({
|
||||||
|
graphHasMissingNodes: () => false
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/scripts/app', () => ({
|
||||||
|
app: {
|
||||||
|
rootGraph: {}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/stores/workspaceStore', () => ({
|
||||||
|
useWorkspaceStore: () => ({
|
||||||
|
shiftDown: false
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
|
||||||
|
const SplitButtonStub = defineComponent({
|
||||||
|
name: 'SplitButton',
|
||||||
|
props: {
|
||||||
|
label: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
severity: {
|
||||||
|
type: String,
|
||||||
|
default: 'primary'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
template: `
|
||||||
|
<button
|
||||||
|
data-testid="split-button"
|
||||||
|
:data-label="label"
|
||||||
|
:data-severity="severity"
|
||||||
|
>
|
||||||
|
<slot name="icon" />
|
||||||
|
</button>
|
||||||
|
`
|
||||||
|
})
|
||||||
|
|
||||||
|
const i18n = createI18n({
|
||||||
|
legacy: false,
|
||||||
|
locale: 'en',
|
||||||
|
messages: {
|
||||||
|
en: {
|
||||||
|
menu: {
|
||||||
|
run: 'Run',
|
||||||
|
disabledTooltip: 'Disabled tooltip',
|
||||||
|
onChange: 'On Change',
|
||||||
|
onChangeTooltip: 'On change tooltip',
|
||||||
|
instant: 'Instant',
|
||||||
|
instantTooltip: 'Instant tooltip',
|
||||||
|
stopRunInstant: 'Stop Run (Instant)',
|
||||||
|
stopRunInstantTooltip: 'Stop running',
|
||||||
|
runWorkflow: 'Run workflow',
|
||||||
|
runWorkflowFront: 'Run workflow front',
|
||||||
|
runWorkflowDisabled: 'Run workflow disabled'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function createTask(id: string, status: JobStatus): TaskItemImpl {
|
||||||
|
const job: JobListItem = {
|
||||||
|
id,
|
||||||
|
status,
|
||||||
|
create_time: Date.now(),
|
||||||
|
priority: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return new TaskItemImpl(job)
|
||||||
|
}
|
||||||
|
|
||||||
|
function createWrapper() {
|
||||||
|
const pinia = createTestingPinia({ createSpy: vi.fn })
|
||||||
|
|
||||||
|
return mount(ComfyQueueButton, {
|
||||||
|
global: {
|
||||||
|
plugins: [pinia, i18n],
|
||||||
|
directives: {
|
||||||
|
tooltip: () => {}
|
||||||
|
},
|
||||||
|
stubs: {
|
||||||
|
SplitButton: SplitButtonStub,
|
||||||
|
BatchCountEdit: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('ComfyQueueButton', () => {
|
||||||
|
it('keeps the run instant presentation while idle even with active jobs', async () => {
|
||||||
|
const wrapper = createWrapper()
|
||||||
|
const queueSettingsStore = useQueueSettingsStore()
|
||||||
|
const queueStore = useQueueStore()
|
||||||
|
|
||||||
|
queueSettingsStore.mode = 'instant-idle'
|
||||||
|
queueStore.runningTasks = [createTask('run-1', 'in_progress')]
|
||||||
|
await nextTick()
|
||||||
|
|
||||||
|
const splitButton = wrapper.get('[data-testid="queue-button"]')
|
||||||
|
|
||||||
|
expect(splitButton.attributes('data-label')).toBe('Run (Instant)')
|
||||||
|
expect(splitButton.attributes('data-severity')).toBe('primary')
|
||||||
|
expect(wrapper.find('.icon-\\[lucide--fast-forward\\]').exists()).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('switches to stop presentation when instant mode is armed', async () => {
|
||||||
|
const wrapper = createWrapper()
|
||||||
|
const queueSettingsStore = useQueueSettingsStore()
|
||||||
|
|
||||||
|
queueSettingsStore.mode = 'instant-running'
|
||||||
|
await nextTick()
|
||||||
|
|
||||||
|
const splitButton = wrapper.get('[data-testid="queue-button"]')
|
||||||
|
|
||||||
|
expect(splitButton.attributes('data-label')).toBe('Stop Run (Instant)')
|
||||||
|
expect(splitButton.attributes('data-severity')).toBe('danger')
|
||||||
|
expect(wrapper.find('.icon-\\[lucide--square\\]').exists()).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('disarms instant mode without interrupting even when jobs are active', async () => {
|
||||||
|
const wrapper = createWrapper()
|
||||||
|
const queueSettingsStore = useQueueSettingsStore()
|
||||||
|
const queueStore = useQueueStore()
|
||||||
|
const commandStore = useCommandStore()
|
||||||
|
|
||||||
|
queueSettingsStore.mode = 'instant-running'
|
||||||
|
queueStore.runningTasks = [createTask('run-1', 'in_progress')]
|
||||||
|
await nextTick()
|
||||||
|
|
||||||
|
await wrapper.get('[data-testid="queue-button"]').trigger('click')
|
||||||
|
await nextTick()
|
||||||
|
|
||||||
|
expect(queueSettingsStore.mode).toBe('instant-idle')
|
||||||
|
const splitButtonWhileStopping = wrapper.get('[data-testid="queue-button"]')
|
||||||
|
expect(splitButtonWhileStopping.attributes('data-label')).toBe(
|
||||||
|
'Run (Instant)'
|
||||||
|
)
|
||||||
|
expect(splitButtonWhileStopping.attributes('data-severity')).toBe('primary')
|
||||||
|
expect(wrapper.find('.icon-\\[lucide--fast-forward\\]').exists()).toBe(true)
|
||||||
|
|
||||||
|
expect(commandStore.execute).not.toHaveBeenCalled()
|
||||||
|
|
||||||
|
const splitButton = wrapper.get('[data-testid="queue-button"]')
|
||||||
|
expect(queueSettingsStore.mode).toBe('instant-idle')
|
||||||
|
expect(splitButton.attributes('data-label')).toBe('Run (Instant)')
|
||||||
|
expect(splitButton.attributes('data-severity')).toBe('primary')
|
||||||
|
expect(wrapper.find('.icon-\\[lucide--fast-forward\\]').exists()).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('activates instant running mode when queueing again', async () => {
|
||||||
|
const wrapper = createWrapper()
|
||||||
|
const queueSettingsStore = useQueueSettingsStore()
|
||||||
|
const commandStore = useCommandStore()
|
||||||
|
|
||||||
|
queueSettingsStore.mode = 'instant-idle'
|
||||||
|
await nextTick()
|
||||||
|
|
||||||
|
await wrapper.get('[data-testid="queue-button"]').trigger('click')
|
||||||
|
await nextTick()
|
||||||
|
|
||||||
|
expect(queueSettingsStore.mode).toBe('instant-running')
|
||||||
|
expect(commandStore.execute).toHaveBeenCalledWith('Comfy.QueuePrompt', {
|
||||||
|
metadata: {
|
||||||
|
subscribe_to_run: false,
|
||||||
|
trigger_source: 'button'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -6,8 +6,8 @@
|
|||||||
showDelay: 600
|
showDelay: 600
|
||||||
}"
|
}"
|
||||||
class="comfyui-queue-button"
|
class="comfyui-queue-button"
|
||||||
:label="String(activeQueueModeMenuItem?.label ?? '')"
|
:label="queueButtonLabel"
|
||||||
severity="primary"
|
:severity="queueButtonSeverity"
|
||||||
size="small"
|
size="small"
|
||||||
:model="queueModeMenuItems"
|
:model="queueModeMenuItems"
|
||||||
data-testid="queue-button"
|
data-testid="queue-button"
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
value: item.tooltip,
|
value: item.tooltip,
|
||||||
showDelay: 600
|
showDelay: 600
|
||||||
}"
|
}"
|
||||||
:variant="item.key === queueMode ? 'primary' : 'secondary'"
|
:variant="item.key === selectedQueueMode ? 'primary' : 'secondary'"
|
||||||
size="sm"
|
size="sm"
|
||||||
class="w-full justify-start"
|
class="w-full justify-start"
|
||||||
>
|
>
|
||||||
@@ -48,7 +48,11 @@ import { useTelemetry } from '@/platform/telemetry'
|
|||||||
import { app } from '@/scripts/app'
|
import { app } from '@/scripts/app'
|
||||||
import { useCommandStore } from '@/stores/commandStore'
|
import { useCommandStore } from '@/stores/commandStore'
|
||||||
import { useNodeDefStore } from '@/stores/nodeDefStore'
|
import { useNodeDefStore } from '@/stores/nodeDefStore'
|
||||||
import { useQueueSettingsStore } from '@/stores/queueStore'
|
import {
|
||||||
|
isInstantMode,
|
||||||
|
isInstantRunningMode,
|
||||||
|
useQueueSettingsStore
|
||||||
|
} from '@/stores/queueStore'
|
||||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||||
import { graphHasMissingNodes } from '@/workbench/extensions/manager/utils/graphHasMissingNodes'
|
import { graphHasMissingNodes } from '@/workbench/extensions/manager/utils/graphHasMissingNodes'
|
||||||
|
|
||||||
@@ -63,6 +67,12 @@ const hasMissingNodes = computed(() =>
|
|||||||
)
|
)
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
type QueueModeMenuKey = 'disabled' | 'change' | 'instant-idle'
|
||||||
|
|
||||||
|
const selectedQueueMode = computed<QueueModeMenuKey>(() =>
|
||||||
|
isInstantMode(queueMode.value) ? 'instant-idle' : queueMode.value
|
||||||
|
)
|
||||||
|
|
||||||
const queueModeMenuItemLookup = computed(() => {
|
const queueModeMenuItemLookup = computed(() => {
|
||||||
const items: Record<string, MenuItem> = {
|
const items: Record<string, MenuItem> = {
|
||||||
disabled: {
|
disabled: {
|
||||||
@@ -86,15 +96,15 @@ const queueModeMenuItemLookup = computed(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!isCloud) {
|
if (!isCloud) {
|
||||||
items.instant = {
|
items['instant-idle'] = {
|
||||||
key: 'instant',
|
key: 'instant-idle',
|
||||||
label: `${t('menu.run')} (${t('menu.instant')})`,
|
label: `${t('menu.run')} (${t('menu.instant')})`,
|
||||||
tooltip: t('menu.instantTooltip'),
|
tooltip: t('menu.instantTooltip'),
|
||||||
command: () => {
|
command: () => {
|
||||||
useTelemetry()?.trackUiButtonClicked({
|
useTelemetry()?.trackUiButtonClicked({
|
||||||
button_id: 'queue_mode_option_run_instant_selected'
|
button_id: 'queue_mode_option_run_instant_selected'
|
||||||
})
|
})
|
||||||
queueMode.value = 'instant'
|
queueMode.value = 'instant-idle'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -104,7 +114,7 @@ const queueModeMenuItemLookup = computed(() => {
|
|||||||
const activeQueueModeMenuItem = computed(() => {
|
const activeQueueModeMenuItem = computed(() => {
|
||||||
// Fallback to disabled mode if current mode is not available (e.g., instant mode in cloud)
|
// Fallback to disabled mode if current mode is not available (e.g., instant mode in cloud)
|
||||||
return (
|
return (
|
||||||
queueModeMenuItemLookup.value[queueMode.value] ||
|
queueModeMenuItemLookup.value[selectedQueueMode.value] ||
|
||||||
queueModeMenuItemLookup.value.disabled
|
queueModeMenuItemLookup.value.disabled
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@@ -112,7 +122,24 @@ const queueModeMenuItems = computed(() =>
|
|||||||
Object.values(queueModeMenuItemLookup.value)
|
Object.values(queueModeMenuItemLookup.value)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const isStopInstantAction = computed(() =>
|
||||||
|
isInstantRunningMode(queueMode.value)
|
||||||
|
)
|
||||||
|
|
||||||
|
const queueButtonLabel = computed(() =>
|
||||||
|
isStopInstantAction.value
|
||||||
|
? t('menu.stopRunInstant')
|
||||||
|
: String(activeQueueModeMenuItem.value?.label ?? '')
|
||||||
|
)
|
||||||
|
|
||||||
|
const queueButtonSeverity = computed(() =>
|
||||||
|
isStopInstantAction.value ? 'danger' : 'primary'
|
||||||
|
)
|
||||||
|
|
||||||
const iconClass = computed(() => {
|
const iconClass = computed(() => {
|
||||||
|
if (isStopInstantAction.value) {
|
||||||
|
return 'icon-[lucide--square]'
|
||||||
|
}
|
||||||
if (hasMissingNodes.value) {
|
if (hasMissingNodes.value) {
|
||||||
return 'icon-[lucide--triangle-alert]'
|
return 'icon-[lucide--triangle-alert]'
|
||||||
}
|
}
|
||||||
@@ -122,7 +149,7 @@ const iconClass = computed(() => {
|
|||||||
if (queueMode.value === 'disabled') {
|
if (queueMode.value === 'disabled') {
|
||||||
return 'icon-[lucide--play]'
|
return 'icon-[lucide--play]'
|
||||||
}
|
}
|
||||||
if (queueMode.value === 'instant') {
|
if (isInstantMode(queueMode.value)) {
|
||||||
return 'icon-[lucide--fast-forward]'
|
return 'icon-[lucide--fast-forward]'
|
||||||
}
|
}
|
||||||
if (queueMode.value === 'change') {
|
if (queueMode.value === 'change') {
|
||||||
@@ -132,6 +159,9 @@ const iconClass = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const queueButtonTooltip = computed(() => {
|
const queueButtonTooltip = computed(() => {
|
||||||
|
if (isStopInstantAction.value) {
|
||||||
|
return t('menu.stopRunInstantTooltip')
|
||||||
|
}
|
||||||
if (hasMissingNodes.value) {
|
if (hasMissingNodes.value) {
|
||||||
return t('menu.runWorkflowDisabled')
|
return t('menu.runWorkflowDisabled')
|
||||||
}
|
}
|
||||||
@@ -143,11 +173,20 @@ const queueButtonTooltip = computed(() => {
|
|||||||
|
|
||||||
const commandStore = useCommandStore()
|
const commandStore = useCommandStore()
|
||||||
const queuePrompt = async (e: Event) => {
|
const queuePrompt = async (e: Event) => {
|
||||||
|
if (isStopInstantAction.value) {
|
||||||
|
queueMode.value = 'instant-idle'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const isShiftPressed = 'shiftKey' in e && e.shiftKey
|
const isShiftPressed = 'shiftKey' in e && e.shiftKey
|
||||||
const commandId = isShiftPressed
|
const commandId = isShiftPressed
|
||||||
? 'Comfy.QueuePromptFront'
|
? 'Comfy.QueuePromptFront'
|
||||||
: 'Comfy.QueuePrompt'
|
: 'Comfy.QueuePrompt'
|
||||||
|
|
||||||
|
if (isInstantMode(queueMode.value)) {
|
||||||
|
queueMode.value = 'instant-running'
|
||||||
|
}
|
||||||
|
|
||||||
if (batchCount.value > 1) {
|
if (batchCount.value > 1) {
|
||||||
useTelemetry()?.trackUiButtonClicked({
|
useTelemetry()?.trackUiButtonClicked({
|
||||||
button_id: 'queue_run_multiple_batches_submitted'
|
button_id: 'queue_run_multiple_batches_submitted'
|
||||||
|
|||||||
@@ -918,6 +918,8 @@
|
|||||||
"runWorkflowFront": "Run workflow (Queue at front)",
|
"runWorkflowFront": "Run workflow (Queue at front)",
|
||||||
"runWorkflowDisabled": "Workflow contains unsupported nodes (highlighted red). Remove these to run the workflow.",
|
"runWorkflowDisabled": "Workflow contains unsupported nodes (highlighted red). Remove these to run the workflow.",
|
||||||
"run": "Run",
|
"run": "Run",
|
||||||
|
"stopRunInstant": "Stop Run (Instant)",
|
||||||
|
"stopRunInstantTooltip": "Stop running",
|
||||||
"execute": "Execute",
|
"execute": "Execute",
|
||||||
"interrupt": "Cancel current run",
|
"interrupt": "Cancel current run",
|
||||||
"refresh": "Refresh node definitions",
|
"refresh": "Refresh node definitions",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { api } from '@/scripts/api'
|
import { api } from '@/scripts/api'
|
||||||
import { app } from '@/scripts/app'
|
import { app } from '@/scripts/app'
|
||||||
import {
|
import {
|
||||||
|
isInstantRunningMode,
|
||||||
useQueuePendingTaskCountStore,
|
useQueuePendingTaskCountStore,
|
||||||
useQueueSettingsStore
|
useQueueSettingsStore
|
||||||
} from '@/stores/queueStore'
|
} from '@/stores/queueStore'
|
||||||
@@ -29,7 +30,7 @@ export function setupAutoQueueHandler() {
|
|||||||
internalCount = queueCountStore.count
|
internalCount = queueCountStore.count
|
||||||
if (!internalCount && !app.lastExecutionError) {
|
if (!internalCount && !app.lastExecutionError) {
|
||||||
if (
|
if (
|
||||||
queueSettingsStore.mode === 'instant' ||
|
isInstantRunningMode(queueSettingsStore.mode) ||
|
||||||
(queueSettingsStore.mode === 'change' && graphHasChanged)
|
(queueSettingsStore.mode === 'change' && graphHasChanged)
|
||||||
) {
|
) {
|
||||||
graphHasChanged = false
|
graphHasChanged = false
|
||||||
|
|||||||
@@ -627,7 +627,20 @@ export const useQueuePendingTaskCountStore = defineStore(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
export type AutoQueueMode = 'disabled' | 'instant' | 'change'
|
export type AutoQueueMode =
|
||||||
|
| 'disabled'
|
||||||
|
| 'change'
|
||||||
|
| 'instant-idle'
|
||||||
|
| 'instant-running'
|
||||||
|
|
||||||
|
export const isInstantMode = (
|
||||||
|
mode: AutoQueueMode
|
||||||
|
): mode is 'instant-idle' | 'instant-running' =>
|
||||||
|
mode === 'instant-idle' || mode === 'instant-running'
|
||||||
|
|
||||||
|
export const isInstantRunningMode = (
|
||||||
|
mode: AutoQueueMode
|
||||||
|
): mode is 'instant-running' => mode === 'instant-running'
|
||||||
|
|
||||||
export const useQueueSettingsStore = defineStore('queueSettingsStore', {
|
export const useQueueSettingsStore = defineStore('queueSettingsStore', {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
|
|||||||
Reference in New Issue
Block a user