mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-14 17:37:46 +00:00
Resolve issues with undo with Nodes 2.0 to fix link dragging/rendering (#8808)
## Summary Resolves the following issue: 1. Enable Nodes 2.0 2. Load default workflow 3. Move any node e.g. VAE decode 4. Undo All links go invisible, input/output slots no longer function ## Changes - **What** - Fixes slot layouts being deleted during undo/redo in `handleDeleteNode`, which prevented link dragging from nodes after undo. Vue patches (not remounts) components with the same key, so `onMounted` never fires to re-register them - these were already being cleared up on unmounted - Fixes links disappearing after undo by clearing `pendingSlotSync` when slot layouts already exist (undo/redo preserved them), rather than waiting for Vue mounts that do not happen ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-8808-Resolve-issues-with-undo-with-Nodes-2-0-to-fix-link-dragging-rendering-3046d73d3650818bbb0adf0104a5792d) by [Unito](https://www.unito.io)
This commit is contained in:
@@ -1,9 +1,14 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
|
||||
import { getSlotKey } from '@/renderer/core/layout/slots/slotIdentifier'
|
||||
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
|
||||
import { LayoutSource } from '@/renderer/core/layout/types'
|
||||
import type { LayoutChange, NodeLayout } from '@/renderer/core/layout/types'
|
||||
import type {
|
||||
LayoutChange,
|
||||
NodeLayout,
|
||||
SlotLayout
|
||||
} from '@/renderer/core/layout/types'
|
||||
|
||||
describe('layoutStore CRDT operations', () => {
|
||||
beforeEach(() => {
|
||||
@@ -406,4 +411,49 @@ describe('layoutStore CRDT operations', () => {
|
||||
LiteGraph.NODE_TITLE_HEIGHT = originalTitleHeight
|
||||
}
|
||||
})
|
||||
|
||||
it.for([
|
||||
{ type: 'input' as const, isInput: true },
|
||||
{ type: 'output' as const, isInput: false }
|
||||
])(
|
||||
'should preserve $type slot layouts when deleting a node',
|
||||
({ type, isInput }) => {
|
||||
const nodeId = 'slot-persist-node'
|
||||
const layout = createTestNode(nodeId)
|
||||
|
||||
layoutStore.applyOperation({
|
||||
type: 'createNode',
|
||||
entity: 'node',
|
||||
nodeId,
|
||||
layout,
|
||||
timestamp: Date.now(),
|
||||
source: LayoutSource.External,
|
||||
actor: 'test'
|
||||
})
|
||||
|
||||
const slotKey = getSlotKey(nodeId, 0, isInput)
|
||||
const slotLayout: SlotLayout = {
|
||||
nodeId,
|
||||
index: 0,
|
||||
type,
|
||||
position: { x: 110, y: 120 },
|
||||
bounds: { x: 105, y: 115, width: 10, height: 10 }
|
||||
}
|
||||
layoutStore.batchUpdateSlotLayouts([{ key: slotKey, layout: slotLayout }])
|
||||
expect(layoutStore.getSlotLayout(slotKey)).toEqual(slotLayout)
|
||||
|
||||
layoutStore.applyOperation({
|
||||
type: 'deleteNode',
|
||||
entity: 'node',
|
||||
nodeId,
|
||||
previousLayout: layout,
|
||||
timestamp: Date.now(),
|
||||
source: LayoutSource.External,
|
||||
actor: 'test'
|
||||
})
|
||||
|
||||
// Slot layout must survive so Vue-patched components can still drag links
|
||||
expect(layoutStore.getSlotLayout(slotKey)).toEqual(slotLayout)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
@@ -151,6 +151,10 @@ class LayoutStoreImpl implements LayoutStore {
|
||||
return this._pendingSlotSync
|
||||
}
|
||||
|
||||
get hasSlotLayouts(): boolean {
|
||||
return this.slotLayouts.size > 0
|
||||
}
|
||||
|
||||
setPendingSlotSync(value: boolean): void {
|
||||
this._pendingSlotSync = value
|
||||
}
|
||||
@@ -513,23 +517,6 @@ class LayoutStoreImpl implements LayoutStore {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all slot layouts for a node
|
||||
*/
|
||||
deleteNodeSlotLayouts(nodeId: NodeId): void {
|
||||
const keysToDelete: string[] = []
|
||||
for (const [key, layout] of this.slotLayouts) {
|
||||
if (layout.nodeId === nodeId) {
|
||||
keysToDelete.push(key)
|
||||
}
|
||||
}
|
||||
for (const key of keysToDelete) {
|
||||
this.slotLayouts.delete(key)
|
||||
// Remove from spatial index
|
||||
this.slotSpatialIndex.remove(key)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all slot layouts and their spatial index (O(1) operations)
|
||||
* Used when switching rendering modes (Vue ↔ LiteGraph)
|
||||
@@ -1117,13 +1104,10 @@ class LayoutStoreImpl implements LayoutStore {
|
||||
// During undo/redo, Vue components may still hold references to the old ref.
|
||||
// If we delete the trigger, Vue won't be notified when the node is re-created.
|
||||
// The trigger will be called in finalizeOperation to notify Vue of the change.
|
||||
|
||||
// We also intentionally do NOT delete slot layouts here for the same reason,
|
||||
// and cleanup is handled by onUnmounted in useSlotElementTracking.
|
||||
// Remove from spatial index
|
||||
this.spatialIndex.remove(operation.nodeId)
|
||||
|
||||
// Clean up associated slot layouts
|
||||
this.deleteNodeSlotLayouts(operation.nodeId)
|
||||
|
||||
// Clean up associated links
|
||||
const linksToDelete = this.findLinksConnectedToNode(operation.nodeId)
|
||||
|
||||
|
||||
@@ -302,7 +302,6 @@ export interface LayoutStore {
|
||||
deleteLinkLayout(linkId: LinkId): void
|
||||
deleteLinkSegmentLayout(linkId: LinkId, rerouteId: RerouteId | null): void
|
||||
deleteSlotLayout(key: string): void
|
||||
deleteNodeSlotLayouts(nodeId: NodeId): void
|
||||
deleteRerouteLayout(rerouteId: RerouteId): void
|
||||
clearAllSlotLayouts(): void
|
||||
|
||||
|
||||
Reference in New Issue
Block a user