mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-19 22:09:37 +00:00
Compare commits
8 Commits
feat/batch
...
feat/repla
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4dc67b375b | ||
|
|
1d8a01cdf8 | ||
|
|
b585dfa4fc | ||
|
|
1be6d27024 | ||
|
|
5aa4baf116 | ||
|
|
7d69a0db5b | ||
|
|
83bb4300e3 | ||
|
|
0d58a92e34 |
@@ -234,6 +234,7 @@ import { isCloud } from '@/platform/distribution/types'
|
|||||||
import type { NodeReplacement } from '@/platform/nodeReplacement/types'
|
import type { NodeReplacement } from '@/platform/nodeReplacement/types'
|
||||||
import { useNodeReplacement } from '@/platform/nodeReplacement/useNodeReplacement'
|
import { useNodeReplacement } from '@/platform/nodeReplacement/useNodeReplacement'
|
||||||
import { useDialogStore } from '@/stores/dialogStore'
|
import { useDialogStore } from '@/stores/dialogStore'
|
||||||
|
import { useExecutionErrorStore } from '@/stores/executionErrorStore'
|
||||||
import type { MissingNodeType } from '@/types/comfy'
|
import type { MissingNodeType } from '@/types/comfy'
|
||||||
import { cn } from '@/utils/tailwindUtil'
|
import { cn } from '@/utils/tailwindUtil'
|
||||||
import { useMissingNodes } from '@/workbench/extensions/manager/composables/nodePack/useMissingNodes'
|
import { useMissingNodes } from '@/workbench/extensions/manager/composables/nodePack/useMissingNodes'
|
||||||
@@ -245,6 +246,7 @@ const { missingNodeTypes } = defineProps<{
|
|||||||
const { missingCoreNodes } = useMissingNodes()
|
const { missingCoreNodes } = useMissingNodes()
|
||||||
const { replaceNodesInPlace } = useNodeReplacement()
|
const { replaceNodesInPlace } = useNodeReplacement()
|
||||||
const dialogStore = useDialogStore()
|
const dialogStore = useDialogStore()
|
||||||
|
const executionErrorStore = useExecutionErrorStore()
|
||||||
|
|
||||||
interface ProcessedNode {
|
interface ProcessedNode {
|
||||||
label: string
|
label: string
|
||||||
@@ -339,6 +341,11 @@ function handleReplaceSelected() {
|
|||||||
replacedTypes.value = nextReplaced
|
replacedTypes.value = nextReplaced
|
||||||
selectedTypes.value = nextSelected
|
selectedTypes.value = nextSelected
|
||||||
|
|
||||||
|
// Sync with execution error store so the Errors Tab updates immediately
|
||||||
|
if (result.length > 0) {
|
||||||
|
executionErrorStore.removeMissingNodesByType(result)
|
||||||
|
}
|
||||||
|
|
||||||
// Auto-close when all replaceable nodes replaced and no non-replaceable remain
|
// Auto-close when all replaceable nodes replaced and no non-replaceable remain
|
||||||
const allReplaced = replaceableNodes.value.every((n) =>
|
const allReplaced = replaceableNodes.value.every((n) =>
|
||||||
nextReplaced.has(n.label)
|
nextReplaced.has(n.label)
|
||||||
|
|||||||
150
src/components/rightSidePanel/errors/SwapNodeGroupRow.vue
Normal file
150
src/components/rightSidePanel/errors/SwapNodeGroupRow.vue
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-col w-full mb-4">
|
||||||
|
<!-- Type header row: type name + chevron -->
|
||||||
|
<div class="flex h-8 items-center w-full">
|
||||||
|
<p
|
||||||
|
class="flex-1 min-w-0 text-sm font-medium overflow-hidden text-ellipsis whitespace-nowrap text-foreground"
|
||||||
|
>
|
||||||
|
{{ `${group.type} (${group.nodeTypes.length})` }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="textonly"
|
||||||
|
size="icon-sm"
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
'size-8 shrink-0 transition-transform duration-200 hover:bg-transparent',
|
||||||
|
{ 'rotate-180': expanded }
|
||||||
|
)
|
||||||
|
"
|
||||||
|
:aria-label="
|
||||||
|
expanded
|
||||||
|
? t('rightSidePanel.missingNodePacks.collapse', 'Collapse')
|
||||||
|
: t('rightSidePanel.missingNodePacks.expand', 'Expand')
|
||||||
|
"
|
||||||
|
@click="toggleExpand"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
class="icon-[lucide--chevron-down] size-4 text-muted-foreground group-hover:text-base-foreground"
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Sub-labels: individual node instances, each with their own Locate button -->
|
||||||
|
<TransitionCollapse>
|
||||||
|
<div
|
||||||
|
v-if="expanded"
|
||||||
|
class="flex flex-col gap-0.5 pl-2 mb-2 overflow-hidden"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="nodeType in group.nodeTypes"
|
||||||
|
:key="getKey(nodeType)"
|
||||||
|
class="flex h-7 items-center"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-if="
|
||||||
|
showNodeIdBadge &&
|
||||||
|
typeof nodeType !== 'string' &&
|
||||||
|
nodeType.nodeId != null
|
||||||
|
"
|
||||||
|
class="shrink-0 rounded-md bg-secondary-background-selected px-2 py-0.5 text-xs font-mono text-muted-foreground font-bold mr-1"
|
||||||
|
>
|
||||||
|
#{{ nodeType.nodeId }}
|
||||||
|
</span>
|
||||||
|
<p class="flex-1 min-w-0 text-xs text-muted-foreground truncate">
|
||||||
|
{{ getLabel(nodeType) }}
|
||||||
|
</p>
|
||||||
|
<Button
|
||||||
|
v-if="typeof nodeType !== 'string' && nodeType.nodeId != null"
|
||||||
|
variant="textonly"
|
||||||
|
size="icon-sm"
|
||||||
|
class="size-6 text-muted-foreground hover:text-base-foreground shrink-0 mr-1"
|
||||||
|
:aria-label="t('rightSidePanel.locateNode', 'Locate Node')"
|
||||||
|
@click="handleLocateNode(nodeType)"
|
||||||
|
>
|
||||||
|
<i class="icon-[lucide--locate] size-3" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TransitionCollapse>
|
||||||
|
|
||||||
|
<!-- Description rows: what it is replaced by -->
|
||||||
|
<div class="flex flex-col text-[13px] mb-2 mt-1 px-1 gap-0.5">
|
||||||
|
<span class="text-muted-foreground">{{
|
||||||
|
t('nodeReplacement.willBeReplacedBy', 'This node will be replaced by:')
|
||||||
|
}}</span>
|
||||||
|
<span class="font-bold text-foreground">{{
|
||||||
|
group.newNodeId ?? t('nodeReplacement.unknownNode', 'Unknown')
|
||||||
|
}}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Replace Action Button -->
|
||||||
|
<div class="flex items-start w-full pt-1 pb-1">
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
size="md"
|
||||||
|
class="flex flex-1 w-full"
|
||||||
|
@click="handleReplaceNode"
|
||||||
|
>
|
||||||
|
<i class="icon-[lucide--repeat] size-4 text-foreground shrink-0 mr-1" />
|
||||||
|
<span class="text-sm text-foreground truncate min-w-0">
|
||||||
|
{{ t('nodeReplacement.replaceNode', 'Replace Node') }}
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { cn } from '@/utils/tailwindUtil'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import Button from '@/components/ui/button/Button.vue'
|
||||||
|
import TransitionCollapse from '@/components/rightSidePanel/layout/TransitionCollapse.vue'
|
||||||
|
import type { MissingNodeType } from '@/types/comfy'
|
||||||
|
import type { SwapNodeGroup } from './useErrorGroups'
|
||||||
|
import { useNodeReplacement } from '@/platform/nodeReplacement/useNodeReplacement'
|
||||||
|
import { useExecutionErrorStore } from '@/stores/executionErrorStore'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
group: SwapNodeGroup
|
||||||
|
showNodeIdBadge: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'locate-node': [nodeId: string]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
const { replaceNodesInPlace } = useNodeReplacement()
|
||||||
|
const executionErrorStore = useExecutionErrorStore()
|
||||||
|
|
||||||
|
const expanded = ref(false)
|
||||||
|
|
||||||
|
function toggleExpand() {
|
||||||
|
expanded.value = !expanded.value
|
||||||
|
}
|
||||||
|
|
||||||
|
function getKey(nodeType: MissingNodeType): string {
|
||||||
|
if (typeof nodeType === 'string') return nodeType
|
||||||
|
return nodeType.nodeId != null ? String(nodeType.nodeId) : nodeType.type
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLabel(nodeType: MissingNodeType): string {
|
||||||
|
return typeof nodeType === 'string' ? nodeType : nodeType.type
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleLocateNode(nodeType: MissingNodeType) {
|
||||||
|
if (typeof nodeType === 'string') return
|
||||||
|
if (nodeType.nodeId != null) {
|
||||||
|
emit('locate-node', String(nodeType.nodeId))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleReplaceNode() {
|
||||||
|
const replaced = replaceNodesInPlace(props.group.nodeTypes)
|
||||||
|
if (replaced.length > 0) {
|
||||||
|
executionErrorStore.removeMissingNodesByType([props.group.type])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
38
src/components/rightSidePanel/errors/SwapNodesCard.vue
Normal file
38
src/components/rightSidePanel/errors/SwapNodesCard.vue
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<template>
|
||||||
|
<div class="px-4 pb-2 mt-2">
|
||||||
|
<!-- Sub-label: guidance message shown above all swap groups -->
|
||||||
|
<p class="m-0 pb-5 text-sm text-muted-foreground leading-relaxed">
|
||||||
|
{{
|
||||||
|
t(
|
||||||
|
'nodeReplacement.swapNodesGuide',
|
||||||
|
'The following nodes can be automatically replaced with compatible alternatives.'
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</p>
|
||||||
|
<!-- Group Rows -->
|
||||||
|
<SwapNodeGroupRow
|
||||||
|
v-for="group in swapNodeGroups"
|
||||||
|
:key="group.type"
|
||||||
|
:group="group"
|
||||||
|
:show-node-id-badge="showNodeIdBadge"
|
||||||
|
@locate-node="emit('locate-node', $event)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import type { SwapNodeGroup } from './useErrorGroups'
|
||||||
|
import SwapNodeGroupRow from './SwapNodeGroupRow.vue'
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
const { swapNodeGroups, showNodeIdBadge } = defineProps<{
|
||||||
|
swapNodeGroups: SwapNodeGroup[]
|
||||||
|
showNodeIdBadge: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'locate-node': [nodeId: string]
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
@@ -27,7 +27,11 @@
|
|||||||
:key="group.title"
|
:key="group.title"
|
||||||
:collapse="collapseState[group.title] ?? false"
|
:collapse="collapseState[group.title] ?? false"
|
||||||
class="border-b border-interface-stroke"
|
class="border-b border-interface-stroke"
|
||||||
:size="group.type === 'missing_node' ? 'lg' : 'default'"
|
:size="
|
||||||
|
group.type === 'missing_node' || group.type === 'swap_nodes'
|
||||||
|
? 'lg'
|
||||||
|
: 'default'
|
||||||
|
"
|
||||||
@update:collapse="collapseState[group.title] = $event"
|
@update:collapse="collapseState[group.title] = $event"
|
||||||
>
|
>
|
||||||
<template #label>
|
<template #label>
|
||||||
@@ -40,7 +44,9 @@
|
|||||||
{{
|
{{
|
||||||
group.type === 'missing_node'
|
group.type === 'missing_node'
|
||||||
? `${group.title} (${missingPackGroups.length})`
|
? `${group.title} (${missingPackGroups.length})`
|
||||||
: group.title
|
: group.type === 'swap_nodes'
|
||||||
|
? `${group.title} (${swapNodeGroups.length})`
|
||||||
|
: group.title
|
||||||
}}
|
}}
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
@@ -69,6 +75,21 @@
|
|||||||
: t('rightSidePanel.missingNodePacks.installAll')
|
: t('rightSidePanel.missingNodePacks.installAll')
|
||||||
}}
|
}}
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
v-else-if="group.type === 'swap_nodes'"
|
||||||
|
v-tooltip.top="
|
||||||
|
t(
|
||||||
|
'nodeReplacement.replaceAllWarning',
|
||||||
|
'Replaces all available nodes in this group.'
|
||||||
|
)
|
||||||
|
"
|
||||||
|
variant="secondary"
|
||||||
|
size="sm"
|
||||||
|
class="shrink-0 mr-2 h-8 rounded-lg text-sm"
|
||||||
|
@click.stop="handleReplaceAll()"
|
||||||
|
>
|
||||||
|
{{ t('nodeReplacement.replaceAll', 'Replace All') }}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -82,8 +103,16 @@
|
|||||||
@open-manager-info="handleOpenManagerInfo"
|
@open-manager-info="handleOpenManagerInfo"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<!-- Swap Nodes -->
|
||||||
|
<SwapNodesCard
|
||||||
|
v-else-if="group.type === 'swap_nodes'"
|
||||||
|
:swap-node-groups="swapNodeGroups"
|
||||||
|
:show-node-id-badge="showNodeIdBadge"
|
||||||
|
@locate-node="handleLocateMissingNode"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- Execution Errors -->
|
<!-- Execution Errors -->
|
||||||
<div v-else class="px-4 space-y-3">
|
<div v-else-if="group.type === 'execution'" class="px-4 space-y-3">
|
||||||
<ErrorNodeCard
|
<ErrorNodeCard
|
||||||
v-for="card in group.cards"
|
v-for="card in group.cards"
|
||||||
:key="card.id"
|
:key="card.id"
|
||||||
@@ -150,11 +179,14 @@ import PropertiesAccordionItem from '../layout/PropertiesAccordionItem.vue'
|
|||||||
import FormSearchInput from '@/renderer/extensions/vueNodes/widgets/components/form/FormSearchInput.vue'
|
import FormSearchInput from '@/renderer/extensions/vueNodes/widgets/components/form/FormSearchInput.vue'
|
||||||
import ErrorNodeCard from './ErrorNodeCard.vue'
|
import ErrorNodeCard from './ErrorNodeCard.vue'
|
||||||
import MissingNodeCard from './MissingNodeCard.vue'
|
import MissingNodeCard from './MissingNodeCard.vue'
|
||||||
|
import SwapNodesCard from './SwapNodesCard.vue'
|
||||||
import Button from '@/components/ui/button/Button.vue'
|
import Button from '@/components/ui/button/Button.vue'
|
||||||
import DotSpinner from '@/components/common/DotSpinner.vue'
|
import DotSpinner from '@/components/common/DotSpinner.vue'
|
||||||
import { usePackInstall } from '@/workbench/extensions/manager/composables/nodePack/usePackInstall'
|
import { usePackInstall } from '@/workbench/extensions/manager/composables/nodePack/usePackInstall'
|
||||||
import { useMissingNodes } from '@/workbench/extensions/manager/composables/nodePack/useMissingNodes'
|
import { useMissingNodes } from '@/workbench/extensions/manager/composables/nodePack/useMissingNodes'
|
||||||
import { useErrorGroups } from './useErrorGroups'
|
import { useErrorGroups } from './useErrorGroups'
|
||||||
|
import { useNodeReplacement } from '@/platform/nodeReplacement/useNodeReplacement'
|
||||||
|
import { useExecutionErrorStore } from '@/stores/executionErrorStore'
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const { copyToClipboard } = useCopyToClipboard()
|
const { copyToClipboard } = useCopyToClipboard()
|
||||||
@@ -167,6 +199,8 @@ const { shouldShowManagerButtons, shouldShowInstallButton, openManager } =
|
|||||||
const { missingNodePacks } = useMissingNodes()
|
const { missingNodePacks } = useMissingNodes()
|
||||||
const { isInstalling: isInstallingAll, installAllPacks: installAll } =
|
const { isInstalling: isInstallingAll, installAllPacks: installAll } =
|
||||||
usePackInstall(() => missingNodePacks.value)
|
usePackInstall(() => missingNodePacks.value)
|
||||||
|
const { replaceNodesInPlace } = useNodeReplacement()
|
||||||
|
const executionErrorStore = useExecutionErrorStore()
|
||||||
|
|
||||||
const searchQuery = ref('')
|
const searchQuery = ref('')
|
||||||
|
|
||||||
@@ -183,7 +217,8 @@ const {
|
|||||||
isSingleNodeSelected,
|
isSingleNodeSelected,
|
||||||
errorNodeCache,
|
errorNodeCache,
|
||||||
missingNodeCache,
|
missingNodeCache,
|
||||||
missingPackGroups
|
missingPackGroups,
|
||||||
|
swapNodeGroups
|
||||||
} = useErrorGroups(searchQuery, t)
|
} = useErrorGroups(searchQuery, t)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -229,6 +264,14 @@ function handleOpenManagerInfo(packId: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleReplaceAll() {
|
||||||
|
const allNodeTypes = swapNodeGroups.value.flatMap((g) => g.nodeTypes)
|
||||||
|
const replaced = replaceNodesInPlace(allNodeTypes)
|
||||||
|
if (replaced.length > 0) {
|
||||||
|
executionErrorStore.removeMissingNodesByType(replaced)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function handleEnterSubgraph(nodeId: string) {
|
function handleEnterSubgraph(nodeId: string) {
|
||||||
enterSubgraph(nodeId, errorNodeCache.value)
|
enterSubgraph(nodeId, errorNodeCache.value)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,3 +22,4 @@ export type ErrorGroup =
|
|||||||
priority: number
|
priority: number
|
||||||
}
|
}
|
||||||
| { type: 'missing_node'; title: string; priority: number }
|
| { type: 'missing_node'; title: string; priority: number }
|
||||||
|
| { type: 'swap_nodes'; title: string; priority: number }
|
||||||
|
|||||||
@@ -42,6 +42,12 @@ export interface MissingPackGroup {
|
|||||||
isResolving: boolean
|
isResolving: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SwapNodeGroup {
|
||||||
|
type: string
|
||||||
|
newNodeId: string | undefined
|
||||||
|
nodeTypes: MissingNodeType[]
|
||||||
|
}
|
||||||
|
|
||||||
interface GroupEntry {
|
interface GroupEntry {
|
||||||
type: 'execution'
|
type: 'execution'
|
||||||
priority: number
|
priority: number
|
||||||
@@ -444,6 +450,8 @@ export function useErrorGroups(
|
|||||||
const resolvingKeys = new Set<string | null>()
|
const resolvingKeys = new Set<string | null>()
|
||||||
|
|
||||||
for (const nodeType of nodeTypes) {
|
for (const nodeType of nodeTypes) {
|
||||||
|
if (typeof nodeType !== 'string' && nodeType.isReplaceable) continue
|
||||||
|
|
||||||
let packId: string | null
|
let packId: string | null
|
||||||
|
|
||||||
if (typeof nodeType === 'string') {
|
if (typeof nodeType === 'string') {
|
||||||
@@ -495,18 +503,53 @@ export function useErrorGroups(
|
|||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const swapNodeGroups = computed<SwapNodeGroup[]>(() => {
|
||||||
|
const nodeTypes = executionErrorStore.missingNodesError?.nodeTypes ?? []
|
||||||
|
const map = new Map<string, SwapNodeGroup>()
|
||||||
|
|
||||||
|
for (const nodeType of nodeTypes) {
|
||||||
|
if (typeof nodeType === 'string' || !nodeType.isReplaceable) continue
|
||||||
|
|
||||||
|
const typeName = nodeType.type
|
||||||
|
const existing = map.get(typeName)
|
||||||
|
if (existing) {
|
||||||
|
existing.nodeTypes.push(nodeType)
|
||||||
|
} else {
|
||||||
|
map.set(typeName, {
|
||||||
|
type: typeName,
|
||||||
|
newNodeId: nodeType.replacement?.new_node_id,
|
||||||
|
nodeTypes: [nodeType]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(map.values()).sort((a, b) => a.type.localeCompare(b.type))
|
||||||
|
})
|
||||||
|
|
||||||
/** Builds an ErrorGroup from missingNodesError. Returns [] when none present. */
|
/** Builds an ErrorGroup from missingNodesError. Returns [] when none present. */
|
||||||
function buildMissingNodeGroups(): ErrorGroup[] {
|
function buildMissingNodeGroups(): ErrorGroup[] {
|
||||||
const error = executionErrorStore.missingNodesError
|
const error = executionErrorStore.missingNodesError
|
||||||
if (!error) return []
|
if (!error) return []
|
||||||
|
|
||||||
return [
|
const groups: ErrorGroup[] = []
|
||||||
{
|
|
||||||
|
if (swapNodeGroups.value.length > 0) {
|
||||||
|
groups.push({
|
||||||
|
type: 'swap_nodes' as const,
|
||||||
|
title: st('nodeReplacement.swapNodesTitle', 'Swap Nodes'),
|
||||||
|
priority: 0
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (missingPackGroups.value.length > 0) {
|
||||||
|
groups.push({
|
||||||
type: 'missing_node' as const,
|
type: 'missing_node' as const,
|
||||||
title: error.message,
|
title: error.message,
|
||||||
priority: 0
|
priority: 1
|
||||||
}
|
})
|
||||||
]
|
}
|
||||||
|
|
||||||
|
return groups.sort((a, b) => a.priority - b.priority)
|
||||||
}
|
}
|
||||||
|
|
||||||
const allErrorGroups = computed<ErrorGroup[]>(() => {
|
const allErrorGroups = computed<ErrorGroup[]>(() => {
|
||||||
@@ -564,6 +607,7 @@ export function useErrorGroups(
|
|||||||
errorNodeCache,
|
errorNodeCache,
|
||||||
missingNodeCache,
|
missingNodeCache,
|
||||||
groupedErrorMessages,
|
groupedErrorMessages,
|
||||||
missingPackGroups
|
missingPackGroups,
|
||||||
|
swapNodeGroups
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import type {
|
|||||||
} from '@/lib/litegraph/src/interfaces'
|
} from '@/lib/litegraph/src/interfaces'
|
||||||
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
|
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
|
||||||
import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations'
|
import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations'
|
||||||
|
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
|
||||||
import { LayoutSource } from '@/renderer/core/layout/types'
|
import { LayoutSource } from '@/renderer/core/layout/types'
|
||||||
import type { NodeId } from '@/renderer/core/layout/types'
|
import type { NodeId } from '@/renderer/core/layout/types'
|
||||||
import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||||
@@ -442,6 +443,11 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager {
|
|||||||
const nodePosition = { x: node.pos[0], y: node.pos[1] }
|
const nodePosition = { x: node.pos[0], y: node.pos[1] }
|
||||||
const nodeSize = { width: node.size[0], height: node.size[1] }
|
const nodeSize = { width: node.size[0], height: node.size[1] }
|
||||||
|
|
||||||
|
// Skip layout creation if it already exists
|
||||||
|
// (e.g. in-place node replacement where the old node's layout is reused for the new node with the same ID).
|
||||||
|
const existingLayout = layoutStore.getNodeLayoutRef(id).value
|
||||||
|
if (existingLayout) return
|
||||||
|
|
||||||
// Add node to layout store with final positions
|
// Add node to layout store with final positions
|
||||||
setSource(LayoutSource.Canvas)
|
setSource(LayoutSource.Canvas)
|
||||||
void createNode(id, {
|
void createNode(id, {
|
||||||
|
|||||||
44
src/composables/useMissingNodeScan.ts
Normal file
44
src/composables/useMissingNodeScan.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
|
||||||
|
import type { LGraph } from '@/lib/litegraph/src/litegraph'
|
||||||
|
import { useNodeReplacementStore } from '@/platform/nodeReplacement/nodeReplacementStore'
|
||||||
|
import { useExecutionErrorStore } from '@/stores/executionErrorStore'
|
||||||
|
import type { MissingNodeType } from '@/types/comfy'
|
||||||
|
import {
|
||||||
|
collectAllNodes,
|
||||||
|
getExecutionIdByNode
|
||||||
|
} from '@/utils/graphTraversalUtil'
|
||||||
|
import { getCnrIdFromNode } from '@/workbench/extensions/manager/utils/missingNodeErrorUtil'
|
||||||
|
|
||||||
|
/** Scan the live graph for unregistered node types and build a full MissingNodeType list. */
|
||||||
|
function scanMissingNodes(rootGraph: LGraph): MissingNodeType[] {
|
||||||
|
const nodeReplacementStore = useNodeReplacementStore()
|
||||||
|
const missingNodeTypes: MissingNodeType[] = []
|
||||||
|
|
||||||
|
const allNodes = collectAllNodes(rootGraph)
|
||||||
|
|
||||||
|
for (const node of allNodes) {
|
||||||
|
const originalType = node.last_serialization?.type ?? node.type ?? 'Unknown'
|
||||||
|
|
||||||
|
if (originalType in LiteGraph.registered_node_types) continue
|
||||||
|
|
||||||
|
const cnrId = getCnrIdFromNode(node)
|
||||||
|
const replacement = nodeReplacementStore.getReplacementFor(originalType)
|
||||||
|
const executionId = getExecutionIdByNode(rootGraph, node)
|
||||||
|
|
||||||
|
missingNodeTypes.push({
|
||||||
|
type: originalType,
|
||||||
|
nodeId: executionId ?? String(node.id),
|
||||||
|
cnrId,
|
||||||
|
isReplaceable: replacement !== null,
|
||||||
|
replacement: replacement ?? undefined
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return missingNodeTypes
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Re-scan the graph for missing nodes and update the error store. */
|
||||||
|
export function rescanAndSurfaceMissingNodes(rootGraph: LGraph): void {
|
||||||
|
const types = scanMissingNodes(rootGraph)
|
||||||
|
useExecutionErrorStore().surfaceMissingNodes(types)
|
||||||
|
}
|
||||||
@@ -3073,7 +3073,14 @@
|
|||||||
"openNodeManager": "Open Node Manager",
|
"openNodeManager": "Open Node Manager",
|
||||||
"skipForNow": "Skip for Now",
|
"skipForNow": "Skip for Now",
|
||||||
"installMissingNodes": "Install Missing Nodes",
|
"installMissingNodes": "Install Missing Nodes",
|
||||||
"replaceWarning": "This will permanently modify the workflow. Save a copy first if unsure."
|
"replaceWarning": "This will permanently modify the workflow. Save a copy first if unsure.",
|
||||||
|
"swapNodesGuide": "The following nodes can be automatically replaced with compatible alternatives.",
|
||||||
|
"willBeReplacedBy": "This node will be replaced by:",
|
||||||
|
"replaceNode": "Replace Node",
|
||||||
|
"replaceAll": "Replace All",
|
||||||
|
"unknownNode": "Unknown",
|
||||||
|
"replaceAllWarning": "Replaces all available nodes in this group.",
|
||||||
|
"swapNodesTitle": "Swap Nodes"
|
||||||
},
|
},
|
||||||
"rightSidePanel": {
|
"rightSidePanel": {
|
||||||
"togglePanel": "Toggle properties panel",
|
"togglePanel": "Toggle properties panel",
|
||||||
|
|||||||
@@ -229,7 +229,12 @@ export function useNodeReplacement() {
|
|||||||
try {
|
try {
|
||||||
const placeholders = collectAllNodes(
|
const placeholders = collectAllNodes(
|
||||||
graph,
|
graph,
|
||||||
(n) => !!n.has_errors && !!n.last_serialization
|
(n) =>
|
||||||
|
!!n.last_serialization &&
|
||||||
|
!(
|
||||||
|
(n.last_serialization.type ?? n.type ?? '') in
|
||||||
|
LiteGraph.registered_node_types
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
for (const node of placeholders) {
|
for (const node of placeholders) {
|
||||||
@@ -261,6 +266,10 @@ export function useNodeReplacement() {
|
|||||||
}
|
}
|
||||||
replaceWithMapping(node, newNode, effectiveReplacement, nodeGraph, idx)
|
replaceWithMapping(node, newNode, effectiveReplacement, nodeGraph, idx)
|
||||||
|
|
||||||
|
// Refresh Vue node data — replaceWithMapping bypasses graph.add()
|
||||||
|
// so onNodeAdded must be called explicitly to update VueNodeData.
|
||||||
|
nodeGraph.onNodeAdded?.(newNode)
|
||||||
|
|
||||||
if (!replacedTypes.includes(match.type)) {
|
if (!replacedTypes.includes(match.type)) {
|
||||||
replacedTypes.push(match.type)
|
replacedTypes.push(match.type)
|
||||||
}
|
}
|
||||||
@@ -279,6 +288,19 @@ export function useNodeReplacement() {
|
|||||||
life: 3000
|
life: 3000
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to replace nodes:', error)
|
||||||
|
if (replacedTypes.length > 0) {
|
||||||
|
graph.updateExecutionOrder()
|
||||||
|
graph.setDirtyCanvas(true, true)
|
||||||
|
}
|
||||||
|
toastStore.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: t('g.error', 'Error'),
|
||||||
|
detail: t('nodeReplacement.replaceFailed', 'Failed to replace nodes'),
|
||||||
|
life: 5000
|
||||||
|
})
|
||||||
|
return replacedTypes
|
||||||
} finally {
|
} finally {
|
||||||
changeTracker?.afterChange()
|
changeTracker?.afterChange()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,12 +79,8 @@ import type { ComfyExtension, MissingNodeType } from '@/types/comfy'
|
|||||||
import type { ExtensionManager } from '@/types/extensionTypes'
|
import type { ExtensionManager } from '@/types/extensionTypes'
|
||||||
import type { NodeExecutionId } from '@/types/nodeIdentification'
|
import type { NodeExecutionId } from '@/types/nodeIdentification'
|
||||||
import { graphToPrompt } from '@/utils/executionUtil'
|
import { graphToPrompt } from '@/utils/executionUtil'
|
||||||
import type { MissingNodeTypeExtraInfo } from '@/workbench/extensions/manager/types/missingNodeErrorTypes'
|
import { getCnrIdFromProperties } from '@/workbench/extensions/manager/utils/missingNodeErrorUtil'
|
||||||
import {
|
import { rescanAndSurfaceMissingNodes } from '@/composables/useMissingNodeScan'
|
||||||
createMissingNodeTypeFromError,
|
|
||||||
getCnrIdFromNode,
|
|
||||||
getCnrIdFromProperties
|
|
||||||
} from '@/workbench/extensions/manager/utils/missingNodeErrorUtil'
|
|
||||||
import { anyItemOverlapsRect } from '@/utils/mathUtil'
|
import { anyItemOverlapsRect } from '@/utils/mathUtil'
|
||||||
import {
|
import {
|
||||||
collectAllNodes,
|
collectAllNodes,
|
||||||
@@ -1190,7 +1186,7 @@ export class ComfyApp {
|
|||||||
const embeddedModels: ModelFile[] = []
|
const embeddedModels: ModelFile[] = []
|
||||||
|
|
||||||
const nodeReplacementStore = useNodeReplacementStore()
|
const nodeReplacementStore = useNodeReplacementStore()
|
||||||
|
await nodeReplacementStore.load()
|
||||||
const collectMissingNodesAndModels = (
|
const collectMissingNodesAndModels = (
|
||||||
nodes: ComfyWorkflowJSON['nodes'],
|
nodes: ComfyWorkflowJSON['nodes'],
|
||||||
pathPrefix: string = '',
|
pathPrefix: string = '',
|
||||||
@@ -1527,35 +1523,8 @@ export class ComfyApp {
|
|||||||
typeof error.response.error === 'object' &&
|
typeof error.response.error === 'object' &&
|
||||||
error.response.error?.type === 'missing_node_type'
|
error.response.error?.type === 'missing_node_type'
|
||||||
) {
|
) {
|
||||||
const extraInfo = (error.response.error.extra_info ??
|
// Re-scan the full graph instead of using the server's single-node response.
|
||||||
{}) as MissingNodeTypeExtraInfo
|
rescanAndSurfaceMissingNodes(this.rootGraph)
|
||||||
|
|
||||||
let graphNode = null
|
|
||||||
if (extraInfo.node_id && this.rootGraph) {
|
|
||||||
graphNode = getNodeByExecutionId(
|
|
||||||
this.rootGraph,
|
|
||||||
extraInfo.node_id
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const enrichedExtraInfo: MissingNodeTypeExtraInfo = {
|
|
||||||
...extraInfo,
|
|
||||||
class_type: extraInfo.class_type ?? graphNode?.type,
|
|
||||||
node_title: extraInfo.node_title ?? graphNode?.title
|
|
||||||
}
|
|
||||||
|
|
||||||
const missingNodeType =
|
|
||||||
createMissingNodeTypeFromError(enrichedExtraInfo)
|
|
||||||
|
|
||||||
if (
|
|
||||||
graphNode &&
|
|
||||||
typeof missingNodeType !== 'string' &&
|
|
||||||
!missingNodeType.cnrId
|
|
||||||
) {
|
|
||||||
missingNodeType.cnrId = getCnrIdFromNode(graphNode)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.showMissingNodesError([missingNodeType])
|
|
||||||
} else if (
|
} else if (
|
||||||
!useSettingStore().get('Comfy.RightSidePanel.ShowErrorsTab') ||
|
!useSettingStore().get('Comfy.RightSidePanel.ShowErrorsTab') ||
|
||||||
!(error instanceof PromptExecutionError)
|
!(error instanceof PromptExecutionError)
|
||||||
|
|||||||
@@ -112,6 +112,17 @@ export const useExecutionErrorStore = defineStore('executionError', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Remove specific node types from the missing nodes list (e.g. after replacement). */
|
||||||
|
function removeMissingNodesByType(typesToRemove: string[]) {
|
||||||
|
if (!missingNodesError.value) return
|
||||||
|
const removeSet = new Set(typesToRemove)
|
||||||
|
const remaining = missingNodesError.value.nodeTypes.filter((node) => {
|
||||||
|
const nodeType = typeof node === 'string' ? node : node.type
|
||||||
|
return !removeSet.has(nodeType)
|
||||||
|
})
|
||||||
|
setMissingNodeTypes(remaining)
|
||||||
|
}
|
||||||
|
|
||||||
function setMissingNodeTypes(types: MissingNodeType[]) {
|
function setMissingNodeTypes(types: MissingNodeType[]) {
|
||||||
if (!types.length) {
|
if (!types.length) {
|
||||||
missingNodesError.value = null
|
missingNodesError.value = null
|
||||||
@@ -406,6 +417,7 @@ export const useExecutionErrorStore = defineStore('executionError', () => {
|
|||||||
// Missing node actions
|
// Missing node actions
|
||||||
setMissingNodeTypes,
|
setMissingNodeTypes,
|
||||||
surfaceMissingNodes,
|
surfaceMissingNodes,
|
||||||
|
removeMissingNodesByType,
|
||||||
|
|
||||||
// Lookup helpers
|
// Lookup helpers
|
||||||
getNodeErrors,
|
getNodeErrors,
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
/**
|
|
||||||
* Extra info returned by the backend for missing_node_type errors
|
|
||||||
* from the /prompt endpoint validation.
|
|
||||||
*/
|
|
||||||
export interface MissingNodeTypeExtraInfo {
|
|
||||||
class_type?: string | null
|
|
||||||
node_title?: string | null
|
|
||||||
node_id?: string
|
|
||||||
}
|
|
||||||
@@ -1,93 +1,70 @@
|
|||||||
import { describe, expect, it } from 'vitest'
|
import { describe, expect, it } from 'vitest'
|
||||||
|
|
||||||
|
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
buildMissingNodeHint,
|
getCnrIdFromProperties,
|
||||||
createMissingNodeTypeFromError
|
getCnrIdFromNode
|
||||||
} from './missingNodeErrorUtil'
|
} from './missingNodeErrorUtil'
|
||||||
|
|
||||||
describe('buildMissingNodeHint', () => {
|
describe('getCnrIdFromProperties', () => {
|
||||||
it('returns hint with title and node ID when both available', () => {
|
it('returns cnr_id when present', () => {
|
||||||
expect(buildMissingNodeHint('My Node', 'MyNodeClass', '42')).toBe(
|
expect(getCnrIdFromProperties({ cnr_id: 'my-pack' })).toBe('my-pack')
|
||||||
'"My Node" (Node ID #42)'
|
})
|
||||||
|
|
||||||
|
it('returns aux_id when cnr_id is absent', () => {
|
||||||
|
expect(getCnrIdFromProperties({ aux_id: 'my-aux-pack' })).toBe(
|
||||||
|
'my-aux-pack'
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('returns hint with title only when no node ID', () => {
|
it('prefers cnr_id over aux_id', () => {
|
||||||
expect(buildMissingNodeHint('My Node', 'MyNodeClass', undefined)).toBe(
|
|
||||||
'"My Node"'
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('returns hint with node ID only when title matches class type', () => {
|
|
||||||
expect(buildMissingNodeHint('MyNodeClass', 'MyNodeClass', '42')).toBe(
|
|
||||||
'Node ID #42'
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('returns undefined when title matches class type and no node ID', () => {
|
|
||||||
expect(
|
expect(
|
||||||
buildMissingNodeHint('MyNodeClass', 'MyNodeClass', undefined)
|
getCnrIdFromProperties({ cnr_id: 'primary', aux_id: 'secondary' })
|
||||||
).toBeUndefined()
|
).toBe('primary')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('returns undefined when title is null and no node ID', () => {
|
it('returns undefined when neither is present', () => {
|
||||||
expect(buildMissingNodeHint(null, 'MyNodeClass', undefined)).toBeUndefined()
|
expect(getCnrIdFromProperties({})).toBeUndefined()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('returns node ID hint when title is null but node ID exists', () => {
|
it('returns undefined for null properties', () => {
|
||||||
expect(buildMissingNodeHint(null, 'MyNodeClass', '42')).toBe('Node ID #42')
|
expect(getCnrIdFromProperties(null)).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns undefined for undefined properties', () => {
|
||||||
|
expect(getCnrIdFromProperties(undefined)).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns undefined when cnr_id is not a string', () => {
|
||||||
|
expect(getCnrIdFromProperties({ cnr_id: 123 })).toBeUndefined()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('createMissingNodeTypeFromError', () => {
|
describe('getCnrIdFromNode', () => {
|
||||||
it('returns string type when no hint is generated', () => {
|
it('returns cnr_id from node properties', () => {
|
||||||
const result = createMissingNodeTypeFromError({
|
const node = {
|
||||||
class_type: 'MyNodeClass',
|
properties: { cnr_id: 'node-pack' }
|
||||||
node_title: 'MyNodeClass'
|
} as unknown as LGraphNode
|
||||||
})
|
expect(getCnrIdFromNode(node)).toBe('node-pack')
|
||||||
expect(result).toBe('MyNodeClass')
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('returns object with hint when title differs from class type', () => {
|
it('returns aux_id when cnr_id is absent', () => {
|
||||||
const result = createMissingNodeTypeFromError({
|
const node = {
|
||||||
class_type: 'MyNodeClass',
|
properties: { aux_id: 'node-aux-pack' }
|
||||||
node_title: 'My Custom Title',
|
} as unknown as LGraphNode
|
||||||
node_id: '42'
|
expect(getCnrIdFromNode(node)).toBe('node-aux-pack')
|
||||||
})
|
|
||||||
expect(result).toEqual({
|
|
||||||
type: 'MyNodeClass',
|
|
||||||
nodeId: '42',
|
|
||||||
hint: '"My Custom Title" (Node ID #42)'
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('handles null class_type by defaulting to Unknown', () => {
|
it('prefers cnr_id over aux_id in node properties', () => {
|
||||||
const result = createMissingNodeTypeFromError({
|
const node = {
|
||||||
class_type: null,
|
properties: { cnr_id: 'primary', aux_id: 'secondary' }
|
||||||
node_title: 'Some Title',
|
} as unknown as LGraphNode
|
||||||
node_id: '42'
|
expect(getCnrIdFromNode(node)).toBe('primary')
|
||||||
})
|
|
||||||
expect(result).toEqual({
|
|
||||||
type: 'Unknown',
|
|
||||||
nodeId: '42',
|
|
||||||
hint: '"Some Title" (Node ID #42)'
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('handles empty extra_info', () => {
|
it('returns undefined when node has no cnr_id or aux_id', () => {
|
||||||
const result = createMissingNodeTypeFromError({})
|
const node = { properties: {} } as unknown as LGraphNode
|
||||||
expect(result).toBe('Unknown')
|
expect(getCnrIdFromNode(node)).toBeUndefined()
|
||||||
})
|
|
||||||
|
|
||||||
it('returns object with node ID hint when only node_id is available', () => {
|
|
||||||
const result = createMissingNodeTypeFromError({
|
|
||||||
class_type: 'MyNodeClass',
|
|
||||||
node_id: '123'
|
|
||||||
})
|
|
||||||
expect(result).toEqual({
|
|
||||||
type: 'MyNodeClass',
|
|
||||||
nodeId: '123',
|
|
||||||
hint: 'Node ID #123'
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,48 +1,4 @@
|
|||||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||||
import type { MissingNodeType } from '@/types/comfy'
|
|
||||||
|
|
||||||
import type { MissingNodeTypeExtraInfo } from '../types/missingNodeErrorTypes'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Builds a hint string from missing node metadata.
|
|
||||||
* Provides context about which node is missing (title, ID) when available.
|
|
||||||
*/
|
|
||||||
export function buildMissingNodeHint(
|
|
||||||
nodeTitle: string | null | undefined,
|
|
||||||
classType: string,
|
|
||||||
nodeId: string | undefined
|
|
||||||
): string | undefined {
|
|
||||||
const hasTitle = nodeTitle && nodeTitle !== classType
|
|
||||||
if (hasTitle && nodeId) {
|
|
||||||
return `"${nodeTitle}" (Node ID #${nodeId})`
|
|
||||||
} else if (hasTitle) {
|
|
||||||
return `"${nodeTitle}"`
|
|
||||||
} else if (nodeId) {
|
|
||||||
return `Node ID #${nodeId}`
|
|
||||||
}
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a MissingNodeType from backend error extra_info.
|
|
||||||
* Used when the /prompt endpoint returns a missing_node_type error.
|
|
||||||
*/
|
|
||||||
export function createMissingNodeTypeFromError(
|
|
||||||
extraInfo: MissingNodeTypeExtraInfo
|
|
||||||
): MissingNodeType {
|
|
||||||
const classType = extraInfo.class_type ?? 'Unknown'
|
|
||||||
const nodeTitle = extraInfo.node_title ?? classType
|
|
||||||
const hint = buildMissingNodeHint(nodeTitle, classType, extraInfo.node_id)
|
|
||||||
|
|
||||||
if (hint) {
|
|
||||||
return {
|
|
||||||
type: classType,
|
|
||||||
...(extraInfo.node_id ? { nodeId: extraInfo.node_id } : {}),
|
|
||||||
...(hint ? { hint } : {})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return classType
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extracts the custom node registry ID (cnr_id or aux_id) from a raw
|
* Extracts the custom node registry ID (cnr_id or aux_id) from a raw
|
||||||
|
|||||||
Reference in New Issue
Block a user