mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-15 01:48:06 +00:00
Compare commits
15 Commits
fix/codera
...
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('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 }) => {
|
||||
|
||||
@@ -25,10 +25,25 @@
|
||||
:key="i"
|
||||
class="flex min-h-8 items-center justify-between px-4 py-2 bg-secondary-background text-muted-foreground"
|
||||
>
|
||||
<span class="text-xs">
|
||||
{{ node.label }}
|
||||
</span>
|
||||
<span v-if="node.hint" class="text-xs">{{ node.hint }}</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<StatusBadge
|
||||
v-if="node.isReplaceable"
|
||||
: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>
|
||||
|
||||
@@ -49,7 +64,9 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
import StatusBadge from '@/components/common/StatusBadge.vue'
|
||||
import MissingCoreNodesMessage from '@/components/dialog/content/MissingCoreNodesMessage.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import type { MissingNodeType } from '@/types/comfy'
|
||||
import { useMissingNodes } from '@/workbench/extensions/manager/composables/nodePack/useMissingNodes'
|
||||
@@ -58,6 +75,10 @@ const props = defineProps<{
|
||||
missingNodeTypes: MissingNodeType[]
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'replace', nodeType: string): void
|
||||
}>()
|
||||
|
||||
// Get missing core nodes for OSS mode
|
||||
const { missingCoreNodes } = useMissingNodes()
|
||||
|
||||
@@ -75,10 +96,12 @@ const uniqueNodes = computed(() => {
|
||||
return {
|
||||
label: node.type,
|
||||
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>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<!-- Cloud mode: Learn More + Got It buttons -->
|
||||
<!-- Cloud mode: Learn More + Replace All + Got It buttons -->
|
||||
<div
|
||||
v-if="isCloud"
|
||||
class="flex w-full items-center justify-between gap-2 py-2 px-4"
|
||||
@@ -15,16 +15,34 @@
|
||||
<i class="icon-[lucide--info]"></i>
|
||||
<span>{{ $t('missingNodes.cloud.learnMore') }}</span>
|
||||
</Button>
|
||||
<Button variant="secondary" size="md" @click="handleGotItClick">{{
|
||||
$t('missingNodes.cloud.gotIt')
|
||||
}}</Button>
|
||||
<div class="flex gap-1">
|
||||
<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>
|
||||
|
||||
<!-- 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">
|
||||
<Button variant="textonly" @click="openManager">{{
|
||||
$t('g.openManager')
|
||||
}}</Button>
|
||||
<Button
|
||||
v-if="hasReplaceableNodes"
|
||||
variant="primary"
|
||||
size="md"
|
||||
@click="emit('replaceAll')"
|
||||
>
|
||||
{{ $t('nodeReplacement.replaceAll') }}
|
||||
</Button>
|
||||
<PackInstallButton
|
||||
v-if="showInstallAllButton"
|
||||
type="secondary"
|
||||
@@ -51,12 +69,25 @@ import Button from '@/components/ui/button/Button.vue'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import { useToastStore } from '@/platform/updates/common/toastStore'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
import type { MissingNodeType } from '@/types/comfy'
|
||||
import PackInstallButton from '@/workbench/extensions/manager/components/manager/button/PackInstallButton.vue'
|
||||
import { useMissingNodes } from '@/workbench/extensions/manager/composables/nodePack/useMissingNodes'
|
||||
import { useManagerState } from '@/workbench/extensions/manager/composables/useManagerState'
|
||||
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
|
||||
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 { 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."
|
||||
}
|
||||
},
|
||||
"nodeReplacement": {
|
||||
"replaceable": "Replaceable",
|
||||
"replace": "Replace",
|
||||
"replaceAll": "Replace All",
|
||||
"replacedNode": "Replaced node: {nodeType}",
|
||||
"replacedAllNodes": "Replaced {count} node type(s)",
|
||||
"replaceFailed": "Failed to replace nodes"
|
||||
},
|
||||
"rightSidePanel": {
|
||||
"togglePanel": "Toggle properties panel",
|
||||
"noSelection": "Select a node to see its properties and info.",
|
||||
|
||||
@@ -236,5 +236,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,7 +15,7 @@ export const useNodeReplacementStore = defineStore('nodeReplacement', () => {
|
||||
)
|
||||
|
||||
async function load() {
|
||||
if (isLoaded.value) return
|
||||
if (!isEnabled.value || isLoaded.value) return
|
||||
|
||||
try {
|
||||
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
|
||||
}
|
||||
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
|
||||
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 nodeReplacementStore = useNodeReplacementStore()
|
||||
const replacement = nodeReplacementStore.getReplacementFor(n.type)
|
||||
|
||||
// TODO: Remove debug log
|
||||
console.log('[MissingNode]', n.type, {
|
||||
isReplaceable: replacement !== null,
|
||||
replacement,
|
||||
allReplacements: nodeReplacementStore.replacements
|
||||
})
|
||||
|
||||
missingNodeTypes.push({
|
||||
type: n.type,
|
||||
...(path && { hint: `in subgraph '${path}'` }),
|
||||
isReplaceable: replacement !== null,
|
||||
replacement: replacement ?? undefined
|
||||
})
|
||||
|
||||
n.type = sanitizeNodeName(n.type)
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import PromptDialogContent from '@/components/dialog/content/PromptDialogContent
|
||||
import { t } from '@/i18n'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import { useNodeReplacement } from '@/platform/nodeReplacement/useNodeReplacement'
|
||||
import { useSubscription } from '@/platform/cloud/subscription/composables/useSubscription'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
import type {
|
||||
@@ -14,6 +15,7 @@ import type {
|
||||
ShowDialogOptions
|
||||
} from '@/stores/dialogStore'
|
||||
|
||||
import type { MissingNodeType } from '@/types/comfy'
|
||||
import type { ConflictDetectionResult } from '@/workbench/extensions/manager/types/conflictDetectionTypes'
|
||||
import type { ComponentAttrs } from 'vue-component-type-helpers'
|
||||
|
||||
@@ -94,6 +96,17 @@ export const useDialogService = () => {
|
||||
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({
|
||||
key: 'global-missing-nodes',
|
||||
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
|
||||
} 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