mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-08 06:30:04 +00:00
feat: App mode progress updates (#9375)
## Summary - move progress bar below preview thumbnail instead of overlaying it - add interactive pending placeholder - fix: scope in-progress items to active workflow in output history ## Screenshots (if applicable) <img width="209" height="86" alt="image" src="https://github.com/user-attachments/assets/46590fdc-3df9-4a40-8492-a54e63e3f44c" /> ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-9375-feat-App-mode-progress-updates-3196d73d3650817ea891c9e744893846) by [Unito](https://www.unito.io)
This commit is contained in:
@@ -8,11 +8,13 @@ defineOptions({ inheritAttrs: false })
|
||||
const {
|
||||
class: className,
|
||||
overallOpacity = 1,
|
||||
activeOpacity = 1
|
||||
activeOpacity = 1,
|
||||
rounded = false
|
||||
} = defineProps<{
|
||||
class?: string
|
||||
overallOpacity?: number
|
||||
activeOpacity?: number
|
||||
rounded?: boolean
|
||||
}>()
|
||||
|
||||
const { totalPercent, currentNodePercent } = useQueueProgress()
|
||||
@@ -24,16 +26,19 @@ const queueStore = useQueueStore()
|
||||
cn(
|
||||
'relative h-2 bg-secondary-background transition-opacity',
|
||||
queueStore.runningTasks.length === 0 && 'opacity-0',
|
||||
rounded && 'rounded-sm',
|
||||
className
|
||||
)
|
||||
"
|
||||
>
|
||||
<div
|
||||
class="absolute inset-0 h-full bg-interface-panel-job-progress-primary transition-[width]"
|
||||
:class="cn(rounded && 'rounded-sm')"
|
||||
:style="{ width: `${totalPercent}%`, opacity: overallOpacity }"
|
||||
/>
|
||||
<div
|
||||
class="absolute inset-0 h-full bg-interface-panel-job-progress-secondary transition-[width]"
|
||||
:class="cn(rounded && 'rounded-sm')"
|
||||
:style="{ width: `${currentNodePercent}%`, opacity: activeOpacity }"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -55,6 +55,12 @@ const visibleHistory = computed(() =>
|
||||
|
||||
const selectableItems = computed(() => {
|
||||
const items: SelectionValue[] = []
|
||||
if (
|
||||
queueCount.value > 0 &&
|
||||
store.activeWorkflowInProgressItems.length === 0
|
||||
) {
|
||||
items.push({ id: 'slot:pending', kind: 'inProgress', itemId: 'pending' })
|
||||
}
|
||||
for (const item of store.activeWorkflowInProgressItems) {
|
||||
items.push({
|
||||
id: `slot:${item.id}`,
|
||||
@@ -177,7 +183,7 @@ useResizeObserver(outputsRef, () => {
|
||||
})
|
||||
watch(
|
||||
[
|
||||
() => store.inProgressItems.length,
|
||||
() => store.activeWorkflowInProgressItems.length,
|
||||
() => visibleHistory.value[0]?.id,
|
||||
queueCount
|
||||
],
|
||||
@@ -280,26 +286,36 @@ useEventListener(document.body, 'keydown', (e: KeyboardEvent) => {
|
||||
data-testid="linear-outputs"
|
||||
class="py-3 overflow-y-clip overflow-x-auto min-w-0"
|
||||
>
|
||||
<div class="flex items-center gap-0.5 mx-auto w-fit">
|
||||
<div class="flex items-start gap-0.5 mx-auto w-fit h-21">
|
||||
<div
|
||||
v-if="queueCount > 0 || hasActiveContent"
|
||||
:class="
|
||||
cn(
|
||||
'sticky left-0 z-10 shrink-0 flex items-center gap-0.5',
|
||||
'sticky left-0 z-10 shrink-0 flex items-start gap-0.5',
|
||||
'bg-comfy-menu-bg md:bg-comfy-menu-secondary-bg'
|
||||
)
|
||||
"
|
||||
>
|
||||
<div v-if="queueCount > 0" class="shrink-0 flex items-center gap-0.5">
|
||||
<OutputHistoryActiveQueueItem :queue-count="queueCount" />
|
||||
<div
|
||||
v-if="hasActiveContent || visibleHistory.length > 0"
|
||||
class="border-l border-border-default h-12 shrink-0 mx-4"
|
||||
/>
|
||||
<OutputHistoryActiveQueueItem
|
||||
v-if="queueCount > 1"
|
||||
class="mr-3"
|
||||
:queue-count="queueCount"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-if="
|
||||
queueCount > 0 && store.activeWorkflowInProgressItems.length === 0
|
||||
"
|
||||
:ref="selectedRef('slot:pending')"
|
||||
v-bind="itemAttrs('slot:pending')"
|
||||
:class="itemClass"
|
||||
@click="store.select('slot:pending')"
|
||||
>
|
||||
<OutputPreviewItem />
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-for="item in store.inProgressItems"
|
||||
v-for="item in store.activeWorkflowInProgressItems"
|
||||
:key="`${item.id}-${item.state}`"
|
||||
:ref="selectedRef(`slot:${item.id}`)"
|
||||
v-bind="itemAttrs(`slot:${item.id}`)"
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { shallowMount } from '@vue/test-utils'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import OutputHistoryActiveQueueItem from './OutputHistoryActiveQueueItem.vue'
|
||||
|
||||
vi.mock('vue-i18n', () => ({
|
||||
useI18n: () => ({ t: (key: string) => key })
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/commandStore', () => ({
|
||||
useCommandStore: () => ({
|
||||
execute: vi.fn()
|
||||
})
|
||||
}))
|
||||
|
||||
function mountComponent(queueCount: number) {
|
||||
return shallowMount(OutputHistoryActiveQueueItem, {
|
||||
props: { queueCount }
|
||||
})
|
||||
}
|
||||
|
||||
describe('OutputHistoryActiveQueueItem', () => {
|
||||
it('shows badge when queueCount is 1', () => {
|
||||
const wrapper = mountComponent(1)
|
||||
const badge = wrapper.find('[aria-hidden="true"]')
|
||||
expect(badge.exists()).toBe(true)
|
||||
expect(badge.text()).toBe('1')
|
||||
})
|
||||
|
||||
it('shows badge with correct count when queueCount is 3', () => {
|
||||
const wrapper = mountComponent(3)
|
||||
const badge = wrapper.find('[aria-hidden="true"]')
|
||||
expect(badge.exists()).toBe(true)
|
||||
expect(badge.text()).toBe('3')
|
||||
})
|
||||
|
||||
it('hides badge when queueCount is 0', () => {
|
||||
const wrapper = mountComponent(0)
|
||||
const badge = wrapper.find('[aria-hidden="true"]')
|
||||
expect(badge.exists()).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -48,7 +48,7 @@ function clearQueue(close: () => void) {
|
||||
</template>
|
||||
</Popover>
|
||||
<div
|
||||
v-if="queueCount > 1"
|
||||
v-if="queueCount > 0"
|
||||
aria-hidden="true"
|
||||
class="absolute top-0 right-0 min-w-4 h-4 flex justify-center items-center rounded-full bg-primary-background text-text-primary text-xs"
|
||||
v-text="queueCount"
|
||||
|
||||
@@ -6,16 +6,13 @@ const { latentPreview } = defineProps<{
|
||||
}>()
|
||||
</script>
|
||||
<template>
|
||||
<div class="relative size-10">
|
||||
<div class="w-10">
|
||||
<img
|
||||
v-if="latentPreview"
|
||||
class="block size-10 rounded-sm object-cover"
|
||||
:src="latentPreview"
|
||||
/>
|
||||
<div v-else class="size-10 rounded-sm skeleton-shimmer" />
|
||||
<LinearProgressBar
|
||||
class="absolute inset-0 size-full bg-transparent"
|
||||
:overall-opacity="0.7"
|
||||
/>
|
||||
<LinearProgressBar class="w-10 h-1 mt-1" rounded />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user