mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-19 22:34:15 +00:00
feat: classify missing nodes by replacement availability (#8483)
## Summary - Extend `MissingNodeType` with `isReplaceable` and `replacement` fields - Classify missing nodes by checking `nodeReplacementStore.getReplacementFor()` during graph load - Wrap hardcoded node patches (T2IAdapterLoader, ConditioningAverage, etc.) in `if (!isEnabled)` guard so they only run when node replacement setting is disabled - Change `useNodeReplacementStore().load()` from fire-and-forget (`void`) to `await` so replacement data is available before missing node detection - Fix guard condition order in `nodeReplacementStore.load()`: check `isEnabled` before `isLoaded` - Align `InputMap` types with actual API response (flat `old_id`/`set_value` fields instead of nested `assign` wrapper) ## Test plan - [x] Load workflow with deprecated nodes (T2IAdapterLoader, Load3DAnimation, SDV_img2vid_Conditioning) - [x] Verify missing nodes are classified with `isReplaceable: true` and `replacement` object - [x] Verify hardcoded patches only run when node replacement setting is OFF - [x] Verify `nodeReplacementStore.load()` is not called when setting is disabled - [x] Unit tests pass (`nodeReplacementStore.test.ts` - 16 tests) - [x] Typecheck passes 🤖 Generated with [Claude Code](https://claude.ai/code)
This commit is contained in:
@@ -100,8 +100,7 @@ const config: StorybookConfig = {
|
||||
rolldownOptions: {
|
||||
treeshake: false,
|
||||
output: {
|
||||
keepNames: true,
|
||||
strictExecutionOrder: true
|
||||
keepNames: true
|
||||
},
|
||||
onwarn: (warning, warn) => {
|
||||
// Suppress specific warnings
|
||||
|
||||
@@ -247,5 +247,15 @@ describe('useNodeReplacementStore', () => {
|
||||
|
||||
expect(fetchNodeReplacements).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('should not call API when setting is disabled', async () => {
|
||||
vi.mocked(fetchNodeReplacements).mockResolvedValue(mockReplacements)
|
||||
store = createStore(false)
|
||||
|
||||
await store.load()
|
||||
|
||||
expect(fetchNodeReplacements).not.toHaveBeenCalled()
|
||||
expect(store.isLoaded).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -15,8 +15,7 @@ export const useNodeReplacementStore = defineStore('nodeReplacement', () => {
|
||||
)
|
||||
|
||||
async function load() {
|
||||
if (isLoaded.value || !isEnabled.value) return
|
||||
|
||||
if (!isEnabled.value || isLoaded.value) return
|
||||
try {
|
||||
replacements.value = await fetchNodeReplacements()
|
||||
isLoaded.value = true
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
interface InputAssignOldId {
|
||||
assign_type: 'old_id'
|
||||
interface InputMapOldId {
|
||||
new_id: string
|
||||
old_id: string
|
||||
}
|
||||
|
||||
interface InputAssignSetValue {
|
||||
assign_type: 'set_value'
|
||||
value: unknown
|
||||
interface InputMapSetValue {
|
||||
new_id: string
|
||||
set_value: unknown
|
||||
}
|
||||
|
||||
interface InputMap {
|
||||
new_id: string
|
||||
assign: InputAssignOldId | InputAssignSetValue
|
||||
}
|
||||
type InputMap = InputMapOldId | InputMapSetValue
|
||||
|
||||
interface OutputMap {
|
||||
new_idx: number
|
||||
|
||||
@@ -795,7 +795,7 @@ export class ComfyApp {
|
||||
await useWorkspaceStore().workflow.syncWorkflows()
|
||||
//Doesn't need to block. Blueprints will load async
|
||||
void useSubgraphStore().fetchSubgraphs()
|
||||
void useNodeReplacementStore().load()
|
||||
await useNodeReplacementStore().load()
|
||||
await useExtensionService().loadExtensions()
|
||||
|
||||
this.addProcessKeyHandler()
|
||||
@@ -1148,6 +1148,8 @@ export class ComfyApp {
|
||||
|
||||
const embeddedModels: ModelFile[] = []
|
||||
|
||||
const nodeReplacementStore = useNodeReplacementStore()
|
||||
|
||||
const collectMissingNodesAndModels = (
|
||||
nodes: ComfyWorkflowJSON['nodes'],
|
||||
path: string = ''
|
||||
@@ -1160,25 +1162,27 @@ export class ComfyApp {
|
||||
return
|
||||
}
|
||||
for (let n of nodes) {
|
||||
// Patch T2IAdapterLoader to ControlNetLoader since they are the same node now
|
||||
if (n.type == 'T2IAdapterLoader') n.type = 'ControlNetLoader'
|
||||
if (n.type == 'ConditioningAverage ') n.type = 'ConditioningAverage' //typo fix
|
||||
if (n.type == 'SDV_img2vid_Conditioning')
|
||||
n.type = 'SVD_img2vid_Conditioning' //typo fix
|
||||
if (n.type == 'Load3DAnimation') n.type = 'Load3D' // Animation node merged into Load3D
|
||||
if (n.type == 'Preview3DAnimation') n.type = 'Preview3D' // Animation node merged into Load3D
|
||||
// When node replacement is disabled, fall back to hardcoded patches
|
||||
if (!nodeReplacementStore.isEnabled) {
|
||||
if (n.type == 'T2IAdapterLoader') n.type = 'ControlNetLoader'
|
||||
if (n.type == 'ConditioningAverage ') n.type = 'ConditioningAverage'
|
||||
if (n.type == 'SDV_img2vid_Conditioning')
|
||||
n.type = 'SVD_img2vid_Conditioning'
|
||||
if (n.type == 'Load3DAnimation') n.type = 'Load3D'
|
||||
if (n.type == 'Preview3DAnimation') n.type = 'Preview3D'
|
||||
}
|
||||
|
||||
// Find missing node types
|
||||
if (!(n.type in LiteGraph.registered_node_types)) {
|
||||
// Include context about subgraph location if applicable
|
||||
if (path) {
|
||||
missingNodeTypes.push({
|
||||
type: n.type,
|
||||
hint: `in subgraph '${path}'`
|
||||
})
|
||||
} else {
|
||||
missingNodeTypes.push(n.type)
|
||||
}
|
||||
const replacement = nodeReplacementStore.getReplacementFor(n.type)
|
||||
|
||||
missingNodeTypes.push({
|
||||
type: n.type,
|
||||
...(path && { hint: `in subgraph '${path}'` }),
|
||||
isReplaceable: replacement !== null,
|
||||
replacement: replacement ?? undefined
|
||||
})
|
||||
|
||||
n.type = sanitizeNodeName(n.type)
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import type {
|
||||
Positionable
|
||||
} from '@/lib/litegraph/src/interfaces'
|
||||
import type { LGraphCanvas, LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type { NodeReplacement } from '@/platform/nodeReplacement/types'
|
||||
import type { SettingParams } from '@/platform/settings/types'
|
||||
import type { ComfyWorkflowJSON } from '@/platform/workflow/validation/schemas/workflowSchema'
|
||||
import type { Keybinding } from '@/platform/keybindings/types'
|
||||
@@ -93,6 +94,8 @@ export type MissingNodeType =
|
||||
text: string
|
||||
callback: () => void
|
||||
}
|
||||
isReplaceable?: boolean
|
||||
replacement?: NodeReplacement
|
||||
}
|
||||
|
||||
export interface ComfyExtension {
|
||||
|
||||
Reference in New Issue
Block a user