mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-08 09:00:05 +00:00
fix: unpacking a missing node causes it to disappear (#7341)
## Summary Fixes the issue where unpacking a subgraph containing missing nodes causes those nodes to disappear. Missing nodes are now automatically restored as placeholder nodes that preserve their original data, allowing them to be recovered when the node types are installed later. ## Changes - **What**: - Modified `multiClone()` to preserve missing nodes as serialized data when creating subgraphs - Added `skipMissingNodes` option to `unpackSubgraph()` method to restore missing nodes as placeholder nodes instead of throwing errors - Updated `useSubgraphOperations.unpackSubgraph()` to automatically restore missing nodes as placeholders (removed confirmation dialog) - Replaced deprecated `LiteGraph.cloneObject()` with `structuredClone()` - Removed unused i18n keys and debugging logs ## Review Focus - **Placeholder node restoration**: Missing nodes are restored using the same mechanism as `LGraph.configure()` (creating `LGraphNode` with `last_serialization` and `has_errors` flags). This ensures compatibility with the existing missing node manager. - **Performance**: Optimized `getMissingNodeTypes()` to check `registered_node_types` first before attempting node creation, and uses Set for O(1) duplicate checking. - **Data preservation**: Missing nodes preserve their original type, title, and serialized data in `last_serialization`, allowing automatic recovery when node types are installed. - **Backward compatibility**: The `skipMissingNodes` option defaults to `false`, maintaining original behavior for other code paths. Only the UI-level `unpackSubgraph()` always uses `skipMissingNodes: true`. <!-- If this PR fixes an issue, uncomment and update the line below --> <!-- Fixes #7279 --> ## Demo Before: https://github.com/user-attachments/assets/e0327d05-802d-4a64-a9db-4d174e185d82 After: https://github.com/user-attachments/assets/37ab3140-0ada-480e-b9d5-fef8856f8b27 ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-7341-fix-unpacking-a-missing-node-causes-it-to-disappear-2c66d73d36508151ac6be70a7b2bc56d) by [Unito](https://www.unito.io)
This commit is contained in:
@@ -37,6 +37,21 @@ export function useSubgraphOperations() {
|
||||
workflowStore.activeWorkflow?.changeTracker?.checkState()
|
||||
}
|
||||
|
||||
const doUnpack = (
|
||||
subgraphNodes: SubgraphNode[],
|
||||
skipMissingNodes: boolean
|
||||
) => {
|
||||
const canvas = canvasStore.getCanvas()
|
||||
const graph = canvas.subgraph ?? canvas.graph
|
||||
if (!graph) return
|
||||
|
||||
for (const subgraphNode of subgraphNodes) {
|
||||
nodeOutputStore.revokeSubgraphPreviews(subgraphNode)
|
||||
graph.unpackSubgraph(subgraphNode, { skipMissingNodes })
|
||||
}
|
||||
workflowStore.activeWorkflow?.changeTracker?.checkState()
|
||||
}
|
||||
|
||||
const unpackSubgraph = () => {
|
||||
const canvas = canvasStore.getCanvas()
|
||||
const graph = canvas.subgraph ?? canvas.graph
|
||||
@@ -53,17 +68,7 @@ export function useSubgraphOperations() {
|
||||
if (subgraphNodes.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
subgraphNodes.forEach((subgraphNode) => {
|
||||
// Revoke any image previews for the subgraph
|
||||
nodeOutputStore.revokeSubgraphPreviews(subgraphNode)
|
||||
|
||||
// Unpack the subgraph
|
||||
graph.unpackSubgraph(subgraphNode)
|
||||
})
|
||||
|
||||
// Trigger change tracking
|
||||
workflowStore.activeWorkflow?.changeTracker?.checkState()
|
||||
doUnpack(subgraphNodes, true)
|
||||
}
|
||||
|
||||
const addSubgraphToLibrary = async () => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useCurrentUser } from '@/composables/auth/useCurrentUser'
|
||||
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
||||
import { useSelectedLiteGraphItems } from '@/composables/canvas/useSelectedLiteGraphItems'
|
||||
import { useSubgraphOperations } from '@/composables/graph/useSubgraphOperations'
|
||||
import { useExternalLink } from '@/composables/useExternalLink'
|
||||
import { useModelSelectorDialog } from '@/composables/useModelSelectorDialog'
|
||||
import {
|
||||
@@ -14,7 +15,6 @@ import {
|
||||
LGraphGroup,
|
||||
LGraphNode,
|
||||
LiteGraph,
|
||||
SubgraphNode
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import type { Point } from '@/lib/litegraph/src/litegraph'
|
||||
import { useAssetBrowserDialog } from '@/platform/assets/composables/useAssetBrowserDialog'
|
||||
@@ -41,7 +41,6 @@ import { useLitegraphService } from '@/services/litegraphService'
|
||||
import type { ComfyCommand } from '@/stores/commandStore'
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
import { useHelpCenterStore } from '@/stores/helpCenterStore'
|
||||
import { useNodeOutputStore } from '@/stores/imagePreviewStore'
|
||||
import { useQueueSettingsStore, useQueueStore } from '@/stores/queueStore'
|
||||
import { useSubgraphNavigationStore } from '@/stores/subgraphNavigationStore'
|
||||
import { useSubgraphStore } from '@/stores/subgraphStore'
|
||||
@@ -1010,14 +1009,8 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
label: 'Unpack the selected Subgraph',
|
||||
versionAdded: '1.26.3',
|
||||
function: () => {
|
||||
const canvas = canvasStore.getCanvas()
|
||||
const graph = canvas.subgraph ?? canvas.graph
|
||||
if (!graph) throw new TypeError('Canvas has no graph or subgraph set.')
|
||||
|
||||
const subgraphNode = app.canvas.selectedItems.values().next().value
|
||||
if (!(subgraphNode instanceof SubgraphNode)) return
|
||||
useNodeOutputStore().revokeSubgraphPreviews(subgraphNode)
|
||||
graph.unpackSubgraph(subgraphNode)
|
||||
const { unpackSubgraph } = useSubgraphOperations()
|
||||
unpackSubgraph()
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1726,9 +1726,15 @@ export class LGraph
|
||||
return { subgraph, node: subgraphNode as SubgraphNode }
|
||||
}
|
||||
|
||||
unpackSubgraph(subgraphNode: SubgraphNode) {
|
||||
unpackSubgraph(
|
||||
subgraphNode: SubgraphNode,
|
||||
options?: { skipMissingNodes?: boolean }
|
||||
) {
|
||||
if (!(subgraphNode instanceof SubgraphNode))
|
||||
throw new Error('Can only unpack Subgraph Nodes')
|
||||
|
||||
const skipMissingNodes = options?.skipMissingNodes ?? false
|
||||
|
||||
this.beforeChange()
|
||||
//NOTE: Create bounds can not be called on positionables directly as the subgraph is not being displayed and boundingRect is not initialized.
|
||||
//NOTE: NODE_TITLE_HEIGHT is explicitly excluded here
|
||||
@@ -1750,9 +1756,21 @@ export class LGraph
|
||||
const movedNodes = multiClone(subgraphNode.subgraph.nodes)
|
||||
const nodeIdMap = new Map<NodeId, NodeId>()
|
||||
for (const n_info of movedNodes) {
|
||||
const node = LiteGraph.createNode(String(n_info.type), n_info.title)
|
||||
let node = LiteGraph.createNode(String(n_info.type), n_info.title)
|
||||
if (!node) {
|
||||
throw new Error('Node not found')
|
||||
if (skipMissingNodes) {
|
||||
console.warn(
|
||||
`Cannot unpack node of type "${n_info.type}" - node type not found. Creating placeholder node.`
|
||||
)
|
||||
node = new LGraphNode(n_info.title || n_info.type || 'Missing Node')
|
||||
node.last_serialization = n_info
|
||||
node.has_errors = true
|
||||
node.type = String(n_info.type)
|
||||
} else {
|
||||
throw new Error(
|
||||
`Cannot unpack: node type "${n_info.type}" is not registered`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
nodeIdMap.set(n_info.id, ++this.last_node_id)
|
||||
|
||||
@@ -221,11 +221,13 @@ export function multiClone(nodes: Iterable<LGraphNode>): ISerialisedNode[] {
|
||||
const newNode = LiteGraph.createNode(node.type)
|
||||
if (!newNode) {
|
||||
console.warn('Failed to create node', node.type)
|
||||
const serializedData = structuredClone(node.serialize())
|
||||
clonedNodes.push(serializedData)
|
||||
continue
|
||||
}
|
||||
|
||||
// Must be cloned; litegraph "serialize" is mostly shallow clone
|
||||
const data = LiteGraph.cloneObject(node.serialize())
|
||||
const data = structuredClone(node.serialize())
|
||||
newNode.configure(data)
|
||||
|
||||
clonedNodes.push(newNode.serialize())
|
||||
|
||||
@@ -2,6 +2,7 @@ import _ from 'es-toolkit/compat'
|
||||
|
||||
import { downloadFile } from '@/base/common/downloadUtil'
|
||||
import { useSelectedLiteGraphItems } from '@/composables/canvas/useSelectedLiteGraphItems'
|
||||
import { useSubgraphOperations } from '@/composables/graph/useSubgraphOperations'
|
||||
import { useNodeAnimatedImage } from '@/composables/node/useNodeAnimatedImage'
|
||||
import { useNodeCanvasImagePreview } from '@/composables/node/useNodeCanvasImagePreview'
|
||||
import { useNodeImage, useNodeVideo } from '@/composables/node/useNodeImage'
|
||||
@@ -661,8 +662,8 @@ export const useLitegraphService = () => {
|
||||
{
|
||||
content: 'Unpack Subgraph',
|
||||
callback: () => {
|
||||
useNodeOutputStore().revokeSubgraphPreviews(this)
|
||||
this.graph.unpackSubgraph(this)
|
||||
const { unpackSubgraph } = useSubgraphOperations()
|
||||
unpackSubgraph()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user