mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-19 22:09:37 +00:00
Compare commits
15 Commits
fix/space-
...
test/node-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
24a3b28fb9 | ||
|
|
6ac72fafcc | ||
|
|
f50afee6c4 | ||
|
|
3b3430e2d8 | ||
|
|
52a46e72c9 | ||
|
|
513dd0e426 | ||
|
|
1fc34dfd6a | ||
|
|
24612b2082 | ||
|
|
7a7a1a5e70 | ||
|
|
4c2f2a910f | ||
|
|
31177bc036 | ||
|
|
8390838ed2 | ||
|
|
ee0c0e9996 | ||
|
|
4b57a12ca5 | ||
|
|
07c8b822bc |
59
browser_tests/assets/missing/replaceable_nodes.json
Normal file
59
browser_tests/assets/missing/replaceable_nodes.json
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
{
|
||||||
|
"last_node_id": 3,
|
||||||
|
"last_link_id": 1,
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"type": "T2IAdapterLoader",
|
||||||
|
"pos": [100, 100],
|
||||||
|
"size": { "0": 300, "1": 58 },
|
||||||
|
"flags": {},
|
||||||
|
"order": 0,
|
||||||
|
"mode": 0,
|
||||||
|
"inputs": [],
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "CONTROL_NET",
|
||||||
|
"type": "CONTROL_NET",
|
||||||
|
"links": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"properties": { "Node name for S&R": "T2IAdapterLoader" },
|
||||||
|
"widgets_values": ["t2iadapter_model.safetensors"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"type": "ImageBatch",
|
||||||
|
"pos": [100, 200],
|
||||||
|
"size": { "0": 210, "1": 46 },
|
||||||
|
"flags": {},
|
||||||
|
"order": 1,
|
||||||
|
"mode": 0,
|
||||||
|
"inputs": [
|
||||||
|
{ "name": "image1", "type": "IMAGE", "link": null },
|
||||||
|
{ "name": "image2", "type": "IMAGE", "link": null }
|
||||||
|
],
|
||||||
|
"outputs": [{ "name": "IMAGE", "type": "IMAGE", "links": [1] }],
|
||||||
|
"properties": { "Node name for S&R": "ImageBatch" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"type": "UNKNOWN_NO_REPLACEMENT",
|
||||||
|
"pos": [100, 300],
|
||||||
|
"size": { "0": 210, "1": 46 },
|
||||||
|
"flags": {},
|
||||||
|
"order": 2,
|
||||||
|
"mode": 0,
|
||||||
|
"inputs": [{ "name": "image", "type": "IMAGE", "link": 1 }],
|
||||||
|
"outputs": [{ "name": "IMAGE", "type": "IMAGE", "links": null }],
|
||||||
|
"properties": { "Node name for S&R": "UNKNOWN_NO_REPLACEMENT" }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"links": [[1, 2, 0, 3, 0, "IMAGE"]],
|
||||||
|
"groups": [],
|
||||||
|
"config": {},
|
||||||
|
"extra": {
|
||||||
|
"ds": { "scale": 1, "offset": [0, 0] }
|
||||||
|
},
|
||||||
|
"version": 0.4
|
||||||
|
}
|
||||||
@@ -34,6 +34,33 @@ test.describe('Load workflow warning', { tag: '@ui' }, () => {
|
|||||||
expect(warningText).toContain('MISSING_NODE_TYPE_IN_SUBGRAPH')
|
expect(warningText).toContain('MISSING_NODE_TYPE_IN_SUBGRAPH')
|
||||||
expect(warningText).toContain('in subgraph')
|
expect(warningText).toContain('in subgraph')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('Should show replacement UI for replaceable missing nodes', async ({
|
||||||
|
comfyPage
|
||||||
|
}) => {
|
||||||
|
await comfyPage.settings.setSetting('Comfy.NodeReplacement.Enabled', true)
|
||||||
|
await comfyPage.workflow.loadWorkflow('missing/replaceable_nodes')
|
||||||
|
|
||||||
|
const missingNodesWarning = comfyPage.page.locator('.comfy-missing-nodes')
|
||||||
|
await expect(missingNodesWarning).toBeVisible()
|
||||||
|
|
||||||
|
// Verify "Replaceable" badges appear for nodes with replacements
|
||||||
|
const replaceableBadges = missingNodesWarning.getByText('Replaceable')
|
||||||
|
await expect(replaceableBadges.first()).toBeVisible()
|
||||||
|
expect(await replaceableBadges.count()).toBeGreaterThanOrEqual(2)
|
||||||
|
|
||||||
|
// Verify individual "Replace" buttons appear
|
||||||
|
const replaceButtons = missingNodesWarning.getByRole('button', {
|
||||||
|
name: 'Replace'
|
||||||
|
})
|
||||||
|
expect(await replaceButtons.count()).toBeGreaterThanOrEqual(2)
|
||||||
|
|
||||||
|
// Verify "Replace All" button appears in footer
|
||||||
|
const replaceAllButton = comfyPage.page.getByRole('button', {
|
||||||
|
name: 'Replace All'
|
||||||
|
})
|
||||||
|
await expect(replaceAllButton).toBeVisible()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
test('Does not report warning on undo/redo', async ({ comfyPage }) => {
|
test('Does not report warning on undo/redo', async ({ comfyPage }) => {
|
||||||
|
|||||||
@@ -25,10 +25,25 @@
|
|||||||
:key="i"
|
:key="i"
|
||||||
class="flex min-h-8 items-center justify-between px-4 py-2 bg-secondary-background text-muted-foreground"
|
class="flex min-h-8 items-center justify-between px-4 py-2 bg-secondary-background text-muted-foreground"
|
||||||
>
|
>
|
||||||
<span class="text-xs">
|
<div class="flex items-center gap-2">
|
||||||
{{ node.label }}
|
<StatusBadge
|
||||||
</span>
|
v-if="node.isReplaceable"
|
||||||
<span v-if="node.hint" class="text-xs">{{ node.hint }}</span>
|
:label="$t('nodeReplacement.replaceable')"
|
||||||
|
severity="default"
|
||||||
|
/>
|
||||||
|
<span class="text-xs">{{ node.label }}</span>
|
||||||
|
<span v-if="node.hint" class="text-xs text-muted-foreground">
|
||||||
|
{{ node.hint }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
v-if="node.isReplaceable"
|
||||||
|
variant="secondary"
|
||||||
|
size="sm"
|
||||||
|
@click="emit('replace', node.label)"
|
||||||
|
>
|
||||||
|
{{ $t('nodeReplacement.replace') }}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -49,7 +64,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
import StatusBadge from '@/components/common/StatusBadge.vue'
|
||||||
import MissingCoreNodesMessage from '@/components/dialog/content/MissingCoreNodesMessage.vue'
|
import MissingCoreNodesMessage from '@/components/dialog/content/MissingCoreNodesMessage.vue'
|
||||||
|
import Button from '@/components/ui/button/Button.vue'
|
||||||
import { isCloud } from '@/platform/distribution/types'
|
import { isCloud } from '@/platform/distribution/types'
|
||||||
import type { MissingNodeType } from '@/types/comfy'
|
import type { MissingNodeType } from '@/types/comfy'
|
||||||
import { useMissingNodes } from '@/workbench/extensions/manager/composables/nodePack/useMissingNodes'
|
import { useMissingNodes } from '@/workbench/extensions/manager/composables/nodePack/useMissingNodes'
|
||||||
@@ -58,6 +75,10 @@ const props = defineProps<{
|
|||||||
missingNodeTypes: MissingNodeType[]
|
missingNodeTypes: MissingNodeType[]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'replace', nodeType: string): void
|
||||||
|
}>()
|
||||||
|
|
||||||
// Get missing core nodes for OSS mode
|
// Get missing core nodes for OSS mode
|
||||||
const { missingCoreNodes } = useMissingNodes()
|
const { missingCoreNodes } = useMissingNodes()
|
||||||
|
|
||||||
@@ -75,10 +96,12 @@ const uniqueNodes = computed(() => {
|
|||||||
return {
|
return {
|
||||||
label: node.type,
|
label: node.type,
|
||||||
hint: node.hint,
|
hint: node.hint,
|
||||||
action: node.action
|
action: node.action,
|
||||||
|
isReplaceable: node.isReplaceable ?? false,
|
||||||
|
replacement: node.replacement
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return { label: node }
|
return { label: node, isReplaceable: false }
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<!-- Cloud mode: Learn More + Got It buttons -->
|
<!-- Cloud mode: Learn More + Replace All + Got It buttons -->
|
||||||
<div
|
<div
|
||||||
v-if="isCloud"
|
v-if="isCloud"
|
||||||
class="flex w-full items-center justify-between gap-2 py-2 px-4"
|
class="flex w-full items-center justify-between gap-2 py-2 px-4"
|
||||||
@@ -15,16 +15,34 @@
|
|||||||
<i class="icon-[lucide--info]"></i>
|
<i class="icon-[lucide--info]"></i>
|
||||||
<span>{{ $t('missingNodes.cloud.learnMore') }}</span>
|
<span>{{ $t('missingNodes.cloud.learnMore') }}</span>
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="secondary" size="md" @click="handleGotItClick">{{
|
<div class="flex gap-1">
|
||||||
$t('missingNodes.cloud.gotIt')
|
<Button
|
||||||
}}</Button>
|
v-if="hasReplaceableNodes"
|
||||||
|
variant="primary"
|
||||||
|
size="md"
|
||||||
|
@click="emit('replaceAll')"
|
||||||
|
>
|
||||||
|
{{ $t('nodeReplacement.replaceAll') }}
|
||||||
|
</Button>
|
||||||
|
<Button variant="secondary" size="md" @click="handleGotItClick">{{
|
||||||
|
$t('missingNodes.cloud.gotIt')
|
||||||
|
}}</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- OSS mode: Open Manager + Install All buttons -->
|
<!-- OSS mode: Open Manager + Replace All + Install All buttons -->
|
||||||
<div v-else-if="showManagerButtons" class="flex justify-end gap-1 py-2 px-4">
|
<div v-else-if="showManagerButtons" class="flex justify-end gap-1 py-2 px-4">
|
||||||
<Button variant="textonly" @click="openManager">{{
|
<Button variant="textonly" @click="openManager">{{
|
||||||
$t('g.openManager')
|
$t('g.openManager')
|
||||||
}}</Button>
|
}}</Button>
|
||||||
|
<Button
|
||||||
|
v-if="hasReplaceableNodes"
|
||||||
|
variant="primary"
|
||||||
|
size="md"
|
||||||
|
@click="emit('replaceAll')"
|
||||||
|
>
|
||||||
|
{{ $t('nodeReplacement.replaceAll') }}
|
||||||
|
</Button>
|
||||||
<PackInstallButton
|
<PackInstallButton
|
||||||
v-if="showInstallAllButton"
|
v-if="showInstallAllButton"
|
||||||
type="secondary"
|
type="secondary"
|
||||||
@@ -51,12 +69,25 @@ import Button from '@/components/ui/button/Button.vue'
|
|||||||
import { isCloud } from '@/platform/distribution/types'
|
import { isCloud } from '@/platform/distribution/types'
|
||||||
import { useToastStore } from '@/platform/updates/common/toastStore'
|
import { useToastStore } from '@/platform/updates/common/toastStore'
|
||||||
import { useDialogStore } from '@/stores/dialogStore'
|
import { useDialogStore } from '@/stores/dialogStore'
|
||||||
|
import type { MissingNodeType } from '@/types/comfy'
|
||||||
import PackInstallButton from '@/workbench/extensions/manager/components/manager/button/PackInstallButton.vue'
|
import PackInstallButton from '@/workbench/extensions/manager/components/manager/button/PackInstallButton.vue'
|
||||||
import { useMissingNodes } from '@/workbench/extensions/manager/composables/nodePack/useMissingNodes'
|
import { useMissingNodes } from '@/workbench/extensions/manager/composables/nodePack/useMissingNodes'
|
||||||
import { useManagerState } from '@/workbench/extensions/manager/composables/useManagerState'
|
import { useManagerState } from '@/workbench/extensions/manager/composables/useManagerState'
|
||||||
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
|
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
|
||||||
import { ManagerTab } from '@/workbench/extensions/manager/types/comfyManagerTypes'
|
import { ManagerTab } from '@/workbench/extensions/manager/types/comfyManagerTypes'
|
||||||
|
|
||||||
|
const { missingNodeTypes = [] } = defineProps<{
|
||||||
|
missingNodeTypes?: MissingNodeType[]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'replaceAll'): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const hasReplaceableNodes = computed(() =>
|
||||||
|
missingNodeTypes.some((n) => typeof n === 'object' && n.isReplaceable)
|
||||||
|
)
|
||||||
|
|
||||||
const dialogStore = useDialogStore()
|
const dialogStore = useDialogStore()
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
|||||||
@@ -2806,6 +2806,14 @@
|
|||||||
"replacementInstruction": "Install these nodes to run this workflow, or replace them with installed alternatives. Missing nodes are highlighted in red on the canvas."
|
"replacementInstruction": "Install these nodes to run this workflow, or replace them with installed alternatives. Missing nodes are highlighted in red on the canvas."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"nodeReplacement": {
|
||||||
|
"replaceable": "Replaceable",
|
||||||
|
"replace": "Replace",
|
||||||
|
"replaceAll": "Replace All",
|
||||||
|
"replacedNode": "Replaced node: {nodeType}",
|
||||||
|
"replacedAllNodes": "Replaced {count} node type(s)",
|
||||||
|
"replaceFailed": "Failed to replace nodes"
|
||||||
|
},
|
||||||
"rightSidePanel": {
|
"rightSidePanel": {
|
||||||
"togglePanel": "Toggle properties panel",
|
"togglePanel": "Toggle properties panel",
|
||||||
"noSelection": "Select a node to see its properties and info.",
|
"noSelection": "Select a node to see its properties and info.",
|
||||||
|
|||||||
@@ -236,5 +236,15 @@ describe('useNodeReplacementStore', () => {
|
|||||||
|
|
||||||
expect(fetchNodeReplacements).toHaveBeenCalledOnce()
|
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,7 +15,7 @@ export const useNodeReplacementStore = defineStore('nodeReplacement', () => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
async function load() {
|
async function load() {
|
||||||
if (isLoaded.value) return
|
if (!isEnabled.value || isLoaded.value) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
replacements.value = await fetchNodeReplacements()
|
replacements.value = await fetchNodeReplacements()
|
||||||
|
|||||||
195
src/platform/nodeReplacement/useNodeReplacement.ts
Normal file
195
src/platform/nodeReplacement/useNodeReplacement.ts
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
import { clone } from 'es-toolkit/compat'
|
||||||
|
|
||||||
|
import { t } from '@/i18n'
|
||||||
|
import { useToastStore } from '@/platform/updates/common/toastStore'
|
||||||
|
import type { ComfyWorkflowJSON } from '@/platform/workflow/validation/schemas/workflowSchema'
|
||||||
|
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
|
||||||
|
import { app } from '@/scripts/app'
|
||||||
|
import type { MissingNodeType } from '@/types/comfy'
|
||||||
|
|
||||||
|
import { useNodeReplacementStore } from './nodeReplacementStore'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modify workflow data to replace missing node types with their replacements
|
||||||
|
* @param graphData The workflow JSON data
|
||||||
|
* @param replacements Map of old node type to new node type
|
||||||
|
* @returns Modified workflow data with node types replaced
|
||||||
|
*/
|
||||||
|
function applyNodeReplacements(
|
||||||
|
graphData: ComfyWorkflowJSON,
|
||||||
|
replacements: Map<string, string>
|
||||||
|
): ComfyWorkflowJSON {
|
||||||
|
const modifiedData = clone(graphData)
|
||||||
|
|
||||||
|
// Helper function to process nodes array
|
||||||
|
function processNodes(nodes: ComfyWorkflowJSON['nodes']) {
|
||||||
|
if (!Array.isArray(nodes)) return
|
||||||
|
|
||||||
|
for (const node of nodes) {
|
||||||
|
const replacement = replacements.get(node.type)
|
||||||
|
if (replacement) {
|
||||||
|
node.type = replacement
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process top-level nodes
|
||||||
|
processNodes(modifiedData.nodes)
|
||||||
|
|
||||||
|
// Process nodes in subgraphs
|
||||||
|
if (modifiedData.definitions?.subgraphs) {
|
||||||
|
for (const subgraph of modifiedData.definitions.subgraphs) {
|
||||||
|
if (subgraph && 'nodes' in subgraph) {
|
||||||
|
processNodes(subgraph.nodes as ComfyWorkflowJSON['nodes'])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return modifiedData
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useNodeReplacement() {
|
||||||
|
const nodeReplacementStore = useNodeReplacementStore()
|
||||||
|
const workflowStore = useWorkflowStore()
|
||||||
|
const toastStore = useToastStore()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a map of replacements from missing node types
|
||||||
|
*/
|
||||||
|
function buildReplacementMap(
|
||||||
|
missingNodeTypes: MissingNodeType[]
|
||||||
|
): Map<string, string> {
|
||||||
|
const replacements = new Map<string, string>()
|
||||||
|
|
||||||
|
for (const nodeType of missingNodeTypes) {
|
||||||
|
if (typeof nodeType === 'object' && nodeType.isReplaceable) {
|
||||||
|
const replacement = nodeType.replacement
|
||||||
|
if (replacement) {
|
||||||
|
replacements.set(nodeType.type, replacement.new_node_id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return replacements
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replace a single node type with its replacement
|
||||||
|
* This reloads the entire workflow with the replacement applied
|
||||||
|
* @param nodeType The type of the missing node to replace
|
||||||
|
* @returns true if replacement was successful
|
||||||
|
*/
|
||||||
|
async function replaceNode(nodeType: string): Promise<boolean> {
|
||||||
|
const replacement = nodeReplacementStore.getReplacementFor(nodeType)
|
||||||
|
if (!replacement) {
|
||||||
|
console.warn(`No replacement found for node type: ${nodeType}`)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const activeWorkflow = workflowStore.activeWorkflow
|
||||||
|
if (!activeWorkflow?.isLoaded) {
|
||||||
|
console.error('No active workflow or workflow not loaded')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Use current graph state, not originalContent, to preserve prior replacements
|
||||||
|
const currentData =
|
||||||
|
app.rootGraph.serialize() as unknown as ComfyWorkflowJSON
|
||||||
|
|
||||||
|
// Create replacement map for single node
|
||||||
|
const replacements = new Map<string, string>()
|
||||||
|
replacements.set(nodeType, replacement.new_node_id)
|
||||||
|
|
||||||
|
// Apply replacements
|
||||||
|
const modifiedData = applyNodeReplacements(currentData, replacements)
|
||||||
|
|
||||||
|
// Reload the workflow with modified data
|
||||||
|
await app.loadGraphData(modifiedData, true, false, activeWorkflow, {
|
||||||
|
showMissingNodesDialog: true,
|
||||||
|
showMissingModelsDialog: true
|
||||||
|
})
|
||||||
|
|
||||||
|
toastStore.add({
|
||||||
|
severity: 'success',
|
||||||
|
summary: t('g.success'),
|
||||||
|
detail: t('nodeReplacement.replacedNode', { nodeType }),
|
||||||
|
life: 3000
|
||||||
|
})
|
||||||
|
|
||||||
|
return true
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to replace node:', error)
|
||||||
|
toastStore.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: t('g.error'),
|
||||||
|
detail: t('nodeReplacement.replaceFailed'),
|
||||||
|
life: 5000
|
||||||
|
})
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replace all replaceable missing nodes
|
||||||
|
* This reloads the entire workflow with all replacements applied
|
||||||
|
* @param missingNodeTypes Array of missing node types (from dialog props)
|
||||||
|
* @returns Number of node types that were replaced
|
||||||
|
*/
|
||||||
|
async function replaceAllNodes(
|
||||||
|
missingNodeTypes: MissingNodeType[]
|
||||||
|
): Promise<number> {
|
||||||
|
const replacements = buildReplacementMap(missingNodeTypes)
|
||||||
|
|
||||||
|
if (replacements.size === 0) {
|
||||||
|
console.warn('No replaceable nodes found')
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
const activeWorkflow = workflowStore.activeWorkflow
|
||||||
|
if (!activeWorkflow?.isLoaded) {
|
||||||
|
console.error('No active workflow or workflow not loaded')
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Use current graph state, not originalContent, to preserve any prior changes
|
||||||
|
const currentData =
|
||||||
|
app.rootGraph.serialize() as unknown as ComfyWorkflowJSON
|
||||||
|
|
||||||
|
// Apply all replacements
|
||||||
|
const modifiedData = applyNodeReplacements(currentData, replacements)
|
||||||
|
|
||||||
|
// Reload the workflow with modified data
|
||||||
|
await app.loadGraphData(modifiedData, true, false, activeWorkflow, {
|
||||||
|
showMissingNodesDialog: true,
|
||||||
|
showMissingModelsDialog: true
|
||||||
|
})
|
||||||
|
|
||||||
|
toastStore.add({
|
||||||
|
severity: 'success',
|
||||||
|
summary: t('g.success'),
|
||||||
|
detail: t('nodeReplacement.replacedAllNodes', {
|
||||||
|
count: replacements.size
|
||||||
|
}),
|
||||||
|
life: 3000
|
||||||
|
})
|
||||||
|
|
||||||
|
return replacements.size
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to replace nodes:', error)
|
||||||
|
toastStore.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: t('g.error'),
|
||||||
|
detail: t('nodeReplacement.replaceFailed'),
|
||||||
|
life: 5000
|
||||||
|
})
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
replaceNode,
|
||||||
|
replaceAllNodes
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1137,25 +1137,25 @@ export class ComfyApp {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
for (let n of nodes) {
|
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
|
|
||||||
|
|
||||||
// Find missing node types
|
// Find missing node types
|
||||||
if (!(n.type in LiteGraph.registered_node_types)) {
|
if (!(n.type in LiteGraph.registered_node_types)) {
|
||||||
// Include context about subgraph location if applicable
|
const nodeReplacementStore = useNodeReplacementStore()
|
||||||
if (path) {
|
const replacement = nodeReplacementStore.getReplacementFor(n.type)
|
||||||
missingNodeTypes.push({
|
|
||||||
type: n.type,
|
// TODO: Remove debug log
|
||||||
hint: `in subgraph '${path}'`
|
console.log('[MissingNode]', n.type, {
|
||||||
})
|
isReplaceable: replacement !== null,
|
||||||
} else {
|
replacement,
|
||||||
missingNodeTypes.push(n.type)
|
allReplacements: nodeReplacementStore.replacements
|
||||||
}
|
})
|
||||||
|
|
||||||
|
missingNodeTypes.push({
|
||||||
|
type: n.type,
|
||||||
|
...(path && { hint: `in subgraph '${path}'` }),
|
||||||
|
isReplaceable: replacement !== null,
|
||||||
|
replacement: replacement ?? undefined
|
||||||
|
})
|
||||||
|
|
||||||
n.type = sanitizeNodeName(n.type)
|
n.type = sanitizeNodeName(n.type)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import PromptDialogContent from '@/components/dialog/content/PromptDialogContent
|
|||||||
import { t } from '@/i18n'
|
import { t } from '@/i18n'
|
||||||
import { useTelemetry } from '@/platform/telemetry'
|
import { useTelemetry } from '@/platform/telemetry'
|
||||||
import { isCloud } from '@/platform/distribution/types'
|
import { isCloud } from '@/platform/distribution/types'
|
||||||
|
import { useNodeReplacement } from '@/platform/nodeReplacement/useNodeReplacement'
|
||||||
import { useSubscription } from '@/platform/cloud/subscription/composables/useSubscription'
|
import { useSubscription } from '@/platform/cloud/subscription/composables/useSubscription'
|
||||||
import { useDialogStore } from '@/stores/dialogStore'
|
import { useDialogStore } from '@/stores/dialogStore'
|
||||||
import type {
|
import type {
|
||||||
@@ -14,6 +15,7 @@ import type {
|
|||||||
ShowDialogOptions
|
ShowDialogOptions
|
||||||
} from '@/stores/dialogStore'
|
} from '@/stores/dialogStore'
|
||||||
|
|
||||||
|
import type { MissingNodeType } from '@/types/comfy'
|
||||||
import type { ConflictDetectionResult } from '@/workbench/extensions/manager/types/conflictDetectionTypes'
|
import type { ConflictDetectionResult } from '@/workbench/extensions/manager/types/conflictDetectionTypes'
|
||||||
import type { ComponentAttrs } from 'vue-component-type-helpers'
|
import type { ComponentAttrs } from 'vue-component-type-helpers'
|
||||||
|
|
||||||
@@ -94,6 +96,17 @@ export const useDialogService = () => {
|
|||||||
lazyMissingNodesFooter()
|
lazyMissingNodesFooter()
|
||||||
])
|
])
|
||||||
|
|
||||||
|
const { replaceNode, replaceAllNodes } = useNodeReplacement()
|
||||||
|
|
||||||
|
const handleReplace = async (nodeType: string) => {
|
||||||
|
await replaceNode(nodeType)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleReplaceAll = async () => {
|
||||||
|
await replaceAllNodes(props.missingNodeTypes as MissingNodeType[])
|
||||||
|
dialogStore.closeDialog({ key: 'global-missing-nodes' })
|
||||||
|
}
|
||||||
|
|
||||||
dialogStore.showDialog({
|
dialogStore.showDialog({
|
||||||
key: 'global-missing-nodes',
|
key: 'global-missing-nodes',
|
||||||
headerComponent: MissingNodesHeader,
|
headerComponent: MissingNodesHeader,
|
||||||
@@ -113,7 +126,14 @@ export const useDialogService = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
props
|
props: {
|
||||||
|
...props,
|
||||||
|
onReplace: handleReplace
|
||||||
|
},
|
||||||
|
footerProps: {
|
||||||
|
missingNodeTypes: props.missingNodeTypes,
|
||||||
|
onReplaceAll: handleReplaceAll
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type {
|
|||||||
Positionable
|
Positionable
|
||||||
} from '@/lib/litegraph/src/interfaces'
|
} from '@/lib/litegraph/src/interfaces'
|
||||||
import type { LGraphCanvas, LGraphNode } from '@/lib/litegraph/src/litegraph'
|
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 { SettingParams } from '@/platform/settings/types'
|
||||||
import type { ComfyWorkflowJSON } from '@/platform/workflow/validation/schemas/workflowSchema'
|
import type { ComfyWorkflowJSON } from '@/platform/workflow/validation/schemas/workflowSchema'
|
||||||
import type { Keybinding } from '@/platform/keybindings/types'
|
import type { Keybinding } from '@/platform/keybindings/types'
|
||||||
@@ -93,6 +94,8 @@ export type MissingNodeType =
|
|||||||
text: string
|
text: string
|
||||||
callback: () => void
|
callback: () => void
|
||||||
}
|
}
|
||||||
|
isReplaceable?: boolean
|
||||||
|
replacement?: NodeReplacement
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ComfyExtension {
|
export interface ComfyExtension {
|
||||||
|
|||||||
Reference in New Issue
Block a user