diff --git a/src/components/actionbar/ComfyRunButton/ComfyQueueButton.vue b/src/components/actionbar/ComfyRunButton/ComfyQueueButton.vue index 2f4972433f..26852dfa61 100644 --- a/src/components/actionbar/ComfyRunButton/ComfyQueueButton.vue +++ b/src/components/actionbar/ComfyRunButton/ComfyQueueButton.vue @@ -78,13 +78,15 @@ import { useI18n } from 'vue-i18n' import { isCloud } from '@/platform/distribution/types' import { useTelemetry } from '@/platform/telemetry' +import { app } from '@/scripts/app' import { useCommandStore } from '@/stores/commandStore' +import { useNodeDefStore } from '@/stores/nodeDefStore' import { useQueuePendingTaskCountStore, useQueueSettingsStore } from '@/stores/queueStore' import { useWorkspaceStore } from '@/stores/workspaceStore' -import { useMissingNodes } from '@/workbench/extensions/manager/composables/nodePack/useMissingNodes' +import { graphHasMissingNodes } from '@/workbench/extensions/manager/utils/graphHasMissingNodes' import BatchCountEdit from '../BatchCountEdit.vue' @@ -92,7 +94,10 @@ const workspaceStore = useWorkspaceStore() const queueCountStore = storeToRefs(useQueuePendingTaskCountStore()) const { mode: queueMode, batchCount } = storeToRefs(useQueueSettingsStore()) -const { hasMissingNodes } = useMissingNodes() +const nodeDefStore = useNodeDefStore() +const hasMissingNodes = computed(() => + graphHasMissingNodes(app.graph, nodeDefStore.nodeDefsByName) +) const { t } = useI18n() const queueModeMenuItemLookup = computed(() => { diff --git a/src/components/breadcrumb/SubgraphBreadcrumbItem.vue b/src/components/breadcrumb/SubgraphBreadcrumbItem.vue index 2e86925585..2181cce1b0 100644 --- a/src/components/breadcrumb/SubgraphBreadcrumbItem.vue +++ b/src/components/breadcrumb/SubgraphBreadcrumbItem.vue @@ -64,11 +64,13 @@ import { ComfyWorkflow, useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore' +import { app } from '@/scripts/app' import { useDialogService } from '@/services/dialogService' import { useCommandStore } from '@/stores/commandStore' +import { useNodeDefStore } from '@/stores/nodeDefStore' import { useSubgraphNavigationStore } from '@/stores/subgraphNavigationStore' import { appendJsonExt } from '@/utils/formatUtil' -import { useMissingNodes } from '@/workbench/extensions/manager/composables/nodePack/useMissingNodes' +import { graphHasMissingNodes } from '@/workbench/extensions/manager/utils/graphHasMissingNodes' interface Props { item: MenuItem @@ -79,7 +81,10 @@ const props = withDefaults(defineProps(), { isActive: false }) -const { hasMissingNodes } = useMissingNodes() +const nodeDefStore = useNodeDefStore() +const hasMissingNodes = computed(() => + graphHasMissingNodes(app.graph, nodeDefStore.nodeDefsByName) +) const { t } = useI18n() const menu = ref & MenuState>() diff --git a/src/workbench/extensions/manager/utils/graphHasMissingNodes.ts b/src/workbench/extensions/manager/utils/graphHasMissingNodes.ts new file mode 100644 index 0000000000..ddf3180ab2 --- /dev/null +++ b/src/workbench/extensions/manager/utils/graphHasMissingNodes.ts @@ -0,0 +1,37 @@ +import { unref } from 'vue' +import type { MaybeRef } from 'vue' + +import type { + LGraph, + LGraphNode, + Subgraph +} from '@/lib/litegraph/src/litegraph' +import type { ComfyNodeDefImpl } from '@/stores/nodeDefStore' +import { collectAllNodes } from '@/utils/graphTraversalUtil' + +export type NodeDefLookup = Record + +const isNodeMissingDefinition = ( + node: LGraphNode, + nodeDefsByName: NodeDefLookup +) => { + const nodeName = node?.type + if (!nodeName) return false + return !nodeDefsByName[nodeName] +} + +export const collectMissingNodes = ( + graph: LGraph | Subgraph | null | undefined, + nodeDefsByName: MaybeRef +): LGraphNode[] => { + if (!graph) return [] + const lookup = unref(nodeDefsByName) + return collectAllNodes(graph, (node) => isNodeMissingDefinition(node, lookup)) +} + +export const graphHasMissingNodes = ( + graph: LGraph | Subgraph | null | undefined, + nodeDefsByName: MaybeRef +) => { + return collectMissingNodes(graph, nodeDefsByName).length > 0 +} diff --git a/tests-ui/tests/workbench/extensions/manager/utils/graphHasMissingNodes.test.ts b/tests-ui/tests/workbench/extensions/manager/utils/graphHasMissingNodes.test.ts new file mode 100644 index 0000000000..2badaa7cc3 --- /dev/null +++ b/tests-ui/tests/workbench/extensions/manager/utils/graphHasMissingNodes.test.ts @@ -0,0 +1,115 @@ +import { describe, expect, it } from 'vitest' + +import type { + LGraph, + LGraphNode, + Subgraph +} from '@/lib/litegraph/src/litegraph' +import { + collectMissingNodes, + graphHasMissingNodes +} from '@/workbench/extensions/manager/utils/graphHasMissingNodes' +import type { NodeDefLookup } from '@/workbench/extensions/manager/utils/graphHasMissingNodes' +import type { ComfyNodeDefImpl } from '@/stores/nodeDefStore' + +type NodeDefs = NodeDefLookup + +let nodeIdCounter = 0 +const mockNodeDef = {} as ComfyNodeDefImpl + +const createGraph = (nodes: LGraphNode[] = []): LGraph => { + return { nodes } as Partial as LGraph +} + +const createSubgraph = (nodes: LGraphNode[]): Subgraph => { + return { nodes } as Partial as Subgraph +} + +const createNode = ( + type?: string, + subgraphNodes?: LGraphNode[] +): LGraphNode => { + return { + id: nodeIdCounter++, + type, + isSubgraphNode: subgraphNodes ? () => true : undefined, + subgraph: subgraphNodes ? createSubgraph(subgraphNodes) : undefined + } as unknown as LGraphNode +} + +describe('graphHasMissingNodes', () => { + it('returns false when graph is null', () => { + expect(graphHasMissingNodes(null, {})).toBe(false) + }) + + it('returns false when graph is undefined', () => { + expect(graphHasMissingNodes(undefined, {})).toBe(false) + }) + + it('returns false when graph has no nodes', () => { + expect(graphHasMissingNodes(createGraph(), {})).toBe(false) + }) + + it('returns false when every node has a definition', () => { + const graph = createGraph([createNode('FooNode'), createNode('BarNode')]) + const nodeDefs: NodeDefs = { + FooNode: mockNodeDef, + BarNode: mockNodeDef + } + + expect(graphHasMissingNodes(graph, nodeDefs)).toBe(false) + }) + + it('returns true when at least one node is missing', () => { + const graph = createGraph([ + createNode('FooNode'), + createNode('MissingNode') + ]) + const nodeDefs: NodeDefs = { + FooNode: mockNodeDef + } + + expect(graphHasMissingNodes(graph, nodeDefs)).toBe(true) + }) + + it('checks nodes nested in subgraphs', () => { + const graph = createGraph([ + createNode('ContainerNode', [createNode('InnerMissing')]) + ]) + const nodeDefs: NodeDefs = { + ContainerNode: mockNodeDef + } + + const missingNodes = collectMissingNodes(graph, nodeDefs) + expect(missingNodes).toHaveLength(1) + expect(missingNodes[0]?.type).toBe('InnerMissing') + }) + + it('ignores nodes without a type', () => { + const graph = createGraph([ + createNode(undefined), + createNode(null as unknown as string) + ]) + + expect(graphHasMissingNodes(graph, {})).toBe(false) + }) + + it('traverses deeply nested subgraphs', () => { + const deepGraph = createGraph([ + createNode('Layer1', [ + createNode('Layer2', [ + createNode('Layer3', [createNode('MissingDeep')]) + ]) + ]) + ]) + const nodeDefs: NodeDefs = { + Layer1: mockNodeDef, + Layer2: mockNodeDef, + Layer3: mockNodeDef + } + + const missingNodes = collectMissingNodes(deepGraph, nodeDefs) + expect(missingNodes).toHaveLength(1) + expect(missingNodes[0]?.type).toBe('MissingDeep') + }) +})