Skip to content

Commit a521066

Browse files
authored
[feat] Add missing nodes warning UI to queue button and breadcrumb (#6674)
1 parent ada0993 commit a521066

File tree

5 files changed

+92
-32
lines changed

5 files changed

+92
-32
lines changed

src/components/actionbar/ComfyRunButton/ComfyQueueButton.vue

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,30 +2,20 @@
22
<div class="queue-button-group flex">
33
<SplitButton
44
v-tooltip.bottom="{
5-
value: workspaceStore.shiftDown
6-
? $t('menu.runWorkflowFront')
7-
: $t('menu.runWorkflow'),
5+
value: queueButtonTooltip,
86
showDelay: 600
97
}"
108
class="comfyui-queue-button"
119
:label="String(activeQueueModeMenuItem?.label ?? '')"
1210
severity="primary"
1311
size="small"
1412
:model="queueModeMenuItems"
13+
:disabled="hasMissingNodes"
1514
data-testid="queue-button"
1615
@click="queuePrompt"
1716
>
1817
<template #icon>
19-
<i v-if="workspaceStore.shiftDown" class="icon-[lucide--list-start]" />
20-
<i v-else-if="queueMode === 'disabled'" class="icon-[lucide--play]" />
21-
<i
22-
v-else-if="queueMode === 'instant'"
23-
class="icon-[lucide--fast-forward]"
24-
/>
25-
<i
26-
v-else-if="queueMode === 'change'"
27-
class="icon-[lucide--step-forward]"
28-
/>
18+
<i :class="iconClass" />
2919
</template>
3020
<template #item="{ item }">
3121
<Button
@@ -95,13 +85,16 @@ import {
9585
useQueueSettingsStore
9686
} from '@/stores/queueStore'
9787
import { useWorkspaceStore } from '@/stores/workspaceStore'
88+
import { useMissingNodes } from '@/workbench/extensions/manager/composables/nodePack/useMissingNodes'
9889
9990
import BatchCountEdit from '../BatchCountEdit.vue'
10091
10192
const workspaceStore = useWorkspaceStore()
10293
const queueCountStore = storeToRefs(useQueuePendingTaskCountStore())
10394
const { mode: queueMode, batchCount } = storeToRefs(useQueueSettingsStore())
10495
96+
const { hasMissingNodes } = useMissingNodes()
97+
10598
const { t } = useI18n()
10699
const queueModeMenuItemLookup = computed(() => {
107100
const items: Record<string, MenuItem> = {
@@ -157,6 +150,35 @@ const hasPendingTasks = computed(
157150
() => queueCountStore.count.value > 1 || queueMode.value !== 'disabled'
158151
)
159152
153+
const iconClass = computed(() => {
154+
if (hasMissingNodes.value) {
155+
return 'icon-[lucide--triangle-alert]'
156+
}
157+
if (workspaceStore.shiftDown) {
158+
return 'icon-[lucide--list-start]'
159+
}
160+
if (queueMode.value === 'disabled') {
161+
return 'icon-[lucide--play]'
162+
}
163+
if (queueMode.value === 'instant') {
164+
return 'icon-[lucide--fast-forward]'
165+
}
166+
if (queueMode.value === 'change') {
167+
return 'icon-[lucide--step-forward]'
168+
}
169+
return 'icon-[lucide--play]'
170+
})
171+
172+
const queueButtonTooltip = computed(() => {
173+
if (hasMissingNodes.value) {
174+
return t('menu.runWorkflowDisabled')
175+
}
176+
if (workspaceStore.shiftDown) {
177+
return t('menu.runWorkflowFront')
178+
}
179+
return t('menu.runWorkflow')
180+
})
181+
160182
const commandStore = useCommandStore()
161183
const queuePrompt = async (e: Event) => {
162184
const isShiftPressed = 'shiftKey' in e && e.shiftKey

src/components/breadcrumb/SubgraphBreadcrumbItem.vue

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<a
33
ref="wrapperRef"
44
v-tooltip.bottom="{
5-
value: item.label,
5+
value: tooltipText,
66
showDelay: 512
77
}"
88
draggable="false"
@@ -16,6 +16,10 @@
1616
}"
1717
@click="handleClick"
1818
>
19+
<i
20+
v-if="hasMissingNodes && isRoot"
21+
class="icon-[lucide--triangle-alert] text-warning-background"
22+
/>
1923
<span class="p-breadcrumb-item-label px-2">{{ item.label }}</span>
2024
<Tag v-if="item.isBlueprint" :value="'Blueprint'" severity="primary" />
2125
<i v-if="isActive" class="pi pi-angle-down text-[10px]"></i>
@@ -64,6 +68,7 @@ import { useDialogService } from '@/services/dialogService'
6468
import { useCommandStore } from '@/stores/commandStore'
6569
import { useSubgraphNavigationStore } from '@/stores/subgraphNavigationStore'
6670
import { appendJsonExt } from '@/utils/formatUtil'
71+
import { useMissingNodes } from '@/workbench/extensions/manager/composables/nodePack/useMissingNodes'
6772
6873
interface Props {
6974
item: MenuItem
@@ -74,6 +79,8 @@ const props = withDefaults(defineProps<Props>(), {
7479
isActive: false
7580
})
7681
82+
const { hasMissingNodes } = useMissingNodes()
83+
7784
const { t } = useI18n()
7885
const menu = ref<InstanceType<typeof Menu> & MenuState>()
7986
const dialogService = useDialogService()
@@ -115,6 +122,14 @@ const rename = async (
115122
}
116123
117124
const isRoot = props.item.key === 'root'
125+
126+
const tooltipText = computed(() => {
127+
if (hasMissingNodes.value && isRoot) {
128+
return t('breadcrumbsMenu.missingNodesWarning')
129+
}
130+
return props.item.label
131+
})
132+
118133
const menuItems = computed<MenuItem[]>(() => {
119134
return [
120135
{

src/locales/en/main.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -779,6 +779,7 @@
779779
"onChangeTooltip": "The workflow will be queued once a change is made",
780780
"runWorkflow": "Run workflow (Shift to queue at front)",
781781
"runWorkflowFront": "Run workflow (Queue at front)",
782+
"runWorkflowDisabled": "Workflow contains unsupported nodes (highlighted red). Remove these to run the workflow.",
782783
"run": "Run",
783784
"execute": "Execute",
784785
"interrupt": "Cancel current run",
@@ -1916,7 +1917,8 @@
19161917
"clearWorkflow": "Clear Workflow",
19171918
"deleteWorkflow": "Delete Workflow",
19181919
"deleteBlueprint": "Delete Blueprint",
1919-
"enterNewName": "Enter new name"
1920+
"enterNewName": "Enter new name",
1921+
"missingNodesWarning": "Workflow contains unsupported nodes (highlighted red)."
19201922
},
19211923
"shortcuts": {
19221924
"shortcuts": "Shortcuts",

src/workbench/extensions/manager/composables/nodePack/useMissingNodes.ts

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { groupBy } from 'es-toolkit/compat'
2-
import { computed, onMounted } from 'vue'
2+
import { createSharedComposable } from '@vueuse/core'
3+
import { computed, watch } from 'vue'
34

45
import type { NodeProperty } from '@/lib/litegraph/src/LGraphNode'
56
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
7+
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
68
import { app } from '@/scripts/app'
79
import { useNodeDefStore } from '@/stores/nodeDefStore'
810
import type { components } from '@/types/comfyRegistryTypes'
@@ -14,10 +16,12 @@ import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comf
1416
* Composable to find missing NodePacks from workflow
1517
* Uses the same filtering approach as ManagerDialogContent.vue
1618
* Automatically fetches workflow pack data when initialized
19+
* This is a shared singleton composable - all components use the same instance
1720
*/
18-
export const useMissingNodes = () => {
21+
export const useMissingNodes = createSharedComposable(() => {
1922
const nodeDefStore = useNodeDefStore()
2023
const comfyManagerStore = useComfyManagerStore()
24+
const workflowStore = useWorkflowStore()
2125
const { workflowPacks, isLoading, error, startFetchWorkflowPacks } =
2226
useWorkflowPacks()
2327

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

64-
// Automatically fetch workflow pack data when composable is used
65-
onMounted(async () => {
66-
if (!workflowPacks.value.length && !isLoading.value) {
67-
await startFetchWorkflowPacks()
68-
}
68+
// Check if workflow has any missing nodes
69+
const hasMissingNodes = computed(() => {
70+
return (
71+
missingNodePacks.value.length > 0 ||
72+
Object.keys(missingCoreNodes.value).length > 0
73+
)
6974
})
7075

76+
// Re-fetch workflow packs when active workflow changes
77+
watch(
78+
() => workflowStore.activeWorkflow,
79+
async () => {
80+
await startFetchWorkflowPacks()
81+
},
82+
{ immediate: true }
83+
)
84+
7185
return {
7286
missingNodePacks,
7387
missingCoreNodes,
88+
hasMissingNodes,
7489
isLoading,
7590
error
7691
}
77-
}
92+
})

tests-ui/tests/composables/useMissingNodes.test.ts

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,12 @@ import { useMissingNodes } from '@/workbench/extensions/manager/composables/node
99
import { useWorkflowPacks } from '@/workbench/extensions/manager/composables/nodePack/useWorkflowPacks'
1010
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
1111

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

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

37+
vi.mock('@/platform/workflow/management/stores/workflowStore', () => ({
38+
useWorkflowStore: vi.fn(() => ({
39+
activeWorkflow: null
40+
}))
41+
}))
42+
3743
vi.mock('@/scripts/app', () => ({
3844
app: {
3945
graph: {
@@ -176,13 +182,13 @@ describe('useMissingNodes', () => {
176182
})
177183

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

182188
expect(mockStartFetchWorkflowPacks).toHaveBeenCalledOnce()
183189
})
184190

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

195201
useMissingNodes()
196202

197-
expect(mockStartFetchWorkflowPacks).not.toHaveBeenCalled()
203+
expect(mockStartFetchWorkflowPacks).toHaveBeenCalledOnce()
198204
})
199205

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

210216
useMissingNodes()
211217

212-
expect(mockStartFetchWorkflowPacks).not.toHaveBeenCalled()
218+
expect(mockStartFetchWorkflowPacks).toHaveBeenCalledOnce()
213219
})
214220
})
215221

0 commit comments

Comments
 (0)