mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-14 01:20:03 +00:00
## Summary Fixes a bug where socket map data was not properly removed when sockets are dynamically added/removed via DynamicCombo widgets in Vue mode (Nodes 2.0). ## Problem When DynamicCombo widgets (e.g., `should_remesh` on Meshy nodes) change their selection, inputs are dynamically added/removed. The Vue `v-for` loop in `NodeSlots.vue` was using array index as the `:key`, causing Vue to **reuse** slot components instead of properly unmounting them. This led to: - Socket map entries leaking (never cleaned up) - Socket positions becoming desynced - Stale cached offset data ## Solution 1. **Use slot `name` as Vue key** instead of array index in `NodeSlots.vue` - Slot names are unique per node (enforced by ComfyUI backend) - When a slot is removed, Vue sees the key disappear and properly unmounts the component - `onUnmounted` cleanup in `useSlotElementTracking` now runs correctly 2. **Add defensive cleanup** in `useSlotElementTracking.ts` - Before registering a new slot, check if a stale entry exists with the same key - Clean up stale entry to handle any edge cases ## Related - Fixes COM-12970 - Related to #7837 (fixed LiteGraph version of this bug, but not Vue mode) ## Testing - Quality checks pass (typecheck, lint, format) - Manual testing with DynamicCombo nodes (Meshy, nodes_logic) recommended ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-8469-fix-Vue-mode-socket-map-data-not-cleaned-up-on-dynamic-input-changes-2f86d73d365081e599eadca4f15e6b6e) by [Unito](https://www.unito.io) --------- Co-authored-by: Subagent 5 <subagent@example.com> Co-authored-by: Amp <amp@ampcode.com>
104 lines
2.9 KiB
Vue
104 lines
2.9 KiB
Vue
<template>
|
|
<div v-if="renderError" class="node-error p-2 text-sm text-red-500">
|
|
{{ st('nodeErrors.slots', 'Node Slots Error') }}
|
|
</div>
|
|
<div v-else :class="cn('flex justify-between min-w-0', unifiedWrapperClass)">
|
|
<div
|
|
v-if="filteredInputs.length"
|
|
:class="cn('flex flex-col min-w-0', unifiedDotsClass)"
|
|
>
|
|
<InputSlot
|
|
v-for="(input, index) in filteredInputs"
|
|
:key="`input-${input.name}`"
|
|
:slot-data="input"
|
|
:node-type="nodeData?.type || ''"
|
|
:node-id="nodeData?.id != null ? String(nodeData.id) : ''"
|
|
:index="getActualInputIndex(input, index)"
|
|
/>
|
|
</div>
|
|
|
|
<div
|
|
v-if="nodeData?.outputs?.length"
|
|
:class="cn('ml-auto flex flex-col min-w-0', unifiedDotsClass)"
|
|
>
|
|
<OutputSlot
|
|
v-for="(output, index) in nodeData.outputs"
|
|
:key="`output-${output.name}`"
|
|
:slot-data="output"
|
|
:node-type="nodeData?.type || ''"
|
|
:node-id="nodeData?.id != null ? String(nodeData.id) : ''"
|
|
:index="index"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { computed, onErrorCaptured, ref } from 'vue'
|
|
|
|
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
|
|
import { useErrorHandling } from '@/composables/useErrorHandling'
|
|
import { st } from '@/i18n'
|
|
import type { INodeSlot } from '@/lib/litegraph/src/litegraph'
|
|
import {
|
|
linkedWidgetedInputs,
|
|
nonWidgetedInputs
|
|
} from '@/renderer/extensions/vueNodes/utils/nodeDataUtils'
|
|
import { cn } from '@/utils/tailwindUtil'
|
|
|
|
import InputSlot from './InputSlot.vue'
|
|
import OutputSlot from './OutputSlot.vue'
|
|
|
|
interface NodeSlotsProps {
|
|
nodeData: VueNodeData
|
|
unified?: boolean
|
|
}
|
|
|
|
const { nodeData, unified = false } = defineProps<NodeSlotsProps>()
|
|
|
|
const linkedWidgetInputs = computed(() =>
|
|
unified ? linkedWidgetedInputs(nodeData) : []
|
|
)
|
|
|
|
const filteredInputs = computed(() => [
|
|
...nonWidgetedInputs(nodeData),
|
|
...linkedWidgetInputs.value
|
|
])
|
|
|
|
const unifiedWrapperClass = computed((): string =>
|
|
cn(
|
|
unified &&
|
|
'absolute inset-0 items-center pointer-events-none opacity-0 z-30'
|
|
)
|
|
)
|
|
const unifiedDotsClass = computed((): string =>
|
|
cn(
|
|
unified &&
|
|
'grid grid-cols-1 grid-rows-1 gap-0 [&>*]:row-span-full [&>*]:col-span-full place-items-center'
|
|
)
|
|
)
|
|
|
|
// Get the actual index of an input slot in the node's inputs array
|
|
// (accounting for filtered widget slots)
|
|
const getActualInputIndex = (
|
|
input: INodeSlot,
|
|
filteredIndex: number
|
|
): number => {
|
|
if (!nodeData?.inputs) return filteredIndex
|
|
|
|
// Find the actual index in the unfiltered inputs array
|
|
const actualIndex = nodeData.inputs.findIndex((i) => i === input)
|
|
return actualIndex !== -1 ? actualIndex : filteredIndex
|
|
}
|
|
|
|
// Error boundary implementation
|
|
const renderError = ref<string | null>(null)
|
|
const { toastErrorHandler } = useErrorHandling()
|
|
|
|
onErrorCaptured((error) => {
|
|
renderError.value = error.message
|
|
toastErrorHandler(error)
|
|
return false
|
|
})
|
|
</script>
|