mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-01-26 19:09:52 +00:00
[feat] Add missing nodes warning UI to queue button and breadcrumb (#6674)
This commit is contained in:
@@ -2,9 +2,7 @@
|
||||
<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"
|
||||
@@ -12,20 +10,12 @@
|
||||
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
|
||||
@@ -95,6 +85,7 @@ import {
|
||||
useQueueSettingsStore
|
||||
} from '@/stores/queueStore'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||
import { useMissingNodes } from '@/workbench/extensions/manager/composables/nodePack/useMissingNodes'
|
||||
|
||||
import BatchCountEdit from '../BatchCountEdit.vue'
|
||||
|
||||
@@ -102,6 +93,8 @@ 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> = {
|
||||
@@ -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
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<a
|
||||
ref="wrapperRef"
|
||||
v-tooltip.bottom="{
|
||||
value: item.label,
|
||||
value: tooltipText,
|
||||
showDelay: 512
|
||||
}"
|
||||
draggable="false"
|
||||
@@ -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>
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -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 [
|
||||
{
|
||||
|
||||
@@ -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",
|
||||
@@ -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",
|
||||
|
||||
@@ -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'
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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: {
|
||||
@@ -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),
|
||||
@@ -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),
|
||||
@@ -209,7 +215,7 @@ describe('useMissingNodes', () => {
|
||||
|
||||
useMissingNodes()
|
||||
|
||||
expect(mockStartFetchWorkflowPacks).not.toHaveBeenCalled()
|
||||
expect(mockStartFetchWorkflowPacks).toHaveBeenCalledOnce()
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user