Skip to content
Merged
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
48 changes: 35 additions & 13 deletions src/components/actionbar/ComfyRunButton/ComfyQueueButton.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,20 @@
<div class="queue-button-group flex">
<SplitButton
v-tooltip.bottom="{
value: workspaceStore.shiftDown
? $t('menu.runWorkflowFront')
: $t('menu.runWorkflow'),
value: queueButtonTooltip,
showDelay: 600
}"
class="comfyui-queue-button"
:label="String(activeQueueModeMenuItem?.label ?? '')"
severity="primary"
size="small"
:model="queueModeMenuItems"
:disabled="hasMissingNodes"
data-testid="queue-button"
@click="queuePrompt"
>
<template #icon>
<i v-if="workspaceStore.shiftDown" class="icon-[lucide--list-start]" />
<i v-else-if="queueMode === 'disabled'" class="icon-[lucide--play]" />
<i
v-else-if="queueMode === 'instant'"
class="icon-[lucide--fast-forward]"
/>
<i
v-else-if="queueMode === 'change'"
class="icon-[lucide--step-forward]"
/>
<i :class="iconClass" />
</template>
<template #item="{ item }">
<Button
Expand Down Expand Up @@ -95,13 +85,16 @@ import {
useQueueSettingsStore
} from '@/stores/queueStore'
import { useWorkspaceStore } from '@/stores/workspaceStore'
import { useMissingNodes } from '@/workbench/extensions/manager/composables/nodePack/useMissingNodes'

import BatchCountEdit from '../BatchCountEdit.vue'

const workspaceStore = useWorkspaceStore()
const queueCountStore = storeToRefs(useQueuePendingTaskCountStore())
const { mode: queueMode, batchCount } = storeToRefs(useQueueSettingsStore())

const { hasMissingNodes } = useMissingNodes()

const { t } = useI18n()
const queueModeMenuItemLookup = computed(() => {
const items: Record<string, MenuItem> = {
Expand Down Expand Up @@ -157,6 +150,35 @@ const hasPendingTasks = computed(
() => queueCountStore.count.value > 1 || queueMode.value !== 'disabled'
)

const iconClass = computed(() => {
if (hasMissingNodes.value) {
return 'icon-[lucide--triangle-alert]'
}
if (workspaceStore.shiftDown) {
return 'icon-[lucide--list-start]'
}
if (queueMode.value === 'disabled') {
return 'icon-[lucide--play]'
}
if (queueMode.value === 'instant') {
return 'icon-[lucide--fast-forward]'
}
if (queueMode.value === 'change') {
return 'icon-[lucide--step-forward]'
}
return 'icon-[lucide--play]'
})

const queueButtonTooltip = computed(() => {
if (hasMissingNodes.value) {
return t('menu.runWorkflowDisabled')
}
if (workspaceStore.shiftDown) {
return t('menu.runWorkflowFront')
}
return t('menu.runWorkflow')
})

const commandStore = useCommandStore()
const queuePrompt = async (e: Event) => {
const isShiftPressed = 'shiftKey' in e && e.shiftKey
Expand Down
17 changes: 16 additions & 1 deletion src/components/breadcrumb/SubgraphBreadcrumbItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<a
ref="wrapperRef"
v-tooltip.bottom="{
value: item.label,
value: tooltipText,
showDelay: 512
}"
draggable="false"
Expand All @@ -16,6 +16,10 @@
}"
@click="handleClick"
>
<i
v-if="hasMissingNodes && isRoot"
class="icon-[lucide--triangle-alert] text-warning-background"
/>
<span class="p-breadcrumb-item-label px-2">{{ item.label }}</span>
<Tag v-if="item.isBlueprint" :value="'Blueprint'" severity="primary" />
<i v-if="isActive" class="pi pi-angle-down text-[10px]"></i>
Expand Down Expand Up @@ -64,6 +68,7 @@ import { useDialogService } from '@/services/dialogService'
import { useCommandStore } from '@/stores/commandStore'
import { useSubgraphNavigationStore } from '@/stores/subgraphNavigationStore'
import { appendJsonExt } from '@/utils/formatUtil'
import { useMissingNodes } from '@/workbench/extensions/manager/composables/nodePack/useMissingNodes'

interface Props {
item: MenuItem
Expand All @@ -74,6 +79,8 @@ const props = withDefaults(defineProps<Props>(), {
isActive: false
})

const { hasMissingNodes } = useMissingNodes()

const { t } = useI18n()
const menu = ref<InstanceType<typeof Menu> & MenuState>()
const dialogService = useDialogService()
Expand Down Expand Up @@ -115,6 +122,14 @@ const rename = async (
}

const isRoot = props.item.key === 'root'

const tooltipText = computed(() => {
if (hasMissingNodes.value && isRoot) {
return t('breadcrumbsMenu.missingNodesWarning')
}
return props.item.label
})

const menuItems = computed<MenuItem[]>(() => {
return [
{
Expand Down
4 changes: 3 additions & 1 deletion src/locales/en/main.json
Original file line number Diff line number Diff line change
Expand Up @@ -779,6 +779,7 @@
"onChangeTooltip": "The workflow will be queued once a change is made",
"runWorkflow": "Run workflow (Shift to queue at front)",
"runWorkflowFront": "Run workflow (Queue at front)",
"runWorkflowDisabled": "Workflow contains unsupported nodes (highlighted red). Remove these to run the workflow.",
"run": "Run",
"execute": "Execute",
"interrupt": "Cancel current run",
Expand Down Expand Up @@ -1916,7 +1917,8 @@
"clearWorkflow": "Clear Workflow",
"deleteWorkflow": "Delete Workflow",
"deleteBlueprint": "Delete Blueprint",
"enterNewName": "Enter new name"
"enterNewName": "Enter new name",
"missingNodesWarning": "Workflow contains unsupported nodes (highlighted red)."
},
"shortcuts": {
"shortcuts": "Shortcuts",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { groupBy } from 'es-toolkit/compat'
import { computed, onMounted } from 'vue'
import { createSharedComposable } from '@vueuse/core'
import { computed, watch } from 'vue'

import type { NodeProperty } from '@/lib/litegraph/src/LGraphNode'
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
import { app } from '@/scripts/app'
import { useNodeDefStore } from '@/stores/nodeDefStore'
import type { components } from '@/types/comfyRegistryTypes'
Expand All @@ -14,10 +16,12 @@ import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comf
* Composable to find missing NodePacks from workflow
* Uses the same filtering approach as ManagerDialogContent.vue
* Automatically fetches workflow pack data when initialized
* This is a shared singleton composable - all components use the same instance
*/
export const useMissingNodes = () => {
export const useMissingNodes = createSharedComposable(() => {
const nodeDefStore = useNodeDefStore()
const comfyManagerStore = useComfyManagerStore()
const workflowStore = useWorkflowStore()
const { workflowPacks, isLoading, error, startFetchWorkflowPacks } =
useWorkflowPacks()

Expand Down Expand Up @@ -61,17 +65,28 @@ export const useMissingNodes = () => {
return groupBy(missingNodes, (node) => String(node.properties?.ver || ''))
})

// Automatically fetch workflow pack data when composable is used
onMounted(async () => {
if (!workflowPacks.value.length && !isLoading.value) {
await startFetchWorkflowPacks()
}
// Check if workflow has any missing nodes
const hasMissingNodes = computed(() => {
return (
missingNodePacks.value.length > 0 ||
Object.keys(missingCoreNodes.value).length > 0
)
})

// Re-fetch workflow packs when active workflow changes
watch(
() => workflowStore.activeWorkflow,
async () => {
await startFetchWorkflowPacks()
},
{ immediate: true }
)

return {
missingNodePacks,
missingCoreNodes,
hasMissingNodes,
isLoading,
error
}
}
})
24 changes: 15 additions & 9 deletions tests-ui/tests/composables/useMissingNodes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ import { useMissingNodes } from '@/workbench/extensions/manager/composables/node
import { useWorkflowPacks } from '@/workbench/extensions/manager/composables/nodePack/useWorkflowPacks'
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'

// Mock Vue's onMounted to execute immediately for testing
vi.mock('vue', async () => {
const actual = await vi.importActual<typeof import('vue')>('vue')
vi.mock('@vueuse/core', async () => {
const actual =
await vi.importActual<typeof import('@vueuse/core')>('@vueuse/core')
return {
...actual,
onMounted: (cb: () => void) => cb()
createSharedComposable: <Fn extends (...args: any[]) => any>(fn: Fn) => fn
}
})

Expand All @@ -34,6 +34,12 @@ vi.mock('@/stores/nodeDefStore', () => ({
useNodeDefStore: vi.fn()
}))

vi.mock('@/platform/workflow/management/stores/workflowStore', () => ({
useWorkflowStore: vi.fn(() => ({
activeWorkflow: null
}))
}))

vi.mock('@/scripts/app', () => ({
app: {
graph: {
Expand Down Expand Up @@ -176,13 +182,13 @@ describe('useMissingNodes', () => {
})

describe('automatic data fetching', () => {
it('fetches workflow packs automatically when none exist', async () => {
it('fetches workflow packs automatically on initialization via watch with immediate:true', async () => {
useMissingNodes()

expect(mockStartFetchWorkflowPacks).toHaveBeenCalledOnce()
})

it('does not fetch when packs already exist', async () => {
it('fetches even when packs already exist (watch always fires with immediate:true)', async () => {
mockUseWorkflowPacks.mockReturnValue({
workflowPacks: ref(mockWorkflowPacks),
isLoading: ref(false),
Expand All @@ -194,10 +200,10 @@ describe('useMissingNodes', () => {

useMissingNodes()

expect(mockStartFetchWorkflowPacks).not.toHaveBeenCalled()
expect(mockStartFetchWorkflowPacks).toHaveBeenCalledOnce()
})

it('does not fetch when already loading', async () => {
it('fetches even when already loading (watch fires regardless of loading state)', async () => {
mockUseWorkflowPacks.mockReturnValue({
workflowPacks: ref([]),
isLoading: ref(true),
Expand All @@ -209,7 +215,7 @@ describe('useMissingNodes', () => {

useMissingNodes()

expect(mockStartFetchWorkflowPacks).not.toHaveBeenCalled()
expect(mockStartFetchWorkflowPacks).toHaveBeenCalledOnce()
})
})

Expand Down
Loading