Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 4 additions & 1 deletion src/components/TopMenuSection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ function createWrapper() {
sideToolbar: {
queueProgressOverlay: {
viewJobHistory: 'View job history',
expandCollapsedQueue: 'Expand collapsed queue'
expandCollapsedQueue: 'Expand collapsed queue',
activeJobsShort: '{count} active | {count} active'
}
}
}
Expand All @@ -48,6 +49,8 @@ function createWrapper() {
plugins: [createTestingPinia({ createSpy: vi.fn }), i18n],
stubs: {
SubgraphBreadcrumb: true,
ComfyActionbar: true,
QueueInlineProgressSummary: true,
QueueProgressOverlay: true,
CurrentUserButton: true,
LoginButton: true
Expand Down
154 changes: 75 additions & 79 deletions src/components/TopMenuSection.vue
Original file line number Diff line number Diff line change
@@ -1,96 +1,94 @@
<template>
<div
v-if="!workspaceStore.focusMode"
class="ml-1 flex gap-x-0.5 pt-1"
class="ml-1 flex flex-col gap-1 pt-1"
@mouseenter="isTopMenuHovered = true"
@mouseleave="isTopMenuHovered = false"
>
<div class="min-w-0 flex-1">
<SubgraphBreadcrumb />
</div>
<div class="flex gap-x-0.5">
<div class="min-w-0 flex-1">
<SubgraphBreadcrumb />
</div>

<div class="mx-1 flex flex-col items-end gap-1">
<div class="flex items-center gap-2">
<div
v-if="managerState.shouldShowManagerButtons.value"
class="pointer-events-auto flex h-12 shrink-0 items-center rounded-lg border border-interface-stroke bg-comfy-menu-bg px-2 shadow-interface"
>
<Button
v-tooltip.bottom="customNodesManagerTooltipConfig"
variant="secondary"
size="icon"
:aria-label="t('menu.customNodesManager')"
class="relative"
@click="openCustomNodeManager"
<div class="mx-1 flex flex-col items-end gap-1">
<div class="flex items-center gap-2">
<div
v-if="managerState.shouldShowManagerButtons.value"
class="pointer-events-auto flex h-12 shrink-0 items-center rounded-lg border border-interface-stroke bg-comfy-menu-bg px-2 shadow-interface"
>
<i class="icon-[lucide--puzzle] size-4" />
<span
v-if="shouldShowRedDot"
class="absolute top-0.5 right-1 size-2 rounded-full bg-red-500"
/>
</Button>
</div>
<Button
v-tooltip.bottom="customNodesManagerTooltipConfig"
variant="secondary"
size="icon"
:aria-label="t('menu.customNodesManager')"
class="relative"
@click="openCustomNodeManager"
>
<i class="icon-[lucide--puzzle] size-4" />
<span
v-if="shouldShowRedDot"
class="absolute top-0.5 right-1 size-2 rounded-full bg-red-500"
/>
</Button>
</div>

<div
class="actionbar-container pointer-events-auto flex gap-2 h-12 items-center rounded-lg border border-interface-stroke bg-comfy-menu-bg px-2 shadow-interface"
>
<ActionBarButtons />
<!-- Support for legacy topbar elements attached by custom scripts, hidden if no elements present -->
<div
ref="legacyCommandsContainerRef"
class="[&:not(:has(*>*:not(:empty)))]:hidden"
></div>
<ComfyActionbar />
<Button
v-tooltip.bottom="queueHistoryTooltipConfig"
type="destructive"
size="icon"
:aria-pressed="isQueueOverlayExpanded"
:aria-label="
t('sideToolbar.queueProgressOverlay.expandCollapsedQueue')
"
@click="toggleQueueOverlay"
ref="actionbarContainerRef"
class="actionbar-container pointer-events-auto relative flex h-12 items-center gap-2 overflow-hidden rounded-lg border border-interface-stroke bg-comfy-menu-bg px-2 shadow-interface"
>
<i class="icon-[lucide--history] size-4" />
<span
v-if="queuedCount > 0"
class="absolute -top-1 -right-1 min-w-[16px] rounded-full bg-primary-background py-0.25 text-[10px] font-medium leading-[14px] text-base-foreground"
<ActionBarButtons />
<!-- Support for legacy topbar elements attached by custom scripts, hidden if no elements present -->
<div
ref="legacyCommandsContainerRef"
class="[&:not(:has(*>*:not(:empty)))]:hidden"
></div>
<ComfyActionbar
v-model:docked="isActionbarDocked"
v-model:queue-overlay-expanded="isQueueOverlayExpanded"
:top-menu-container="actionbarContainerRef"
/>
<CurrentUserButton
v-if="isLoggedIn && !isIntegratedTabBar"
class="shrink-0"
/>
<LoginButton v-else-if="isDesktop && !isIntegratedTabBar" />
<Button
v-if="!isRightSidePanelOpen"
v-tooltip.bottom="rightSidePanelTooltipConfig"
type="secondary"
size="icon"
:aria-label="t('rightSidePanel.togglePanel')"
@click="rightSidePanelStore.togglePanel"
>
{{ queuedCount }}
</span>
</Button>
<CurrentUserButton
v-if="isLoggedIn && !isIntegratedTabBar"
class="shrink-0"
/>
<LoginButton v-else-if="isDesktop && !isIntegratedTabBar" />
<Button
v-if="!isRightSidePanelOpen"
v-tooltip.bottom="rightSidePanelTooltipConfig"
type="secondary"
size="icon"
:aria-label="t('rightSidePanel.togglePanel')"
@click="rightSidePanelStore.togglePanel"
>
<i class="icon-[lucide--panel-right] size-4" />
</Button>
<i class="icon-[lucide--panel-right] size-4" />
</Button>
</div>
</div>
<QueueProgressOverlay
v-model:expanded="isQueueOverlayExpanded"
:menu-hovered="isTopMenuHovered"
/>
</div>
<QueueProgressOverlay
v-model:expanded="isQueueOverlayExpanded"
:menu-hovered="isTopMenuHovered"
</div>

<div v-if="isActionbarEnabled && !isActionbarFloating">
<QueueInlineProgressSummary
class="pr-1"
:hidden="isQueueOverlayExpanded"
/>
</div>
</div>
</template>

<script setup lang="ts">
import { useLocalStorage } from '@vueuse/core'
import { storeToRefs } from 'pinia'
import { computed, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'

import ComfyActionbar from '@/components/actionbar/ComfyActionbar.vue'
import SubgraphBreadcrumb from '@/components/breadcrumb/SubgraphBreadcrumb.vue'
import QueueInlineProgressSummary from '@/components/queue/QueueInlineProgressSummary.vue'
import QueueProgressOverlay from '@/components/queue/QueueProgressOverlay.vue'
import ActionBarButtons from '@/components/topbar/ActionBarButtons.vue'
import CurrentUserButton from '@/components/topbar/CurrentUserButton.vue'
Expand All @@ -102,8 +100,7 @@ import { buildTooltipConfig } from '@/composables/useTooltipConfig'
import { useSettingStore } from '@/platform/settings/settingStore'
import { useReleaseStore } from '@/platform/updates/common/releaseStore'
import { app } from '@/scripts/app'
import { useCommandStore } from '@/stores/commandStore'
import { useQueueStore, useQueueUIStore } from '@/stores/queueStore'
import { useQueueUIStore } from '@/stores/queueStore'
import { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore'
import { useWorkspaceStore } from '@/stores/workspaceStore'
import { isElectron } from '@/utils/envUtil'
Expand All @@ -119,22 +116,25 @@ const { isLoggedIn } = useCurrentUser()
const isDesktop = isElectron()
const { t } = useI18n()
const { toastErrorHandler } = useErrorHandling()
const commandStore = useCommandStore()
const queueStore = useQueueStore()
const queueUIStore = useQueueUIStore()
const { isOverlayExpanded: isQueueOverlayExpanded } = storeToRefs(queueUIStore)
const releaseStore = useReleaseStore()
const { shouldShowRedDot: showReleaseRedDot } = storeToRefs(releaseStore)
const { shouldShowRedDot: shouldShowConflictRedDot } =
useConflictAcknowledgment()
const isTopMenuHovered = ref(false)
const queuedCount = computed(() => queueStore.pendingTasks.length)
const actionbarContainerRef = ref<HTMLElement>()
const isActionbarDocked = useLocalStorage('Comfy.MenuPosition.Docked', true)
const actionbarPosition = computed(() => settingStore.get('Comfy.UseNewMenu'))
const isActionbarEnabled = computed(
() => actionbarPosition.value !== 'Disabled'
)
const isActionbarFloating = computed(
() => isActionbarEnabled.value && !isActionbarDocked.value
)
const isIntegratedTabBar = computed(
() => settingStore.get('Comfy.UI.TabBarLayout') === 'Integrated'
)
const queueHistoryTooltipConfig = computed(() =>
buildTooltipConfig(t('sideToolbar.queueProgressOverlay.viewJobHistory'))
)
const customNodesManagerTooltipConfig = computed(() =>
buildTooltipConfig(t('menu.customNodesManager'))
)
Expand All @@ -160,10 +160,6 @@ onMounted(() => {
}
})

const toggleQueueOverlay = () => {
commandStore.execute('Comfy.Queue.ToggleOverlay')
}

const openCustomNodeManager = async () => {
try {
await managerState.openManager({
Expand Down
129 changes: 129 additions & 0 deletions src/components/actionbar/ComfyActionbar.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { mount } from '@vue/test-utils'
import type { MenuItem } from 'primevue/menuitem'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { nextTick, reactive } from 'vue'
import { createI18n } from 'vue-i18n'

import ComfyActionbar from '@/components/actionbar/ComfyActionbar.vue'

const queueStoreMock = reactive({
pendingTasks: [] as Array<{ promptId?: string }>,
runningTasks: [] as Array<{ promptId?: string }>
})

const executionStoreMock = reactive({
isIdle: true,
clearInitializationByPromptIds: vi.fn()
})

vi.mock('@/platform/settings/settingStore', () => ({
useSettingStore: () => ({
get: vi.fn((key: string) => (key === 'Comfy.UseNewMenu' ? 'Top' : null))
})
}))

vi.mock('@/platform/telemetry', () => ({
useTelemetry: () => ({
trackUiButtonClicked: vi.fn()
})
}))

vi.mock('@/stores/commandStore', () => ({
useCommandStore: () => ({
execute: vi.fn()
})
}))

vi.mock('@/stores/executionStore', () => ({
useExecutionStore: () => executionStoreMock
}))

vi.mock('@/stores/queueStore', () => ({
useQueueStore: () => queueStoreMock
}))

function createWrapper() {
const i18n = createI18n({
legacy: false,
locale: 'en',
messages: {
en: {
sideToolbar: {
queueProgressOverlay: {
activeJobsShort: '{count} active | {count} active',
clearQueueTooltip: 'Clear queue',
viewJobHistory: 'View job history',
expandCollapsedQueue: 'Expand job queue'
}
},
menu: {
interrupt: 'Interrupt'
}
}
}
})

return mount(ComfyActionbar, {
props: {
docked: true,
queueOverlayExpanded: false
},
global: {
plugins: [i18n],
stubs: {
Panel: true,
ComfyRunButton: true,
QueueInlineProgress: true,
QueueInlineProgressSummary: true,
ContextMenu: {
name: 'ContextMenu',
props: ['model'],
template: '<div />'
}
},
directives: {
tooltip: () => {}
}
}
})
}

describe('ComfyActionbar', () => {
beforeEach(() => {
queueStoreMock.pendingTasks = []
queueStoreMock.runningTasks = []
})

it('shows the active jobs label with the current count', async () => {
const wrapper = createWrapper()
queueStoreMock.pendingTasks = [{ promptId: 'pending-1' }]
queueStoreMock.runningTasks = [
{ promptId: 'running-1' },
{ promptId: 'running-2' }
]

await nextTick()

const queueButton = wrapper.find('[data-testid="queue-overlay-toggle"]')
expect(queueButton.text()).toContain('3 active')

Check failure on line 108 in src/components/actionbar/ComfyActionbar.test.ts

View workflow job for this annotation

GitHub Actions / test

src/components/actionbar/ComfyActionbar.test.ts > ComfyActionbar > shows the active jobs label with the current count

Error: Cannot call text on an empty DOMWrapper. ❯ Object.get node_modules/.pnpm/@VUE+test-utils@2.4.6/node_modules/@vue/test-utils/dist/vue-test-utils.cjs.js:1574:27 ❯ src/components/actionbar/ComfyActionbar.test.ts:108:24

Check failure on line 108 in src/components/actionbar/ComfyActionbar.test.ts

View workflow job for this annotation

GitHub Actions / test

src/components/actionbar/ComfyActionbar.test.ts > ComfyActionbar > shows the active jobs label with the current count

Error: Cannot call text on an empty DOMWrapper. ❯ Object.get node_modules/.pnpm/@VUE+test-utils@2.4.6/node_modules/@vue/test-utils/dist/vue-test-utils.cjs.js:1574:27 ❯ src/components/actionbar/ComfyActionbar.test.ts:108:24

Check failure on line 108 in src/components/actionbar/ComfyActionbar.test.ts

View workflow job for this annotation

GitHub Actions / test

src/components/actionbar/ComfyActionbar.test.ts > ComfyActionbar > shows the active jobs label with the current count

Error: Cannot call text on an empty DOMWrapper. ❯ Object.get node_modules/.pnpm/@VUE+test-utils@2.4.6/node_modules/@vue/test-utils/dist/vue-test-utils.cjs.js:1574:27 ❯ src/components/actionbar/ComfyActionbar.test.ts:108:24
})

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[]

Check failure on line 114 in src/components/actionbar/ComfyActionbar.test.ts

View workflow job for this annotation

GitHub Actions / test

src/components/actionbar/ComfyActionbar.test.ts > ComfyActionbar > disables the clear queue context menu item when no queued jobs exist

Error: Cannot call props on an empty VueWrapper. ❯ Object.get node_modules/.pnpm/@VUE+test-utils@2.4.6/node_modules/@vue/test-utils/dist/vue-test-utils.cjs.js:1574:27 ❯ src/components/actionbar/ComfyActionbar.test.ts:114:24

Check failure on line 114 in src/components/actionbar/ComfyActionbar.test.ts

View workflow job for this annotation

GitHub Actions / test

src/components/actionbar/ComfyActionbar.test.ts > ComfyActionbar > disables the clear queue context menu item when no queued jobs exist

Error: Cannot call props on an empty VueWrapper. ❯ Object.get node_modules/.pnpm/@VUE+test-utils@2.4.6/node_modules/@vue/test-utils/dist/vue-test-utils.cjs.js:1574:27 ❯ src/components/actionbar/ComfyActionbar.test.ts:114:24

Check failure on line 114 in src/components/actionbar/ComfyActionbar.test.ts

View workflow job for this annotation

GitHub Actions / test

src/components/actionbar/ComfyActionbar.test.ts > ComfyActionbar > disables the clear queue context menu item when no queued jobs exist

Error: Cannot call props on an empty VueWrapper. ❯ Object.get node_modules/.pnpm/@VUE+test-utils@2.4.6/node_modules/@vue/test-utils/dist/vue-test-utils.cjs.js:1574:27 ❯ src/components/actionbar/ComfyActionbar.test.ts:114:24
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()
queueStoreMock.pendingTasks = [{ promptId: 'pending-1' }]

await nextTick()

const menu = wrapper.findComponent({ name: 'ContextMenu' })
const model = menu.props('model') as MenuItem[]

Check failure on line 126 in src/components/actionbar/ComfyActionbar.test.ts

View workflow job for this annotation

GitHub Actions / test

src/components/actionbar/ComfyActionbar.test.ts > ComfyActionbar > enables the clear queue context menu item when queued jobs exist

Error: Cannot call props on an empty VueWrapper. ❯ Object.get node_modules/.pnpm/@VUE+test-utils@2.4.6/node_modules/@vue/test-utils/dist/vue-test-utils.cjs.js:1574:27 ❯ src/components/actionbar/ComfyActionbar.test.ts:126:24

Check failure on line 126 in src/components/actionbar/ComfyActionbar.test.ts

View workflow job for this annotation

GitHub Actions / test

src/components/actionbar/ComfyActionbar.test.ts > ComfyActionbar > enables the clear queue context menu item when queued jobs exist

Error: Cannot call props on an empty VueWrapper. ❯ Object.get node_modules/.pnpm/@VUE+test-utils@2.4.6/node_modules/@vue/test-utils/dist/vue-test-utils.cjs.js:1574:27 ❯ src/components/actionbar/ComfyActionbar.test.ts:126:24

Check failure on line 126 in src/components/actionbar/ComfyActionbar.test.ts

View workflow job for this annotation

GitHub Actions / test

src/components/actionbar/ComfyActionbar.test.ts > ComfyActionbar > enables the clear queue context menu item when queued jobs exist

Error: Cannot call props on an empty VueWrapper. ❯ Object.get node_modules/.pnpm/@VUE+test-utils@2.4.6/node_modules/@vue/test-utils/dist/vue-test-utils.cjs.js:1574:27 ❯ src/components/actionbar/ComfyActionbar.test.ts:126:24
expect(model[0]?.disabled).toBe(false)
})
})
Loading
Loading