mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-11 00:10:40 +00:00
refactor: remove legacy missing nodes dialog (#10102)
## Summary - Remove the legacy missing nodes modal dialog and migrate all functionality to the existing Error Overlay / TabErrors system - Migrate core node version warning from `MissingCoreNodesMessage.vue` to `MissingNodeCard` in the errors tab - Remove `Comfy.Workflow.ShowMissingNodesWarning` setting (errors tab always surfaces missing nodes) - Delete 6 legacy files: `useMissingNodesDialog.ts`, `MissingNodesContent.vue`, `MissingNodesFooter.vue`, `MissingNodesHeader.vue`, `MissingCoreNodesMessage.vue` and its test - Rename `showMissingNodesDialog`/`showMissingModelsDialog` params to `showMissingNodes`/`showMissingModels` - Add `errorOverlay` and `missingNodeCard` to centralized `TestIds` - Migrate all E2E tests from legacy dialog selectors to error overlay testIds - Add new E2E test: MissingNodeCard visible via "See Errors" button flow - Add new E2E test: subgraph missing node type verified by expanding pack row - Add `surfaceMissingNodes` unit tests to `executionErrorStore` - Guard `semver.compare` against invalid version strings - Add `role="alert"`, `aria-hidden` for accessibility - Use reactive props destructuring in `MissingNodeCard` and `MissingPackGroupRow` **Net change: -669 lines** (19 files, +323 / -992) <img width="733" height="579" alt="image" src="https://github.com/user-attachments/assets/c497809d-b176-43bf-9872-34bd74c6ea0d" /> ## Test plan - [x] Unit tests: MissingNodeCard core node warning (7 tests) - [x] Unit tests: surfaceMissingNodes (4 tests) - [x] Unit tests: workflowService showPendingWarnings (updated) - [x] E2E: Error overlay visible on missing nodes workflow - [x] E2E: Error overlay visible on subgraph missing nodes - [x] E2E: MissingNodeCard visible via See Errors button - [x] E2E: Subgraph node type visible after expanding pack row - [x] E2E: Error overlay does not resurface on undo/redo - [x] E2E: Error overlay does not reappear on workflow tab switch - [x] Typecheck, lint, knip all passing ## Related issues - Closes #9923 (partially — `errorOverlay` and `missingNodeCard` added to TestIds) - References #10027 (mock hoisting inconsistency) - References #10033 (i18n-based test selectors) - References #10085 (DDD layer violation + focusedErrorNodeId) ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-10102-refactor-remove-legacy-missing-nodes-dialog-3256d73d365081c194d2e90bc6401846) by [Unito](https://www.unito.io)
This commit is contained in:
@@ -1,199 +0,0 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import PrimeVue from 'primevue/config'
|
||||
import Message from 'primevue/message'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { nextTick } from 'vue'
|
||||
|
||||
import MissingCoreNodesMessage from '@/components/dialog/content/MissingCoreNodesMessage.vue'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { useSystemStatsStore } from '@/stores/systemStatsStore'
|
||||
|
||||
// Mock the stores
|
||||
vi.mock('@/stores/systemStatsStore', () => ({
|
||||
useSystemStatsStore: vi.fn()
|
||||
}))
|
||||
|
||||
const createMockNode = (type: string, version?: string): LGraphNode =>
|
||||
// @ts-expect-error - Creating a partial mock of LGraphNode for testing purposes.
|
||||
// We only need specific properties for our tests, not the full LGraphNode interface.
|
||||
({
|
||||
type,
|
||||
properties: { cnr_id: 'comfy-core', ver: version },
|
||||
id: 1,
|
||||
title: type,
|
||||
pos: [0, 0],
|
||||
size: [100, 100],
|
||||
flags: {},
|
||||
graph: null,
|
||||
mode: 0,
|
||||
inputs: [],
|
||||
outputs: []
|
||||
})
|
||||
|
||||
describe('MissingCoreNodesMessage', () => {
|
||||
const mockSystemStatsStore = {
|
||||
systemStats: null as { system?: { comfyui_version?: string } } | null,
|
||||
refetchSystemStats: vi.fn()
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
// Reset the mock store state
|
||||
mockSystemStatsStore.systemStats = null
|
||||
mockSystemStatsStore.refetchSystemStats = vi.fn()
|
||||
// @ts-expect-error - Mocking the return value of useSystemStatsStore for testing.
|
||||
// The actual store has more properties, but we only need these for our tests.
|
||||
useSystemStatsStore.mockReturnValue(mockSystemStatsStore)
|
||||
})
|
||||
|
||||
const mountComponent = (props = {}) => {
|
||||
return mount(MissingCoreNodesMessage, {
|
||||
global: {
|
||||
plugins: [PrimeVue],
|
||||
components: { Message },
|
||||
mocks: {
|
||||
$t: (key: string, params?: { version?: string }) => {
|
||||
const translations: Record<string, string> = {
|
||||
'loadWorkflowWarning.outdatedVersion': `Some nodes require a newer version of ComfyUI (current: ${params?.version}). Please update to use all nodes.`,
|
||||
'loadWorkflowWarning.outdatedVersionGeneric':
|
||||
'Some nodes require a newer version of ComfyUI. Please update to use all nodes.',
|
||||
'loadWorkflowWarning.coreNodesFromVersion': `Requires ComfyUI ${params?.version}:`
|
||||
}
|
||||
return translations[key] || key
|
||||
}
|
||||
}
|
||||
},
|
||||
props: {
|
||||
missingCoreNodes: {},
|
||||
...props
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
it('does not render when there are no missing core nodes', () => {
|
||||
const wrapper = mountComponent()
|
||||
expect(wrapper.findComponent(Message).exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('renders message when there are missing core nodes', async () => {
|
||||
const missingCoreNodes = {
|
||||
'1.2.0': [createMockNode('TestNode', '1.2.0')]
|
||||
}
|
||||
|
||||
const wrapper = mountComponent({ missingCoreNodes })
|
||||
await nextTick()
|
||||
|
||||
expect(wrapper.findComponent(Message).exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('displays current ComfyUI version when available', async () => {
|
||||
// Set systemStats directly (store auto-fetches with useAsyncState)
|
||||
mockSystemStatsStore.systemStats = {
|
||||
system: { comfyui_version: '1.0.0' }
|
||||
}
|
||||
|
||||
const missingCoreNodes = {
|
||||
'1.2.0': [createMockNode('TestNode', '1.2.0')]
|
||||
}
|
||||
|
||||
const wrapper = mountComponent({ missingCoreNodes })
|
||||
|
||||
// Wait for component to render
|
||||
await nextTick()
|
||||
|
||||
// No need to check if fetchSystemStats was called since useAsyncState auto-fetches
|
||||
expect(wrapper.text()).toContain(
|
||||
'Some nodes require a newer version of ComfyUI (current: 1.0.0)'
|
||||
)
|
||||
})
|
||||
|
||||
it('displays generic message when version is unavailable', async () => {
|
||||
// No systemStats set - version unavailable
|
||||
mockSystemStatsStore.systemStats = null
|
||||
|
||||
const missingCoreNodes = {
|
||||
'1.2.0': [createMockNode('TestNode', '1.2.0')]
|
||||
}
|
||||
|
||||
const wrapper = mountComponent({ missingCoreNodes })
|
||||
|
||||
// Wait for the async operations to complete
|
||||
await nextTick()
|
||||
await new Promise((resolve) => setTimeout(resolve, 0))
|
||||
await nextTick()
|
||||
|
||||
expect(wrapper.text()).toContain(
|
||||
'Some nodes require a newer version of ComfyUI. Please update to use all nodes.'
|
||||
)
|
||||
})
|
||||
|
||||
it('groups nodes by version and displays them', async () => {
|
||||
const missingCoreNodes = {
|
||||
'1.2.0': [
|
||||
createMockNode('NodeA', '1.2.0'),
|
||||
createMockNode('NodeB', '1.2.0')
|
||||
],
|
||||
'1.3.0': [createMockNode('NodeC', '1.3.0')]
|
||||
}
|
||||
|
||||
const wrapper = mountComponent({ missingCoreNodes })
|
||||
await nextTick()
|
||||
|
||||
const text = wrapper.text()
|
||||
expect(text).toContain('Requires ComfyUI 1.3.0:')
|
||||
expect(text).toContain('NodeC')
|
||||
expect(text).toContain('Requires ComfyUI 1.2.0:')
|
||||
expect(text).toContain('NodeA, NodeB')
|
||||
})
|
||||
|
||||
it('sorts versions in descending order', async () => {
|
||||
const missingCoreNodes = {
|
||||
'1.1.0': [createMockNode('Node1', '1.1.0')],
|
||||
'1.3.0': [createMockNode('Node3', '1.3.0')],
|
||||
'1.2.0': [createMockNode('Node2', '1.2.0')]
|
||||
}
|
||||
|
||||
const wrapper = mountComponent({ missingCoreNodes })
|
||||
await nextTick()
|
||||
|
||||
const text = wrapper.text()
|
||||
const version13Index = text.indexOf('1.3.0')
|
||||
const version12Index = text.indexOf('1.2.0')
|
||||
const version11Index = text.indexOf('1.1.0')
|
||||
|
||||
expect(version13Index).toBeLessThan(version12Index)
|
||||
expect(version12Index).toBeLessThan(version11Index)
|
||||
})
|
||||
|
||||
it('removes duplicate node names within the same version', async () => {
|
||||
const missingCoreNodes = {
|
||||
'1.2.0': [
|
||||
createMockNode('DuplicateNode', '1.2.0'),
|
||||
createMockNode('DuplicateNode', '1.2.0'),
|
||||
createMockNode('UniqueNode', '1.2.0')
|
||||
]
|
||||
}
|
||||
|
||||
const wrapper = mountComponent({ missingCoreNodes })
|
||||
await nextTick()
|
||||
|
||||
const text = wrapper.text()
|
||||
// Should only appear once in the sorted list
|
||||
expect(text).toContain('DuplicateNode, UniqueNode')
|
||||
// Count occurrences of 'DuplicateNode' - should be only 1
|
||||
const matches = text.match(/DuplicateNode/g) || []
|
||||
expect(matches.length).toBe(1)
|
||||
})
|
||||
|
||||
it('handles nodes with missing version info', async () => {
|
||||
const missingCoreNodes = {
|
||||
'': [createMockNode('NoVersionNode')]
|
||||
}
|
||||
|
||||
const wrapper = mountComponent({ missingCoreNodes })
|
||||
await nextTick()
|
||||
|
||||
expect(wrapper.text()).toContain('Requires ComfyUI unknown:')
|
||||
expect(wrapper.text()).toContain('NoVersionNode')
|
||||
})
|
||||
})
|
||||
@@ -1,83 +0,0 @@
|
||||
<template>
|
||||
<Message
|
||||
v-if="hasMissingCoreNodes"
|
||||
severity="info"
|
||||
icon="pi pi-info-circle"
|
||||
class="m-2"
|
||||
:pt="{
|
||||
root: { class: 'flex-col' },
|
||||
text: { class: 'flex-1' }
|
||||
}"
|
||||
>
|
||||
<div class="flex flex-col gap-2">
|
||||
<div>
|
||||
{{
|
||||
currentComfyUIVersion
|
||||
? $t('loadWorkflowWarning.outdatedVersion', {
|
||||
version: currentComfyUIVersion
|
||||
})
|
||||
: $t('loadWorkflowWarning.outdatedVersionGeneric')
|
||||
}}
|
||||
</div>
|
||||
<div
|
||||
v-for="[version, nodes] in sortedMissingCoreNodes"
|
||||
:key="version"
|
||||
class="ml-4"
|
||||
>
|
||||
<div class="text-sm font-medium text-surface-600">
|
||||
{{
|
||||
$t('loadWorkflowWarning.coreNodesFromVersion', {
|
||||
version: version || 'unknown'
|
||||
})
|
||||
}}
|
||||
</div>
|
||||
<div class="ml-4 text-sm text-surface-500">
|
||||
{{ getUniqueNodeNames(nodes).join(', ') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Message>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Message from 'primevue/message'
|
||||
import { compare } from 'semver'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { useSystemStatsStore } from '@/stores/systemStatsStore'
|
||||
|
||||
const { missingCoreNodes } = defineProps<{
|
||||
missingCoreNodes: Record<string, LGraphNode[]>
|
||||
}>()
|
||||
|
||||
const systemStatsStore = useSystemStatsStore()
|
||||
|
||||
const hasMissingCoreNodes = computed(() => {
|
||||
return Object.keys(missingCoreNodes).length > 0
|
||||
})
|
||||
|
||||
// Use computed for reactive version tracking
|
||||
const currentComfyUIVersion = computed<string | null>(() => {
|
||||
if (!hasMissingCoreNodes.value) return null
|
||||
return systemStatsStore.systemStats?.system?.comfyui_version ?? null
|
||||
})
|
||||
|
||||
const sortedMissingCoreNodes = computed(() => {
|
||||
return Object.entries(missingCoreNodes).sort(([a], [b]) => {
|
||||
// Sort by version in descending order (newest first)
|
||||
return compare(b, a) // Reversed for descending order
|
||||
})
|
||||
})
|
||||
|
||||
const getUniqueNodeNames = (nodes: LGraphNode[]): string[] => {
|
||||
return nodes
|
||||
.reduce<string[]>((acc, node) => {
|
||||
if (node.type && !acc.includes(node.type)) {
|
||||
acc.push(node.type)
|
||||
}
|
||||
return acc
|
||||
}, [])
|
||||
.sort()
|
||||
}
|
||||
</script>
|
||||
@@ -1,360 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
data-testid="missing-nodes-warning"
|
||||
class="comfy-missing-nodes flex w-[490px] flex-col border-t border-border-default"
|
||||
:class="isCloud ? 'border-b' : ''"
|
||||
>
|
||||
<div class="flex size-full flex-col gap-4 p-4">
|
||||
<!-- Description -->
|
||||
<div>
|
||||
<p class="m-0 text-sm/5 text-muted-foreground">
|
||||
{{
|
||||
isCloud
|
||||
? $t('missingNodes.cloud.description')
|
||||
: $t('missingNodes.oss.description')
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<MissingCoreNodesMessage v-if="!isCloud" :missing-core-nodes />
|
||||
|
||||
<!-- QUICK FIX AVAILABLE Section -->
|
||||
<div v-if="replaceableNodes.length > 0" class="flex flex-col gap-2">
|
||||
<!-- Section header with Replace button -->
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-xs font-semibold text-primary uppercase">
|
||||
{{ $t('nodeReplacement.quickFixAvailable') }}
|
||||
</span>
|
||||
<div class="size-2 rounded-full bg-primary" />
|
||||
</div>
|
||||
<Button
|
||||
v-tooltip.top="$t('nodeReplacement.replaceWarning')"
|
||||
variant="primary"
|
||||
size="md"
|
||||
:disabled="selectedTypes.size === 0"
|
||||
@click="handleReplaceSelected"
|
||||
>
|
||||
<i class="mr-1.5 icon-[lucide--refresh-cw] size-4" />
|
||||
{{
|
||||
$t('nodeReplacement.replaceSelected', {
|
||||
count: selectedTypes.size
|
||||
})
|
||||
}}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- Replaceable nodes list -->
|
||||
<div
|
||||
class="flex scrollbar-custom max-h-[200px] flex-col overflow-y-auto rounded-lg bg-secondary-background"
|
||||
>
|
||||
<!-- Select All row (sticky header) -->
|
||||
<div
|
||||
:class="
|
||||
cn(
|
||||
'sticky top-0 z-10 flex items-center gap-3 border-b border-border-default bg-secondary-background px-3 py-2',
|
||||
pendingNodes.length > 0
|
||||
? 'cursor-pointer hover:bg-secondary-background-hover'
|
||||
: 'pointer-events-none opacity-50'
|
||||
)
|
||||
"
|
||||
tabindex="0"
|
||||
role="checkbox"
|
||||
:aria-checked="
|
||||
isAllSelected ? 'true' : isSomeSelected ? 'mixed' : 'false'
|
||||
"
|
||||
@click="toggleSelectAll"
|
||||
@keydown.enter.prevent="toggleSelectAll"
|
||||
@keydown.space.prevent="toggleSelectAll"
|
||||
>
|
||||
<div
|
||||
class="flex size-4 shrink-0 items-center justify-center rounded-sm p-0.5 transition-all duration-200"
|
||||
:class="
|
||||
isAllSelected || isSomeSelected
|
||||
? 'bg-primary-background'
|
||||
: 'bg-secondary-background'
|
||||
"
|
||||
>
|
||||
<i
|
||||
v-if="isAllSelected"
|
||||
class="text-bold icon-[lucide--check] text-xs text-base-foreground"
|
||||
/>
|
||||
<i
|
||||
v-else-if="isSomeSelected"
|
||||
class="text-bold icon-[lucide--minus] text-xs text-base-foreground"
|
||||
/>
|
||||
</div>
|
||||
<span class="text-xs font-medium text-muted-foreground uppercase">
|
||||
{{ $t('nodeReplacement.compatibleAlternatives') }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Replaceable node items -->
|
||||
<div
|
||||
v-for="node in replaceableNodes"
|
||||
:key="node.label"
|
||||
:class="
|
||||
cn(
|
||||
'flex items-center gap-3 px-3 py-2',
|
||||
replacedTypes.has(node.label)
|
||||
? 'pointer-events-none opacity-50'
|
||||
: 'cursor-pointer hover:bg-secondary-background-hover'
|
||||
)
|
||||
"
|
||||
tabindex="0"
|
||||
role="checkbox"
|
||||
:aria-checked="
|
||||
replacedTypes.has(node.label) || selectedTypes.has(node.label)
|
||||
? 'true'
|
||||
: 'false'
|
||||
"
|
||||
@click="toggleNode(node.label)"
|
||||
@keydown.enter.prevent="toggleNode(node.label)"
|
||||
@keydown.space.prevent="toggleNode(node.label)"
|
||||
>
|
||||
<div
|
||||
class="flex size-4 shrink-0 items-center justify-center rounded-sm p-0.5 transition-all duration-200"
|
||||
:class="
|
||||
replacedTypes.has(node.label) || selectedTypes.has(node.label)
|
||||
? 'bg-primary-background'
|
||||
: 'bg-secondary-background'
|
||||
"
|
||||
>
|
||||
<i
|
||||
v-if="
|
||||
replacedTypes.has(node.label) || selectedTypes.has(node.label)
|
||||
"
|
||||
class="text-bold icon-[lucide--check] text-xs text-base-foreground"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col gap-0.5">
|
||||
<div class="flex items-center gap-2">
|
||||
<span
|
||||
v-if="replacedTypes.has(node.label)"
|
||||
class="border-success bg-success/10 text-success inline-flex h-4 items-center rounded-full border px-1.5 text-xxxs font-semibold uppercase"
|
||||
>
|
||||
{{ $t('nodeReplacement.replaced') }}
|
||||
</span>
|
||||
<span
|
||||
v-else
|
||||
class="inline-flex h-4 items-center rounded-full border border-primary bg-primary/10 px-1.5 text-xxxs font-semibold text-primary uppercase"
|
||||
>
|
||||
{{ $t('nodeReplacement.replaceable') }}
|
||||
</span>
|
||||
<span class="text-foreground text-sm">
|
||||
{{ node.label }}
|
||||
</span>
|
||||
</div>
|
||||
<span class="text-xs text-muted-foreground">
|
||||
{{ node.replacement?.new_node_id ?? node.hint ?? '' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- MANUAL INSTALLATION REQUIRED Section -->
|
||||
<div
|
||||
v-if="nonReplaceableNodes.length > 0"
|
||||
class="flex max-h-[200px] flex-col gap-2"
|
||||
>
|
||||
<!-- Section header -->
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-xs font-semibold text-error uppercase">
|
||||
{{ $t('nodeReplacement.installationRequired') }}
|
||||
</span>
|
||||
<i class="icon-[lucide--info] text-xs text-error" />
|
||||
</div>
|
||||
|
||||
<!-- Non-replaceable nodes list -->
|
||||
<div
|
||||
class="flex scrollbar-custom flex-col overflow-y-auto rounded-lg bg-secondary-background"
|
||||
>
|
||||
<div
|
||||
v-for="node in nonReplaceableNodes"
|
||||
:key="node.label"
|
||||
class="flex items-center justify-between px-4 py-3"
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex flex-col gap-0.5">
|
||||
<div class="flex items-center gap-2">
|
||||
<span
|
||||
class="inline-flex h-4 items-center rounded-full border border-error bg-error/10 px-1.5 text-xxxs font-semibold text-error uppercase"
|
||||
>
|
||||
{{ $t('nodeReplacement.notReplaceable') }}
|
||||
</span>
|
||||
<span class="text-foreground text-sm">
|
||||
{{ node.label }}
|
||||
</span>
|
||||
</div>
|
||||
<span v-if="node.hint" class="text-xs text-muted-foreground">
|
||||
{{ node.hint }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
v-if="node.action"
|
||||
variant="destructive-textonly"
|
||||
size="sm"
|
||||
@click="node.action.callback"
|
||||
>
|
||||
{{ node.action.text }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bottom instruction box -->
|
||||
<div
|
||||
class="flex gap-3 rounded-lg border border-warning-background bg-warning-background/10 p-3"
|
||||
>
|
||||
<i
|
||||
class="mt-0.5 icon-[lucide--triangle-alert] size-4 shrink-0 text-warning-background"
|
||||
/>
|
||||
<p class="text-neutral-foreground m-0 text-xs/5">
|
||||
<i18n-t keypath="nodeReplacement.instructionMessage">
|
||||
<template #red>
|
||||
<span class="text-error">{{
|
||||
$t('nodeReplacement.redHighlight')
|
||||
}}</span>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import MissingCoreNodesMessage from '@/components/dialog/content/MissingCoreNodesMessage.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import type { NodeReplacement } from '@/platform/nodeReplacement/types'
|
||||
import { useNodeReplacement } from '@/platform/nodeReplacement/useNodeReplacement'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
import { useExecutionErrorStore } from '@/stores/executionErrorStore'
|
||||
import type { MissingNodeType } from '@/types/comfy'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
import { useMissingNodes } from '@/workbench/extensions/manager/composables/nodePack/useMissingNodes'
|
||||
|
||||
const { missingNodeTypes } = defineProps<{
|
||||
missingNodeTypes: MissingNodeType[]
|
||||
}>()
|
||||
|
||||
const { missingCoreNodes } = useMissingNodes()
|
||||
const { replaceNodesInPlace } = useNodeReplacement()
|
||||
const dialogStore = useDialogStore()
|
||||
const executionErrorStore = useExecutionErrorStore()
|
||||
|
||||
interface ProcessedNode {
|
||||
label: string
|
||||
hint?: string
|
||||
action?: { text: string; callback: () => void }
|
||||
isReplaceable: boolean
|
||||
replacement?: NodeReplacement
|
||||
}
|
||||
|
||||
const replacedTypes = ref<Set<string>>(new Set())
|
||||
|
||||
const uniqueNodes = computed<ProcessedNode[]>(() => {
|
||||
const seenTypes = new Set<string>()
|
||||
return missingNodeTypes
|
||||
.filter((node) => {
|
||||
const type = typeof node === 'object' ? node.type : node
|
||||
if (seenTypes.has(type)) return false
|
||||
seenTypes.add(type)
|
||||
return true
|
||||
})
|
||||
.map((node) => {
|
||||
if (typeof node === 'object') {
|
||||
return {
|
||||
label: node.type,
|
||||
hint: node.hint,
|
||||
action: node.action,
|
||||
isReplaceable: node.isReplaceable ?? false,
|
||||
replacement: node.replacement
|
||||
}
|
||||
}
|
||||
return { label: node, isReplaceable: false }
|
||||
})
|
||||
})
|
||||
|
||||
const replaceableNodes = computed(() =>
|
||||
uniqueNodes.value.filter((n) => n.isReplaceable)
|
||||
)
|
||||
|
||||
const pendingNodes = computed(() =>
|
||||
replaceableNodes.value.filter((n) => !replacedTypes.value.has(n.label))
|
||||
)
|
||||
|
||||
const nonReplaceableNodes = computed(() =>
|
||||
uniqueNodes.value.filter((n) => !n.isReplaceable)
|
||||
)
|
||||
|
||||
// Selection state - all pending nodes selected by default
|
||||
const selectedTypes = ref(new Set(pendingNodes.value.map((n) => n.label)))
|
||||
|
||||
const isAllSelected = computed(
|
||||
() =>
|
||||
pendingNodes.value.length > 0 &&
|
||||
pendingNodes.value.every((n) => selectedTypes.value.has(n.label))
|
||||
)
|
||||
|
||||
const isSomeSelected = computed(
|
||||
() => selectedTypes.value.size > 0 && !isAllSelected.value
|
||||
)
|
||||
|
||||
function toggleNode(label: string) {
|
||||
if (replacedTypes.value.has(label)) return
|
||||
const next = new Set(selectedTypes.value)
|
||||
if (next.has(label)) {
|
||||
next.delete(label)
|
||||
} else {
|
||||
next.add(label)
|
||||
}
|
||||
selectedTypes.value = next
|
||||
}
|
||||
|
||||
function toggleSelectAll() {
|
||||
if (isAllSelected.value) {
|
||||
selectedTypes.value = new Set()
|
||||
} else {
|
||||
selectedTypes.value = new Set(pendingNodes.value.map((n) => n.label))
|
||||
}
|
||||
}
|
||||
|
||||
function handleReplaceSelected() {
|
||||
const selected = missingNodeTypes.filter((node) => {
|
||||
const type = typeof node === 'object' ? node.type : node
|
||||
return selectedTypes.value.has(type)
|
||||
})
|
||||
|
||||
const result = replaceNodesInPlace(selected)
|
||||
const nextReplaced = new Set(replacedTypes.value)
|
||||
const nextSelected = new Set(selectedTypes.value)
|
||||
for (const type of result) {
|
||||
nextReplaced.add(type)
|
||||
nextSelected.delete(type)
|
||||
}
|
||||
replacedTypes.value = nextReplaced
|
||||
selectedTypes.value = nextSelected
|
||||
|
||||
// replaceNodesInPlace() handles canvas rendering via onNodeAdded(),
|
||||
// but the modal only updates its own local UI state above.
|
||||
// Without this call the Errors Tab would still list the replaced nodes
|
||||
// as missing because executionErrorStore is not aware of the replacement.
|
||||
if (result.length > 0) {
|
||||
executionErrorStore.removeMissingNodesByType(result)
|
||||
}
|
||||
|
||||
// Auto-close when all replaceable nodes replaced and no non-replaceable remain
|
||||
const allReplaced = replaceableNodes.value.every((n) =>
|
||||
nextReplaced.has(n.label)
|
||||
)
|
||||
if (allReplaced && nonReplaceableNodes.value.length === 0) {
|
||||
dialogStore.closeDialog({ key: 'global-missing-nodes' })
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,199 +0,0 @@
|
||||
<template>
|
||||
<div class="flex w-full flex-col gap-2 px-4 py-2">
|
||||
<div class="flex flex-col gap-1 text-sm text-muted-foreground">
|
||||
<div class="flex items-center gap-1">
|
||||
<input
|
||||
id="doNotAskAgainNodes"
|
||||
v-model="doNotAskAgain"
|
||||
type="checkbox"
|
||||
class="size-4 cursor-pointer"
|
||||
/>
|
||||
<label for="doNotAskAgainNodes">{{
|
||||
$t('missingModelsDialog.doNotAskAgain')
|
||||
}}</label>
|
||||
</div>
|
||||
<i18n-t
|
||||
v-if="doNotAskAgain"
|
||||
keypath="missingModelsDialog.reEnableInSettings"
|
||||
tag="span"
|
||||
class="ml-6 text-sm text-muted-foreground"
|
||||
>
|
||||
<template #link>
|
||||
<Button
|
||||
variant="textonly"
|
||||
class="cursor-pointer p-0 text-sm text-muted-foreground underline hover:bg-transparent"
|
||||
@click="openShowMissingNodesSetting"
|
||||
>
|
||||
{{ $t('missingModelsDialog.reEnableInSettingsLink') }}
|
||||
</Button>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</div>
|
||||
|
||||
<!-- All nodes replaceable: Skip button (cloud + OSS) -->
|
||||
<div v-if="!hasNonReplaceableNodes" class="flex justify-end gap-1">
|
||||
<Button variant="secondary" size="md" @click="handleGotItClick">
|
||||
{{ $t('nodeReplacement.skipForNow') }}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- Cloud mode: Learn More + Got It buttons -->
|
||||
<div
|
||||
v-else-if="isCloud"
|
||||
class="flex w-full items-center justify-between gap-2"
|
||||
>
|
||||
<Button
|
||||
variant="textonly"
|
||||
size="sm"
|
||||
as="a"
|
||||
href="https://www.comfy.org/cloud"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<i class="icon-[lucide--info]"></i>
|
||||
<span>{{ $t('missingNodes.cloud.learnMore') }}</span>
|
||||
</Button>
|
||||
<Button variant="secondary" size="md" @click="handleGotItClick">{{
|
||||
$t('missingNodes.cloud.gotIt')
|
||||
}}</Button>
|
||||
</div>
|
||||
|
||||
<!-- OSS mode: Manager buttons -->
|
||||
<div v-else-if="showManagerButtons" class="flex justify-end gap-1">
|
||||
<Button variant="textonly" @click="handleOpenManager">{{
|
||||
$t('g.openManager')
|
||||
}}</Button>
|
||||
<PackInstallButton
|
||||
v-if="showInstallAllButton"
|
||||
type="secondary"
|
||||
size="md"
|
||||
:disabled="
|
||||
isLoading || !!error || missingNodePacks.length === 0 || isInstalling
|
||||
"
|
||||
:is-loading="isLoading"
|
||||
:node-packs="missingNodePacks"
|
||||
:label="
|
||||
isLoading
|
||||
? $t('manager.gettingInfo')
|
||||
: $t('manager.installAllMissingNodes')
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useToastStore } from '@/platform/updates/common/toastStore'
|
||||
import { useSettingsDialog } from '@/platform/settings/composables/useSettingsDialog'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
import type { MissingNodeType } from '@/types/comfy'
|
||||
import PackInstallButton from '@/workbench/extensions/manager/components/manager/button/PackInstallButton.vue'
|
||||
import { useMissingNodes } from '@/workbench/extensions/manager/composables/nodePack/useMissingNodes'
|
||||
import { useManagerState } from '@/workbench/extensions/manager/composables/useManagerState'
|
||||
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
|
||||
import { ManagerTab } from '@/workbench/extensions/manager/types/comfyManagerTypes'
|
||||
|
||||
const { missingNodeTypes } = defineProps<{
|
||||
missingNodeTypes?: MissingNodeType[]
|
||||
}>()
|
||||
|
||||
const dialogStore = useDialogStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
const doNotAskAgain = ref(false)
|
||||
|
||||
watch(doNotAskAgain, (value) => {
|
||||
void useSettingStore().set('Comfy.Workflow.ShowMissingNodesWarning', !value)
|
||||
})
|
||||
|
||||
const handleGotItClick = () => {
|
||||
dialogStore.closeDialog({ key: 'global-missing-nodes' })
|
||||
}
|
||||
|
||||
function openShowMissingNodesSetting() {
|
||||
dialogStore.closeDialog({ key: 'global-missing-nodes' })
|
||||
useSettingsDialog().show(undefined, 'Comfy.Workflow.ShowMissingNodesWarning')
|
||||
}
|
||||
|
||||
const { missingNodePacks, isLoading, error } = useMissingNodes()
|
||||
const comfyManagerStore = useComfyManagerStore()
|
||||
const managerState = useManagerState()
|
||||
function handleOpenManager() {
|
||||
managerState.openManager({
|
||||
initialTab: ManagerTab.Missing,
|
||||
showToastOnLegacyError: true
|
||||
})
|
||||
}
|
||||
|
||||
// Check if any of the missing packs are currently being installed
|
||||
const isInstalling = computed(() => {
|
||||
if (!missingNodePacks.value?.length) return false
|
||||
return missingNodePacks.value.some((pack) =>
|
||||
comfyManagerStore.isPackInstalling(pack.id)
|
||||
)
|
||||
})
|
||||
|
||||
// Show manager buttons unless manager is disabled
|
||||
const showManagerButtons = computed(() => {
|
||||
return managerState.shouldShowManagerButtons.value
|
||||
})
|
||||
|
||||
// Only show Install All button for NEW_UI (new manager with v4 support)
|
||||
const showInstallAllButton = computed(() => {
|
||||
return managerState.shouldShowInstallButton.value
|
||||
})
|
||||
|
||||
const hasNonReplaceableNodes = computed(
|
||||
() =>
|
||||
missingNodeTypes?.some(
|
||||
(n) =>
|
||||
typeof n === 'string' || (typeof n === 'object' && !n.isReplaceable)
|
||||
) ?? false
|
||||
)
|
||||
|
||||
// Track whether missingNodePacks was ever non-empty (i.e. there were packs to install)
|
||||
const hadMissingPacks = ref(false)
|
||||
|
||||
watch(
|
||||
missingNodePacks,
|
||||
(packs) => {
|
||||
if (packs && packs.length > 0) hadMissingPacks.value = true
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
// Only consider "all installed" when packs transitioned from non-empty to empty
|
||||
// (actual installation happened). Replaceable-only case is handled by Content auto-close.
|
||||
const allMissingNodesInstalled = computed(() => {
|
||||
if (!hadMissingPacks.value) return false
|
||||
return (
|
||||
!isLoading.value &&
|
||||
!isInstalling.value &&
|
||||
missingNodePacks.value?.length === 0
|
||||
)
|
||||
})
|
||||
|
||||
// Watch for completion and close dialog (OSS mode only)
|
||||
watch(allMissingNodesInstalled, async (allInstalled) => {
|
||||
if (!isCloud && allInstalled && showInstallAllButton.value) {
|
||||
// Use nextTick to ensure state updates are complete
|
||||
await nextTick()
|
||||
|
||||
dialogStore.closeDialog({ key: 'global-missing-nodes' })
|
||||
|
||||
// Show success toast
|
||||
useToastStore().add({
|
||||
severity: 'success',
|
||||
summary: t('g.success'),
|
||||
detail: t('manager.allMissingNodesInstalled'),
|
||||
life: 3000
|
||||
})
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -1,18 +0,0 @@
|
||||
<template>
|
||||
<div class="flex w-full items-center justify-between p-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<i class="icon-[lucide--triangle-alert] text-warning-background"></i>
|
||||
<p class="m-0 text-sm">
|
||||
{{
|
||||
isCloud
|
||||
? $t('missingNodes.cloud.title')
|
||||
: $t('missingNodes.oss.title')
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
</script>
|
||||
@@ -7,13 +7,41 @@ import { createI18n } from 'vue-i18n'
|
||||
|
||||
import type { MissingPackGroup } from '@/components/rightSidePanel/errors/useErrorGroups'
|
||||
|
||||
const mockIsCloud = { value: false }
|
||||
const mockIsCloud = vi.hoisted(() => ({ value: false }))
|
||||
vi.mock('@/platform/distribution/types', () => ({
|
||||
get isCloud() {
|
||||
return mockIsCloud.value
|
||||
}
|
||||
}))
|
||||
|
||||
const mockMissingCoreNodes = vi.hoisted(() => ({
|
||||
value: {} as Record<string, { type: string }[]>
|
||||
}))
|
||||
const mockSystemStats = vi.hoisted(() => ({
|
||||
value: null as { system?: { comfyui_version?: string } } | null
|
||||
}))
|
||||
|
||||
vi.mock(
|
||||
'@/workbench/extensions/manager/composables/nodePack/useMissingNodes',
|
||||
() => ({
|
||||
useMissingNodes: () => ({
|
||||
missingCoreNodes: mockMissingCoreNodes,
|
||||
missingNodePacks: { value: [] },
|
||||
isLoading: { value: false },
|
||||
error: { value: null },
|
||||
hasMissingNodes: { value: false }
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
vi.mock('@/stores/systemStatsStore', () => ({
|
||||
useSystemStatsStore: () => ({
|
||||
get systemStats() {
|
||||
return mockSystemStats.value
|
||||
}
|
||||
})
|
||||
}))
|
||||
|
||||
const mockApplyChanges = vi.fn()
|
||||
const mockIsRestarting = ref(false)
|
||||
vi.mock('@/workbench/extensions/manager/composables/useApplyChanges', () => ({
|
||||
@@ -61,6 +89,13 @@ const i18n = createI18n({
|
||||
'To install missing nodes, first run {pipCmd} in your Python environment to install Node Manager, then restart ComfyUI with the {flag} flag.',
|
||||
applyChanges: 'Apply Changes'
|
||||
}
|
||||
},
|
||||
loadWorkflowWarning: {
|
||||
outdatedVersion:
|
||||
'Some nodes require a newer version of ComfyUI (current: {version}).',
|
||||
outdatedVersionGeneric:
|
||||
'Some nodes require a newer version of ComfyUI.',
|
||||
coreNodesFromVersion: 'Requires ComfyUI {version}:'
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -109,6 +144,8 @@ describe('MissingNodeCard', () => {
|
||||
mockIsCloud.value = false
|
||||
mockShouldShowManagerButtons.value = false
|
||||
mockIsRestarting.value = false
|
||||
mockMissingCoreNodes.value = {}
|
||||
mockSystemStats.value = null
|
||||
})
|
||||
|
||||
describe('Rendering & Props', () => {
|
||||
@@ -234,4 +271,81 @@ describe('MissingNodeCard', () => {
|
||||
expect(wrapper.emitted('openManagerInfo')?.[0]).toEqual(['pack-0'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('Core Node Version Warning', () => {
|
||||
it('does not render warning when no missing core nodes', () => {
|
||||
const wrapper = mountCard()
|
||||
expect(wrapper.text()).not.toContain('newer version of ComfyUI')
|
||||
})
|
||||
|
||||
it('renders warning with version when missing core nodes exist', () => {
|
||||
mockMissingCoreNodes.value = {
|
||||
'1.2.0': [{ type: 'TestNode' }]
|
||||
}
|
||||
mockSystemStats.value = { system: { comfyui_version: '1.0.0' } }
|
||||
const wrapper = mountCard()
|
||||
expect(wrapper.text()).toContain('(current: 1.0.0)')
|
||||
expect(wrapper.text()).toContain('Requires ComfyUI 1.2.0:')
|
||||
expect(wrapper.text()).toContain('TestNode')
|
||||
})
|
||||
|
||||
it('renders generic message when version is unavailable', () => {
|
||||
mockMissingCoreNodes.value = {
|
||||
'1.2.0': [{ type: 'TestNode' }]
|
||||
}
|
||||
const wrapper = mountCard()
|
||||
expect(wrapper.text()).toContain(
|
||||
'Some nodes require a newer version of ComfyUI.'
|
||||
)
|
||||
})
|
||||
|
||||
it('does not render warning on Cloud', () => {
|
||||
mockIsCloud.value = true
|
||||
mockMissingCoreNodes.value = {
|
||||
'1.2.0': [{ type: 'TestNode' }]
|
||||
}
|
||||
const wrapper = mountCard()
|
||||
expect(wrapper.text()).not.toContain('newer version of ComfyUI')
|
||||
})
|
||||
|
||||
it('deduplicates and sorts node names within a version', () => {
|
||||
mockMissingCoreNodes.value = {
|
||||
'1.2.0': [
|
||||
{ type: 'ZebraNode' },
|
||||
{ type: 'AlphaNode' },
|
||||
{ type: 'ZebraNode' }
|
||||
]
|
||||
}
|
||||
const wrapper = mountCard()
|
||||
expect(wrapper.text()).toContain('AlphaNode, ZebraNode')
|
||||
expect(wrapper.text().match(/ZebraNode/g)?.length).toBe(1)
|
||||
})
|
||||
|
||||
it('sorts versions in descending order', () => {
|
||||
mockMissingCoreNodes.value = {
|
||||
'1.1.0': [{ type: 'Node1' }],
|
||||
'1.3.0': [{ type: 'Node3' }],
|
||||
'1.2.0': [{ type: 'Node2' }]
|
||||
}
|
||||
const wrapper = mountCard()
|
||||
const text = wrapper.text()
|
||||
const v13 = text.indexOf('1.3.0')
|
||||
const v12 = text.indexOf('1.2.0')
|
||||
const v11 = text.indexOf('1.1.0')
|
||||
expect(v13).toBeLessThan(v12)
|
||||
expect(v12).toBeLessThan(v11)
|
||||
})
|
||||
|
||||
it('handles empty string version key without crashing', () => {
|
||||
mockMissingCoreNodes.value = {
|
||||
'': [{ type: 'NoVersionNode' }],
|
||||
'1.2.0': [{ type: 'VersionedNode' }]
|
||||
}
|
||||
const wrapper = mountCard()
|
||||
expect(wrapper.text()).toContain('Requires ComfyUI 1.2.0:')
|
||||
expect(wrapper.text()).toContain('VersionedNode')
|
||||
expect(wrapper.text()).toContain('unknown')
|
||||
expect(wrapper.text()).toContain('NoVersionNode')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,5 +1,42 @@
|
||||
<template>
|
||||
<div class="px-4 pb-2">
|
||||
<div data-testid="missing-node-card" class="px-4 pb-2">
|
||||
<!-- Core node version warning (OSS only) -->
|
||||
<div
|
||||
v-if="!isCloud && hasMissingCoreNodes"
|
||||
role="alert"
|
||||
class="mb-3 flex gap-2.5 rounded-lg border border-warning-background/30 bg-warning-background/10 p-3"
|
||||
>
|
||||
<i
|
||||
aria-hidden="true"
|
||||
class="mt-0.5 icon-[lucide--triangle-alert] size-4 shrink-0 text-warning-background"
|
||||
/>
|
||||
<div class="flex flex-col gap-1.5 text-xs/relaxed text-muted-foreground">
|
||||
<p class="m-0">
|
||||
{{
|
||||
currentComfyUIVersion
|
||||
? t('loadWorkflowWarning.outdatedVersion', {
|
||||
version: currentComfyUIVersion
|
||||
})
|
||||
: t('loadWorkflowWarning.outdatedVersionGeneric')
|
||||
}}
|
||||
</p>
|
||||
<div
|
||||
v-for="[version, nodes] in sortedMissingCoreNodes"
|
||||
:key="version"
|
||||
class="ml-2"
|
||||
>
|
||||
<span class="font-medium">
|
||||
{{
|
||||
t('loadWorkflowWarning.coreNodesFromVersion', {
|
||||
version: version || t('loadWorkflowWarning.unknownVersion')
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
<span class="ml-1">{{ getUniqueNodeNames(nodes).join(', ') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sub-label: cloud or OSS message shown above all pack groups -->
|
||||
<p
|
||||
class="m-0 text-sm/relaxed text-muted-foreground"
|
||||
@@ -53,7 +90,11 @@
|
||||
@click="applyChanges()"
|
||||
>
|
||||
<DotSpinner v-if="isRestarting" duration="1s" :size="14" />
|
||||
<i v-else class="icon-[lucide--refresh-cw] size-4 shrink-0" />
|
||||
<i
|
||||
v-else
|
||||
aria-hidden="true"
|
||||
class="icon-[lucide--refresh-cw] size-4 shrink-0"
|
||||
/>
|
||||
<span class="min-w-0 truncate">{{
|
||||
t('rightSidePanel.missingNodePacks.applyChanges')
|
||||
}}</span>
|
||||
@@ -64,16 +105,20 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { compare, valid } from 'semver'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import DotSpinner from '@/components/common/DotSpinner.vue'
|
||||
import { useApplyChanges } from '@/workbench/extensions/manager/composables/useApplyChanges'
|
||||
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import { useManagerState } from '@/workbench/extensions/manager/composables/useManagerState'
|
||||
import { useMissingNodes } from '@/workbench/extensions/manager/composables/nodePack/useMissingNodes'
|
||||
import { useSystemStatsStore } from '@/stores/systemStatsStore'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type { MissingPackGroup } from '@/components/rightSidePanel/errors/useErrorGroups'
|
||||
import MissingPackGroupRow from '@/components/rightSidePanel/errors/MissingPackGroupRow.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
const { showInfoButton, showNodeIdBadge, missingPackGroups } = defineProps<{
|
||||
showInfoButton: boolean
|
||||
showNodeIdBadge: boolean
|
||||
missingPackGroups: MissingPackGroup[]
|
||||
@@ -86,6 +131,34 @@ const emit = defineEmits<{
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const { missingCoreNodes } = useMissingNodes()
|
||||
const systemStatsStore = useSystemStatsStore()
|
||||
|
||||
const hasMissingCoreNodes = computed(
|
||||
() => Object.keys(missingCoreNodes.value).length > 0
|
||||
)
|
||||
|
||||
const currentComfyUIVersion = computed<string | null>(() => {
|
||||
if (!hasMissingCoreNodes.value) return null
|
||||
return systemStatsStore.systemStats?.system?.comfyui_version ?? null
|
||||
})
|
||||
|
||||
const sortedMissingCoreNodes = computed(() =>
|
||||
Object.entries(missingCoreNodes.value).sort(([a], [b]) => {
|
||||
const aValid = valid(a)
|
||||
const bValid = valid(b)
|
||||
if (!aValid && !bValid) return 0
|
||||
if (!aValid) return 1
|
||||
if (!bValid) return -1
|
||||
return compare(b, a)
|
||||
})
|
||||
)
|
||||
|
||||
function getUniqueNodeNames(nodes: LGraphNode[]): string[] {
|
||||
const types = new Set(nodes.map((node) => node.type).filter(Boolean))
|
||||
return [...types].sort()
|
||||
}
|
||||
|
||||
const comfyManagerStore = useComfyManagerStore()
|
||||
const { isRestarting, applyChanges } = useApplyChanges()
|
||||
const { shouldShowManagerButtons } = useManagerState()
|
||||
@@ -95,7 +168,7 @@ const { shouldShowManagerButtons } = useManagerState()
|
||||
* - Not on Cloud (OSS/local only)
|
||||
* - Manager is disabled (showInfoButton is false)
|
||||
*/
|
||||
const showManagerHint = computed(() => !isCloud && !props.showInfoButton)
|
||||
const showManagerHint = computed(() => !isCloud && !showInfoButton)
|
||||
|
||||
/**
|
||||
* Show Apply Changes when any pack from the error group is already installed
|
||||
@@ -103,7 +176,7 @@ const showManagerHint = computed(() => !isCloud && !props.showInfoButton)
|
||||
* This is server-state based → persists across browser refreshes.
|
||||
*/
|
||||
const hasInstalledPacksPendingRestart = computed(() =>
|
||||
props.missingPackGroups.some(
|
||||
missingPackGroups.some(
|
||||
(g) => g.packId !== null && comfyManagerStore.isPackInstalled(g.packId)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -193,7 +193,7 @@ import { ManagerTab } from '@/workbench/extensions/manager/types/comfyManagerTyp
|
||||
import type { MissingNodeType } from '@/types/comfy'
|
||||
import type { MissingPackGroup } from '@/components/rightSidePanel/errors/useErrorGroups'
|
||||
|
||||
const props = defineProps<{
|
||||
const { group, showInfoButton, showNodeIdBadge } = defineProps<{
|
||||
group: MissingPackGroup
|
||||
showInfoButton: boolean
|
||||
showNodeIdBadge: boolean
|
||||
@@ -211,8 +211,8 @@ const comfyManagerStore = useComfyManagerStore()
|
||||
const { shouldShowManagerButtons, openManager } = useManagerState()
|
||||
|
||||
const nodePack = computed(() => {
|
||||
if (!props.group.packId) return null
|
||||
return missingNodePacks.value.find((p) => p.id === props.group.packId) ?? null
|
||||
if (!group.packId) return null
|
||||
return missingNodePacks.value.find((p) => p.id === group.packId) ?? null
|
||||
})
|
||||
|
||||
const { isInstalling, installAllPacks } = usePackInstall(() =>
|
||||
@@ -220,8 +220,8 @@ const { isInstalling, installAllPacks } = usePackInstall(() =>
|
||||
)
|
||||
|
||||
function handlePackInstallClick() {
|
||||
if (!props.group.packId) return
|
||||
if (!comfyManagerStore.isPackInstalled(props.group.packId)) {
|
||||
if (!group.packId) return
|
||||
if (!comfyManagerStore.isPackInstalled(group.packId)) {
|
||||
void installAllPacks()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user