mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-13 09:00:16 +00:00
fix: Prune invalid builder mappings on load (#9376)
## Summary - extract resolveNode to reusable util - remove mid builder pruning - handle missing widgets with label ## Review Focus `resolveNode` was simplified for subgraphs by calling getNodeById on each of the subgraphs instead of searching their inner nodes manually. ## Screenshots (if applicable) "Widget not visible" <img width="657" height="822" alt="image" src="https://github.com/user-attachments/assets/ab7d1e87-3210-4e54-876a-07881974b5c7" /> <img width="674" height="375" alt="image" src="https://github.com/user-attachments/assets/c50ec871-d423-43d6-8e1e-7b1a362f621c" /> ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-9376-fix-Prune-invalid-builder-mappings-on-load-3196d73d3650811280c2d459ed0271af) by [Unito](https://www.unito.io)
This commit is contained in:
@@ -7,6 +7,8 @@ import type { LoadedComfyWorkflow } from '@/platform/workflow/management/stores/
|
||||
import { ComfyWorkflow as ComfyWorkflowClass } from '@/platform/workflow/management/stores/comfyWorkflow'
|
||||
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
|
||||
import { app } from '@/scripts/app'
|
||||
import type { LGraphNode, NodeId } from '@/lib/litegraph/src/LGraphNode'
|
||||
import type { ChangeTracker } from '@/scripts/changeTracker'
|
||||
import { createMockChangeTracker } from '@/utils/__tests__/litegraphTestUtils'
|
||||
|
||||
vi.mock('@/scripts/app', () => ({
|
||||
@@ -15,6 +17,14 @@ vi.mock('@/scripts/app', () => ({
|
||||
}
|
||||
}))
|
||||
|
||||
const mockResolveNode = vi.hoisted(() =>
|
||||
vi.fn<(id: NodeId) => LGraphNode | undefined>(() => undefined)
|
||||
)
|
||||
vi.mock('@/utils/litegraphUtil', async (importOriginal) => ({
|
||||
...(await importOriginal()),
|
||||
resolveNode: mockResolveNode
|
||||
}))
|
||||
|
||||
vi.mock('@/renderer/core/canvas/canvasStore', () => ({
|
||||
useCanvasStore: () => ({
|
||||
getCanvas: () => ({ read_only: false })
|
||||
@@ -43,6 +53,7 @@ describe('appModeStore', () => {
|
||||
setActivePinia(createTestingPinia({ stubActions: false }))
|
||||
vi.clearAllMocks()
|
||||
vi.mocked(app.rootGraph).extra = {}
|
||||
mockResolveNode.mockReturnValue(undefined)
|
||||
})
|
||||
|
||||
describe('enterBuilder', () => {
|
||||
@@ -93,6 +104,105 @@ describe('appModeStore', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('loadSelections pruning', () => {
|
||||
function mockNode(id: number) {
|
||||
return { id }
|
||||
}
|
||||
|
||||
function workflowWithLinearData(
|
||||
inputs: [number, string][],
|
||||
outputs: number[]
|
||||
) {
|
||||
const workflow = createBuilderWorkflow('app')
|
||||
workflow.changeTracker = createMockChangeTracker({
|
||||
activeState: {
|
||||
last_node_id: 0,
|
||||
last_link_id: 0,
|
||||
nodes: [],
|
||||
links: [],
|
||||
groups: [],
|
||||
config: {},
|
||||
version: 0.4,
|
||||
extra: { linearData: { inputs, outputs } }
|
||||
}
|
||||
} as unknown as Partial<ChangeTracker>)
|
||||
return workflow
|
||||
}
|
||||
|
||||
it('removes inputs referencing deleted nodes on load', async () => {
|
||||
const node1 = mockNode(1)
|
||||
mockResolveNode.mockImplementation((id) =>
|
||||
id == 1 ? (node1 as unknown as LGraphNode) : undefined
|
||||
)
|
||||
|
||||
const workflowStore = useWorkflowStore()
|
||||
const store = useAppModeStore()
|
||||
|
||||
workflowStore.activeWorkflow = workflowWithLinearData(
|
||||
[
|
||||
[1, 'prompt'],
|
||||
[99, 'width']
|
||||
],
|
||||
[]
|
||||
)
|
||||
await nextTick()
|
||||
|
||||
expect(store.selectedInputs).toEqual([[1, 'prompt']])
|
||||
})
|
||||
|
||||
it('keeps inputs for existing nodes even if widget is missing', async () => {
|
||||
const node1 = mockNode(1)
|
||||
mockResolveNode.mockImplementation((id) =>
|
||||
id == 1 ? (node1 as unknown as LGraphNode) : undefined
|
||||
)
|
||||
|
||||
const workflowStore = useWorkflowStore()
|
||||
const store = useAppModeStore()
|
||||
|
||||
workflowStore.activeWorkflow = workflowWithLinearData(
|
||||
[
|
||||
[1, 'prompt'],
|
||||
[1, 'deleted_widget']
|
||||
],
|
||||
[]
|
||||
)
|
||||
await nextTick()
|
||||
|
||||
expect(store.selectedInputs).toEqual([
|
||||
[1, 'prompt'],
|
||||
[1, 'deleted_widget']
|
||||
])
|
||||
})
|
||||
|
||||
it('removes outputs referencing deleted nodes on load', async () => {
|
||||
const node1 = mockNode(1)
|
||||
mockResolveNode.mockImplementation((id) =>
|
||||
id == 1 ? (node1 as unknown as LGraphNode) : undefined
|
||||
)
|
||||
|
||||
const workflowStore = useWorkflowStore()
|
||||
const store = useAppModeStore()
|
||||
|
||||
workflowStore.activeWorkflow = workflowWithLinearData([], [1, 99])
|
||||
await nextTick()
|
||||
|
||||
expect(store.selectedOutputs).toEqual([1])
|
||||
})
|
||||
|
||||
it('hasOutputs is false when all output nodes are deleted', async () => {
|
||||
mockResolveNode.mockReturnValue(undefined)
|
||||
|
||||
const workflowStore = useWorkflowStore()
|
||||
const store = useAppModeStore()
|
||||
|
||||
workflowStore.activeWorkflow = workflowWithLinearData([], [10, 20])
|
||||
await nextTick()
|
||||
|
||||
expect(store.selectedOutputs).toEqual([])
|
||||
expect(store.hasOutputs).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('linearData sync watcher', () => {
|
||||
it('writes linearData to rootGraph.extra when in builder mode', async () => {
|
||||
const workflowStore = useWorkflowStore()
|
||||
|
||||
@@ -7,6 +7,7 @@ import type { LinearData } from '@/platform/workflow/management/stores/comfyWork
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
|
||||
import { app } from '@/scripts/app'
|
||||
import { resolveNode } from '@/utils/litegraphUtil'
|
||||
|
||||
export const useAppModeStore = defineStore('appMode', () => {
|
||||
const { getCanvas } = useCanvasStore()
|
||||
@@ -18,8 +19,21 @@ export const useAppModeStore = defineStore('appMode', () => {
|
||||
const hasOutputs = computed(() => !!selectedOutputs.length)
|
||||
|
||||
function loadSelections(data: Partial<LinearData> | undefined) {
|
||||
selectedInputs.splice(0, selectedInputs.length, ...(data?.inputs ?? []))
|
||||
selectedOutputs.splice(0, selectedOutputs.length, ...(data?.outputs ?? []))
|
||||
const rawInputs = data?.inputs ?? []
|
||||
const rawOutputs = data?.outputs ?? []
|
||||
|
||||
// Prune entries referencing nodes deleted in workflow mode.
|
||||
// Only check node existence, not widgets — dynamic widgets can
|
||||
// hide/show other widgets so a missing widget does not mean stale data.
|
||||
const inputs = app.rootGraph
|
||||
? rawInputs.filter(([nodeId]) => resolveNode(nodeId))
|
||||
: rawInputs
|
||||
const outputs = app.rootGraph
|
||||
? rawOutputs.filter((nodeId) => resolveNode(nodeId))
|
||||
: rawOutputs
|
||||
|
||||
selectedInputs.splice(0, selectedInputs.length, ...inputs)
|
||||
selectedOutputs.splice(0, selectedOutputs.length, ...outputs)
|
||||
}
|
||||
|
||||
function resetSelectedToWorkflow() {
|
||||
|
||||
Reference in New Issue
Block a user