Files
ComfyUI_frontend/src/renderer/extensions/vueNodes/components/InputSlot.vue
Arjan Singh a2c7db9dc2 [fix] add InputSlot and dot error state (#5813)
## Summary

Error states were not getting propagated down to the InputSlots from the
API Repsonse

I created a provider and injected error state. It seemed like a way
better idea than prop drilling or building a composable that only two
nodes (`InputSlot` and `OutputSlot`) would need.

## Changes

The follow are now error code red when an input node has errors:
1. There's a error round border around the dot.
2. The dot is error colored.
3. The input text is error colored.

This treatment was okay after feedback from design.

## Screenshots - Error State

<img width="749" height="616" alt="Screenshot 2025-09-26 at 9 02 58 PM"
src="https://github.com/user-attachments/assets/55c7edc9-081b-4a9d-9753-120465959b5d"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5813-fix-add-InputSlot-and-dot-error-state-27b6d73d36508151a955e485f00a2d05)
by [Unito](https://www.unito.io)

---------

Co-authored-by: github-actions <github-actions@github.com>
2025-09-27 20:14:13 -07:00

155 lines
4.1 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div v-if="renderError" class="node-error p-1 text-red-500 text-xs"></div>
<div v-else v-tooltip.left="tooltipConfig" :class="slotWrapperClass">
<!-- Connection Dot -->
<SlotConnectionDot
ref="connectionDotRef"
:color="slotColor"
:class="cn('-translate-x-1/2', errorClassesDot)"
v-on="readonly ? {} : { pointerdown: onPointerDown }"
/>
<!-- Slot Name -->
<div class="relative">
<span
v-if="!dotOnly"
:class="
cn('whitespace-nowrap text-sm font-normal lod-toggle', labelClasses)
"
>
{{ slotData.localized_name || slotData.name || `Input ${index}` }}
</span>
<LODFallback />
</div>
</div>
</template>
<script setup lang="ts">
import {
type ComponentPublicInstance,
type Ref,
computed,
inject,
onErrorCaptured,
ref,
watchEffect
} from 'vue'
import { useErrorHandling } from '@/composables/useErrorHandling'
import { getSlotColor } from '@/constants/slotColors'
import type { INodeSlot } from '@/lib/litegraph/src/litegraph'
import { useNodeTooltips } from '@/renderer/extensions/vueNodes/composables/useNodeTooltips'
import { useSlotElementTracking } from '@/renderer/extensions/vueNodes/composables/useSlotElementTracking'
import { useSlotLinkInteraction } from '@/renderer/extensions/vueNodes/composables/useSlotLinkInteraction'
import { useExecutionStore } from '@/stores/executionStore'
import { cn } from '@/utils/tailwindUtil'
import LODFallback from './LODFallback.vue'
import SlotConnectionDot from './SlotConnectionDot.vue'
interface InputSlotProps {
nodeType?: string
nodeId?: string
slotData: INodeSlot
index: number
connected?: boolean
compatible?: boolean
readonly?: boolean
dotOnly?: boolean
}
const props = defineProps<InputSlotProps>()
const executionStore = useExecutionStore()
const hasSlotError = computed(() => {
const nodeErrors = executionStore.lastNodeErrors?.[props.nodeId ?? '']
if (!nodeErrors) return false
const slotName = props.slotData.name
return nodeErrors.errors.some(
(error) => error.extra_info?.input_name === slotName
)
})
const errorClassesDot = computed(() => {
return hasSlotError.value
? 'ring-2 ring-error dark-theme:ring-error ring-offset-0 rounded-full'
: ''
})
const labelClasses = computed(() =>
hasSlotError.value
? 'text-error dark-theme:text-error font-medium'
: 'dark-theme:text-slate-200 text-stone-200'
)
const renderError = ref<string | null>(null)
const { toastErrorHandler } = useErrorHandling()
const tooltipContainer =
inject<Ref<HTMLElement | undefined>>('tooltipContainer')
const { getInputSlotTooltip, createTooltipConfig } = useNodeTooltips(
props.nodeType || '',
tooltipContainer
)
const tooltipConfig = computed(() => {
const slotName = props.slotData.localized_name || props.slotData.name || ''
const tooltipText = getInputSlotTooltip(slotName)
const fallbackText = tooltipText || `Input: ${slotName}`
return createTooltipConfig(fallbackText)
})
onErrorCaptured((error) => {
renderError.value = error.message
toastErrorHandler(error)
return false
})
const slotColor = computed(() => {
if (hasSlotError.value) {
return 'var(--color-error)'
}
return getSlotColor(props.slotData.type)
})
const slotWrapperClass = computed(() =>
cn(
'lg-slot lg-slot--input flex items-center group rounded-r-lg h-6',
props.readonly ? 'cursor-default opacity-70' : 'cursor-crosshair',
props.dotOnly
? 'lg-slot--dot-only'
: 'pr-6 hover:bg-black/5 hover:dark:bg-white/5',
{
'lg-slot--connected': props.connected,
'lg-slot--compatible': props.compatible
}
)
)
const connectionDotRef = ref<ComponentPublicInstance<{
slotElRef: HTMLElement | undefined
}> | null>(null)
const slotElRef = ref<HTMLElement | null>(null)
watchEffect(() => {
const el = connectionDotRef.value?.slotElRef
slotElRef.value = el || null
})
useSlotElementTracking({
nodeId: props.nodeId ?? '',
index: props.index,
type: 'input',
element: slotElRef
})
const { onPointerDown } = useSlotLinkInteraction({
nodeId: props.nodeId ?? '',
index: props.index,
type: 'input',
readonly: props.readonly
})
</script>