mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-12 08:30:08 +00:00
fix: align run controls with queue modal design (#9134)
## Summary - move queue batch controls to the left of the run button - align run control styling to the Figma queue modal spec using PrimeVue PT/Tailwind (secondary background on batch + dropdown, primary run button) - normalize control heights to match actionbar buttons and tighten dropdown hit area - update run typography/spacing and replace all three chevrons (dropdown + batch up/down) with the requested SVG Design: https://www.figma.com/design/LVilZgHGk5RwWOkVN6yCEK/Queue-Progress-Modal?node-id=3845-23904&m=dev <img width="303" height="122" alt="image" src="https://github.com/user-attachments/assets/4ed80ee7-3ceb-4512-96ce-f55ec6da835e" /> ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-9134-fix-align-run-controls-with-queue-modal-design-3106d73d36508160afcedbcfe4b98291) by [Unito](https://www.unito.io) --------- Co-authored-by: github-actions <github-actions@github.com> Co-authored-by: GitHub Action <action@github.com>
This commit is contained in:
98
src/components/actionbar/BatchCountEdit.test.ts
Normal file
98
src/components/actionbar/BatchCountEdit.test.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import { createTestingPinia } from '@pinia/testing'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { nextTick } from 'vue'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import { useQueueSettingsStore } from '@/stores/queueStore'
|
||||
|
||||
import BatchCountEdit from './BatchCountEdit.vue'
|
||||
|
||||
const maxBatchCount = 16
|
||||
|
||||
vi.mock('@/platform/settings/settingStore', () => ({
|
||||
useSettingStore: () => ({
|
||||
get: (settingId: string) =>
|
||||
settingId === 'Comfy.QueueButton.BatchCountLimit' ? maxBatchCount : 1
|
||||
})
|
||||
}))
|
||||
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: 'en',
|
||||
messages: {
|
||||
en: {
|
||||
g: {
|
||||
increment: 'Increment',
|
||||
decrement: 'Decrement'
|
||||
},
|
||||
menu: {
|
||||
batchCount: 'Batch Count'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
function createWrapper(initialBatchCount = 1) {
|
||||
const pinia = createTestingPinia({
|
||||
createSpy: vi.fn,
|
||||
stubActions: false,
|
||||
initialState: {
|
||||
queueSettingsStore: {
|
||||
batchCount: initialBatchCount
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const wrapper = mount(BatchCountEdit, {
|
||||
global: {
|
||||
plugins: [pinia, i18n],
|
||||
directives: {
|
||||
tooltip: () => {}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const queueSettingsStore = useQueueSettingsStore()
|
||||
|
||||
return { wrapper, queueSettingsStore }
|
||||
}
|
||||
|
||||
describe('BatchCountEdit', () => {
|
||||
it('doubles the current batch count when increment is clicked', async () => {
|
||||
const { wrapper, queueSettingsStore } = createWrapper(3)
|
||||
|
||||
await wrapper.get('button[aria-label="Increment"]').trigger('click')
|
||||
|
||||
expect(queueSettingsStore.batchCount).toBe(6)
|
||||
})
|
||||
|
||||
it('halves the current batch count when decrement is clicked', async () => {
|
||||
const { wrapper, queueSettingsStore } = createWrapper(9)
|
||||
|
||||
await wrapper.get('button[aria-label="Decrement"]').trigger('click')
|
||||
|
||||
expect(queueSettingsStore.batchCount).toBe(4)
|
||||
})
|
||||
|
||||
it('clamps typed values to queue limits on blur', async () => {
|
||||
const { wrapper, queueSettingsStore } = createWrapper(2)
|
||||
const input = wrapper.get('input')
|
||||
|
||||
await input.setValue('999')
|
||||
await input.trigger('blur')
|
||||
await nextTick()
|
||||
|
||||
expect(queueSettingsStore.batchCount).toBe(maxBatchCount)
|
||||
expect((input.element as HTMLInputElement).value).toBe(
|
||||
String(maxBatchCount)
|
||||
)
|
||||
|
||||
await input.setValue('0')
|
||||
await input.trigger('blur')
|
||||
await nextTick()
|
||||
|
||||
expect(queueSettingsStore.batchCount).toBe(1)
|
||||
expect((input.element as HTMLInputElement).value).toBe('1')
|
||||
})
|
||||
})
|
||||
@@ -1,71 +1,129 @@
|
||||
<template>
|
||||
<div
|
||||
v-tooltip.bottom="{
|
||||
value: $t('menu.batchCount'),
|
||||
value: t('menu.batchCount'),
|
||||
showDelay: 600
|
||||
}"
|
||||
class="batch-count"
|
||||
:aria-label="$t('menu.batchCount')"
|
||||
class="batch-count h-full"
|
||||
:aria-label="t('menu.batchCount')"
|
||||
>
|
||||
<InputNumber
|
||||
v-model="batchCount"
|
||||
class="w-14"
|
||||
:min="minQueueCount"
|
||||
:max="maxQueueCount"
|
||||
fluid
|
||||
show-buttons
|
||||
:pt="{
|
||||
incrementButton: {
|
||||
class: 'w-6',
|
||||
onmousedown: () => {
|
||||
handleClick(true)
|
||||
}
|
||||
},
|
||||
decrementButton: {
|
||||
class: 'w-6',
|
||||
onmousedown: () => {
|
||||
handleClick(false)
|
||||
}
|
||||
}
|
||||
}"
|
||||
/>
|
||||
<div
|
||||
class="flex h-full w-14 overflow-hidden rounded-l-lg bg-secondary-background"
|
||||
>
|
||||
<input
|
||||
ref="batchCountInputRef"
|
||||
v-model="batchCountInput"
|
||||
type="text"
|
||||
inputmode="numeric"
|
||||
:aria-label="t('menu.batchCount')"
|
||||
:class="inputClass"
|
||||
@focus="onInputFocus"
|
||||
@input="onInput"
|
||||
@blur="onInputBlur"
|
||||
@keydown.enter.prevent="onInputEnter"
|
||||
/>
|
||||
<div class="flex h-full w-6 flex-col">
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="unset"
|
||||
:aria-label="t('g.increment')"
|
||||
:class="cn(stepButtonClass, incrementButtonClass)"
|
||||
:disabled="isIncrementDisabled"
|
||||
@click="incrementBatchCount"
|
||||
>
|
||||
<TinyChevronIcon rotate-up />
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="unset"
|
||||
:aria-label="t('g.decrement')"
|
||||
:class="cn(stepButtonClass, decrementButtonClass)"
|
||||
:disabled="isDecrementDisabled"
|
||||
@click="decrementBatchCount"
|
||||
>
|
||||
<TinyChevronIcon />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { storeToRefs } from 'pinia'
|
||||
import InputNumber from 'primevue/inputnumber'
|
||||
import { computed } from 'vue'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useQueueSettingsStore } from '@/stores/queueStore'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
import TinyChevronIcon from './TinyChevronIcon.vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const queueSettingsStore = useQueueSettingsStore()
|
||||
const { batchCount } = storeToRefs(queueSettingsStore)
|
||||
const minQueueCount = 1
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
const minQueueCount = 1
|
||||
const maxQueueCount = computed(() =>
|
||||
settingStore.get('Comfy.QueueButton.BatchCountLimit')
|
||||
)
|
||||
|
||||
const handleClick = (increment: boolean) => {
|
||||
let newCount: number
|
||||
if (increment) {
|
||||
const originalCount = batchCount.value - 1
|
||||
newCount = Math.min(originalCount * 2, maxQueueCount.value)
|
||||
} else {
|
||||
const originalCount = batchCount.value + 1
|
||||
newCount = Math.floor(originalCount / 2)
|
||||
}
|
||||
const batchCountInputRef = ref<HTMLInputElement | null>(null)
|
||||
const batchCountInput = ref(String(batchCount.value))
|
||||
const isEditing = ref(false)
|
||||
|
||||
batchCount.value = newCount
|
||||
const isIncrementDisabled = computed(
|
||||
() => batchCount.value >= maxQueueCount.value
|
||||
)
|
||||
const isDecrementDisabled = computed(() => batchCount.value <= minQueueCount)
|
||||
const inputClass =
|
||||
'h-full min-w-0 flex-1 border-none bg-secondary-background pl-1 pr-0 text-center text-sm font-normal tabular-nums text-base-foreground outline-none'
|
||||
const stepButtonClass =
|
||||
'h-1/2 w-full rounded-none border-none p-0 text-muted-foreground hover:bg-secondary-background-hover disabled:cursor-not-allowed disabled:opacity-50'
|
||||
const incrementButtonClass = 'rounded-tr-none border-b border-border-subtle'
|
||||
const decrementButtonClass = 'rounded-br-none'
|
||||
|
||||
watch(batchCount, (nextBatchCount) => {
|
||||
if (!isEditing.value) {
|
||||
batchCountInput.value = String(nextBatchCount)
|
||||
}
|
||||
})
|
||||
|
||||
const clampBatchCount = (nextBatchCount: number): number =>
|
||||
Math.min(Math.max(nextBatchCount, minQueueCount), maxQueueCount.value)
|
||||
|
||||
const setBatchCount = (nextBatchCount: number) => {
|
||||
batchCount.value = clampBatchCount(nextBatchCount)
|
||||
batchCountInput.value = String(batchCount.value)
|
||||
}
|
||||
|
||||
const incrementBatchCount = () => {
|
||||
setBatchCount(batchCount.value * 2)
|
||||
}
|
||||
|
||||
const decrementBatchCount = () => {
|
||||
setBatchCount(Math.floor(batchCount.value / 2))
|
||||
}
|
||||
|
||||
const onInputFocus = () => {
|
||||
isEditing.value = true
|
||||
}
|
||||
|
||||
const onInput = (event: Event) => {
|
||||
const input = event.target as HTMLInputElement
|
||||
batchCountInput.value = input.value.replace(/[^0-9]/g, '')
|
||||
}
|
||||
|
||||
const onInputBlur = () => {
|
||||
isEditing.value = false
|
||||
const parsedInput = Number.parseInt(batchCountInput.value, 10)
|
||||
setBatchCount(Number.isNaN(parsedInput) ? minQueueCount : parsedInput)
|
||||
}
|
||||
|
||||
const onInputEnter = () => {
|
||||
batchCountInputRef.value?.blur()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
:deep(.p-inputtext) {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createTestingPinia } from '@pinia/testing'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { defineComponent, nextTick } from 'vue'
|
||||
import { nextTick } from 'vue'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import type {
|
||||
@@ -41,28 +41,9 @@ vi.mock('@/stores/workspaceStore', () => ({
|
||||
})
|
||||
}))
|
||||
|
||||
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 BatchCountEditStub = {
|
||||
template: '<div data-testid="batch-count-edit" />'
|
||||
}
|
||||
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
@@ -107,14 +88,26 @@ function createWrapper() {
|
||||
tooltip: () => {}
|
||||
},
|
||||
stubs: {
|
||||
SplitButton: SplitButtonStub,
|
||||
BatchCountEdit: true
|
||||
BatchCountEdit: BatchCountEditStub,
|
||||
DropdownMenuRoot: { template: '<div><slot /></div>' },
|
||||
DropdownMenuTrigger: { template: '<div><slot /></div>' },
|
||||
DropdownMenuPortal: { template: '<div><slot /></div>' },
|
||||
DropdownMenuContent: { template: '<div><slot /></div>' },
|
||||
DropdownMenuItem: { template: '<div><slot /></div>' }
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
describe('ComfyQueueButton', () => {
|
||||
it('renders the batch count control before the run button', () => {
|
||||
const wrapper = createWrapper()
|
||||
const controls = wrapper.get('.queue-button-group').element.children
|
||||
|
||||
expect(controls[0]?.getAttribute('data-testid')).toBe('batch-count-edit')
|
||||
expect(controls[1]?.getAttribute('data-testid')).toBe('queue-button')
|
||||
})
|
||||
|
||||
it('keeps the run instant presentation while idle even with active jobs', async () => {
|
||||
const wrapper = createWrapper()
|
||||
const queueSettingsStore = useQueueSettingsStore()
|
||||
@@ -124,10 +117,10 @@ describe('ComfyQueueButton', () => {
|
||||
queueStore.runningTasks = [createTask('run-1', 'in_progress')]
|
||||
await nextTick()
|
||||
|
||||
const splitButton = wrapper.get('[data-testid="queue-button"]')
|
||||
const queueButton = wrapper.get('[data-testid="queue-button"]')
|
||||
|
||||
expect(splitButton.attributes('data-label')).toBe('Run (Instant)')
|
||||
expect(splitButton.attributes('data-severity')).toBe('primary')
|
||||
expect(queueButton.text()).toContain('Run (Instant)')
|
||||
expect(queueButton.attributes('data-variant')).toBe('primary')
|
||||
expect(wrapper.find('.icon-\\[lucide--fast-forward\\]').exists()).toBe(true)
|
||||
})
|
||||
|
||||
@@ -138,10 +131,10 @@ describe('ComfyQueueButton', () => {
|
||||
queueSettingsStore.mode = 'instant-running'
|
||||
await nextTick()
|
||||
|
||||
const splitButton = wrapper.get('[data-testid="queue-button"]')
|
||||
const queueButton = wrapper.get('[data-testid="queue-button"]')
|
||||
|
||||
expect(splitButton.attributes('data-label')).toBe('Stop Run (Instant)')
|
||||
expect(splitButton.attributes('data-severity')).toBe('danger')
|
||||
expect(queueButton.text()).toContain('Stop Run (Instant)')
|
||||
expect(queueButton.attributes('data-variant')).toBe('destructive')
|
||||
expect(wrapper.find('.icon-\\[lucide--square\\]').exists()).toBe(true)
|
||||
})
|
||||
|
||||
@@ -159,19 +152,17 @@ describe('ComfyQueueButton', () => {
|
||||
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')
|
||||
const queueButtonWhileStopping = wrapper.get('[data-testid="queue-button"]')
|
||||
expect(queueButtonWhileStopping.text()).toContain('Run (Instant)')
|
||||
expect(queueButtonWhileStopping.attributes('data-variant')).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"]')
|
||||
const queueButton = 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(queueButton.text()).toContain('Run (Instant)')
|
||||
expect(queueButton.attributes('data-variant')).toBe('primary')
|
||||
expect(wrapper.find('.icon-\\[lucide--fast-forward\\]').exists()).toBe(true)
|
||||
})
|
||||
|
||||
|
||||
@@ -1,48 +1,83 @@
|
||||
<template>
|
||||
<div class="queue-button-group flex">
|
||||
<SplitButton
|
||||
<ButtonGroup
|
||||
class="queue-button-group h-8 rounded-lg bg-secondary-background"
|
||||
>
|
||||
<BatchCountEdit />
|
||||
<Button
|
||||
v-tooltip.bottom="{
|
||||
value: queueButtonTooltip,
|
||||
showDelay: 600
|
||||
}"
|
||||
class="comfyui-queue-button"
|
||||
:label="queueButtonLabel"
|
||||
:severity="queueButtonSeverity"
|
||||
size="small"
|
||||
:model="queueModeMenuItems"
|
||||
:variant="queueButtonVariant"
|
||||
size="unset"
|
||||
:class="queueActionButtonClass"
|
||||
data-testid="queue-button"
|
||||
:data-variant="queueButtonVariant"
|
||||
@click="queuePrompt"
|
||||
>
|
||||
<template #icon>
|
||||
<i :class="iconClass" />
|
||||
</template>
|
||||
<template #item="{ item }">
|
||||
<i :class="cn(iconClass, 'size-4')" />
|
||||
{{ queueButtonLabel }}
|
||||
</Button>
|
||||
|
||||
<DropdownMenuRoot>
|
||||
<DropdownMenuTrigger as-child>
|
||||
<Button
|
||||
v-tooltip="{
|
||||
value: item.tooltip,
|
||||
showDelay: 600
|
||||
}"
|
||||
:variant="item.key === selectedQueueMode ? 'primary' : 'secondary'"
|
||||
size="sm"
|
||||
class="w-full justify-start"
|
||||
variant="secondary"
|
||||
size="unset"
|
||||
:class="queueMenuTriggerClass"
|
||||
:aria-label="t('menu.run')"
|
||||
data-testid="queue-mode-menu-trigger"
|
||||
>
|
||||
<i v-if="item.icon" :class="item.icon" />
|
||||
{{ String(item.label ?? '') }}
|
||||
<TinyChevronIcon />
|
||||
</Button>
|
||||
</template>
|
||||
</SplitButton>
|
||||
<BatchCountEdit />
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuPortal>
|
||||
<DropdownMenuContent
|
||||
:side-offset="4"
|
||||
class="z-1000 min-w-44 rounded-lg border border-border-subtle bg-base-background p-1 shadow-interface"
|
||||
>
|
||||
<DropdownMenuItem
|
||||
v-for="item in queueModeMenuItems"
|
||||
:key="item.key"
|
||||
as-child
|
||||
@select.prevent="item.command"
|
||||
>
|
||||
<Button
|
||||
v-tooltip="{
|
||||
value: item.tooltip,
|
||||
showDelay: 600
|
||||
}"
|
||||
:variant="
|
||||
item.key === selectedQueueMode ? 'primary' : 'secondary'
|
||||
"
|
||||
size="sm"
|
||||
:class="queueMenuItemButtonClass"
|
||||
>
|
||||
{{ item.label }}
|
||||
</Button>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenuPortal>
|
||||
</DropdownMenuRoot>
|
||||
</ButtonGroup>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuRoot,
|
||||
DropdownMenuTrigger
|
||||
} from 'reka-ui'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import type { MenuItem } from 'primevue/menuitem'
|
||||
import SplitButton from 'primevue/splitbutton'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import BatchCountEdit from '@/components/actionbar/BatchCountEdit.vue'
|
||||
import TinyChevronIcon from '@/components/actionbar/TinyChevronIcon.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import ButtonGroup from '@/components/ui/button-group/ButtonGroup.vue'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
import { app } from '@/scripts/app'
|
||||
@@ -54,10 +89,9 @@ import {
|
||||
useQueueSettingsStore
|
||||
} from '@/stores/queueStore'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
import { graphHasMissingNodes } from '@/workbench/extensions/manager/utils/graphHasMissingNodes'
|
||||
|
||||
import BatchCountEdit from '../BatchCountEdit.vue'
|
||||
|
||||
const workspaceStore = useWorkspaceStore()
|
||||
const { mode: queueMode, batchCount } = storeToRefs(useQueueSettingsStore())
|
||||
|
||||
@@ -69,50 +103,60 @@ const hasMissingNodes = computed(() =>
|
||||
const { t } = useI18n()
|
||||
type QueueModeMenuKey = 'disabled' | 'change' | 'instant-idle'
|
||||
|
||||
interface QueueModeMenuItem {
|
||||
key: QueueModeMenuKey
|
||||
label: string
|
||||
tooltip: string
|
||||
command: () => void
|
||||
}
|
||||
|
||||
const selectedQueueMode = computed<QueueModeMenuKey>(() =>
|
||||
isInstantMode(queueMode.value) ? 'instant-idle' : queueMode.value
|
||||
)
|
||||
|
||||
const queueModeMenuItemLookup = computed(() => {
|
||||
const items: Record<string, MenuItem> = {
|
||||
disabled: {
|
||||
key: 'disabled',
|
||||
label: t('menu.run'),
|
||||
tooltip: t('menu.disabledTooltip'),
|
||||
command: () => {
|
||||
queueMode.value = 'disabled'
|
||||
}
|
||||
},
|
||||
change: {
|
||||
key: 'change',
|
||||
label: `${t('menu.run')} (${t('menu.onChange')})`,
|
||||
tooltip: t('menu.onChangeTooltip'),
|
||||
command: () => {
|
||||
useTelemetry()?.trackUiButtonClicked({
|
||||
button_id: 'queue_mode_option_run_on_change_selected'
|
||||
})
|
||||
queueMode.value = 'change'
|
||||
const queueModeMenuItemLookup = computed<Record<string, QueueModeMenuItem>>(
|
||||
() => {
|
||||
const items: Record<string, QueueModeMenuItem> = {
|
||||
disabled: {
|
||||
key: 'disabled',
|
||||
label: t('menu.run'),
|
||||
tooltip: t('menu.disabledTooltip'),
|
||||
command: () => {
|
||||
queueMode.value = 'disabled'
|
||||
}
|
||||
},
|
||||
change: {
|
||||
key: 'change',
|
||||
label: `${t('menu.run')} (${t('menu.onChange')})`,
|
||||
tooltip: t('menu.onChangeTooltip'),
|
||||
command: () => {
|
||||
useTelemetry()?.trackUiButtonClicked({
|
||||
button_id: 'queue_mode_option_run_on_change_selected'
|
||||
})
|
||||
queueMode.value = 'change'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!isCloud) {
|
||||
items['instant-idle'] = {
|
||||
key: 'instant-idle',
|
||||
label: `${t('menu.run')} (${t('menu.instant')})`,
|
||||
tooltip: t('menu.instantTooltip'),
|
||||
command: () => {
|
||||
useTelemetry()?.trackUiButtonClicked({
|
||||
button_id: 'queue_mode_option_run_instant_selected'
|
||||
})
|
||||
queueMode.value = 'instant-idle'
|
||||
|
||||
if (!isCloud) {
|
||||
items['instant-idle'] = {
|
||||
key: 'instant-idle',
|
||||
label: `${t('menu.run')} (${t('menu.instant')})`,
|
||||
tooltip: t('menu.instantTooltip'),
|
||||
command: () => {
|
||||
useTelemetry()?.trackUiButtonClicked({
|
||||
button_id: 'queue_mode_option_run_instant_selected'
|
||||
})
|
||||
queueMode.value = 'instant-idle'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return items
|
||||
}
|
||||
return items
|
||||
})
|
||||
)
|
||||
|
||||
const activeQueueModeMenuItem = computed(() => {
|
||||
// Fallback to disabled mode if current mode is not available (e.g., instant mode in cloud)
|
||||
return (
|
||||
queueModeMenuItemLookup.value[selectedQueueMode.value] ||
|
||||
queueModeMenuItemLookup.value.disabled
|
||||
@@ -132,9 +176,13 @@ const queueButtonLabel = computed(() =>
|
||||
: String(activeQueueModeMenuItem.value?.label ?? '')
|
||||
)
|
||||
|
||||
const queueButtonSeverity = computed(() =>
|
||||
isStopInstantAction.value ? 'danger' : 'primary'
|
||||
const queueButtonVariant = computed<'destructive' | 'primary'>(() =>
|
||||
isStopInstantAction.value ? 'destructive' : 'primary'
|
||||
)
|
||||
const queueActionButtonClass = 'h-full rounded-lg gap-1.5 px-4 font-light'
|
||||
const queueMenuTriggerClass =
|
||||
'h-full w-6 rounded-l-none rounded-r-lg border-l border-border-subtle p-0 text-muted-foreground data-[state=open]:bg-secondary-background-hover'
|
||||
const queueMenuItemButtonClass = 'w-full justify-start font-normal'
|
||||
|
||||
const iconClass = computed(() => {
|
||||
if (isStopInstantAction.value) {
|
||||
@@ -201,10 +249,3 @@ const queuePrompt = async (e: Event) => {
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.comfyui-queue-button :deep(.p-splitbutton-dropdown) {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
26
src/components/actionbar/TinyChevronIcon.vue
Normal file
26
src/components/actionbar/TinyChevronIcon.vue
Normal file
@@ -0,0 +1,26 @@
|
||||
<template>
|
||||
<svg
|
||||
class="h-[5px] min-h-[5px] w-[8px] min-w-[8px]"
|
||||
:class="{ 'rotate-180': rotateUp }"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="8"
|
||||
height="5"
|
||||
viewBox="0 0 8 5"
|
||||
fill="none"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M0.650391 0.649902L3.65039 3.6499L6.65039 0.649902"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.3"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const { rotateUp = false } = defineProps<{
|
||||
rotateUp?: boolean
|
||||
}>()
|
||||
</script>
|
||||
26
src/components/ui/button-group/ButtonGroup.vue
Normal file
26
src/components/ui/button-group/ButtonGroup.vue
Normal file
@@ -0,0 +1,26 @@
|
||||
<script setup lang="ts">
|
||||
import type { PrimitiveProps } from 'reka-ui'
|
||||
import { Primitive, useForwardProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
interface Props extends PrimitiveProps {
|
||||
class?: HTMLAttributes['class']
|
||||
}
|
||||
|
||||
const { as = 'div', class: className = '', ...restProps } = defineProps<Props>()
|
||||
const forwardedProps = useForwardProps(restProps)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Primitive
|
||||
v-bind="forwardedProps"
|
||||
:as
|
||||
:class="
|
||||
cn('inline-flex items-stretch overflow-hidden rounded-md', className)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
</Primitive>
|
||||
</template>
|
||||
@@ -2,7 +2,7 @@ import type { VariantProps } from 'cva'
|
||||
import { cva } from 'cva'
|
||||
|
||||
export const buttonVariants = cva({
|
||||
base: 'relative inline-flex items-center justify-center gap-2 cursor-pointer whitespace-nowrap appearance-none border-none rounded-md text-sm font-medium font-inter transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
|
||||
base: 'relative inline-flex items-center justify-center gap-2 cursor-pointer whitespace-nowrap appearance-none border-none rounded-md text-sm font-medium font-inter transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([width]):not([height])]:size-4 [&_svg]:shrink-0',
|
||||
variants: {
|
||||
variant: {
|
||||
secondary:
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
:disabled="isUpdating"
|
||||
@click="updateAllPacks"
|
||||
>
|
||||
<DotSpinner v-if="isUpdating" duration="1s" :size="12" />
|
||||
<DotSpinner v-if="isUpdating" duration="1s" />
|
||||
<i v-else class="icon-[lucide--refresh-cw]" />
|
||||
<span>{{
|
||||
nodePacks.length > 1 ? $t('manager.updateAll') : $t('manager.update')
|
||||
|
||||
Reference in New Issue
Block a user