fix: improve error overlay design and error indicator placement

Move error border from TopMenuSection/ComfyActionbar to ErrorOverlay.
Add error indicator (outline + StatusBadge dot) on right side panel
toggle button when errors exist and panel/overlay are closed.
Replace technical group titles with user-friendly i18n messages in
ErrorOverlay. Dynamically change action button label based on single
error type. Remove unused hasAnyError prop from ComfyActionbar.
This commit is contained in:
jaeone94
2026-03-26 22:45:36 +09:00
parent bcb39b1bf6
commit 307e8e4203
5 changed files with 100 additions and 40 deletions

View File

@@ -46,7 +46,6 @@
<ComfyActionbar
:top-menu-container="actionbarContainerRef"
:queue-overlay-expanded="isQueueOverlayExpanded"
:has-any-error="hasAnyError"
@update:progress-target="updateProgressTarget"
/>
<CurrentUserButton
@@ -67,16 +66,29 @@
{{ t('actionbar.share') }}
</span>
</Button>
<Button
v-if="!isRightSidePanelOpen"
v-tooltip.bottom="rightSidePanelTooltipConfig"
type="secondary"
size="icon"
:aria-label="t('rightSidePanel.togglePanel')"
@click="rightSidePanelStore.togglePanel"
>
<i class="icon-[lucide--panel-right] size-4" />
</Button>
<div v-if="!isRightSidePanelOpen" class="relative">
<Button
v-tooltip.bottom="rightSidePanelTooltipConfig"
:class="
cn(
showErrorIndicatorOnPanelButton &&
'outline-1 outline-destructive-background'
)
"
type="secondary"
size="icon"
:aria-label="t('rightSidePanel.togglePanel')"
@click="rightSidePanelStore.togglePanel"
>
<i class="icon-[lucide--panel-right] size-4" />
</Button>
<StatusBadge
v-if="showErrorIndicatorOnPanelButton"
variant="dot"
severity="danger"
class="absolute -top-1 -right-1"
/>
</div>
</div>
</div>
<ErrorOverlay />
@@ -129,6 +141,7 @@ import ErrorOverlay from '@/components/error/ErrorOverlay.vue'
import ActionBarButtons from '@/components/topbar/ActionBarButtons.vue'
import CurrentUserButton from '@/components/topbar/CurrentUserButton.vue'
import LoginButton from '@/components/topbar/LoginButton.vue'
import StatusBadge from '@/components/common/StatusBadge.vue'
import Button from '@/components/ui/button/Button.vue'
import { useCurrentUser } from '@/composables/auth/useCurrentUser'
import { useQueueFeatureFlags } from '@/composables/queue/useQueueFeatureFlags'
@@ -206,12 +219,7 @@ const actionbarContainerClass = computed(() => {
)
}
const borderClass =
!isActionbarFloating.value && hasAnyError.value
? 'border-destructive-background-hover'
: 'border-interface-stroke'
return cn(base, 'px-2', borderClass)
return cn(base, 'px-2', 'border-interface-stroke')
})
const isIntegratedTabBar = computed(
() => settingStore.get('Comfy.UI.TabBarLayout') !== 'Legacy'
@@ -254,7 +262,14 @@ const shouldShowRedDot = computed((): boolean => {
return shouldShowConflictRedDot.value
})
const { hasAnyError } = storeToRefs(executionErrorStore)
const { hasAnyError, isErrorOverlayOpen } = storeToRefs(executionErrorStore)
const showErrorIndicatorOnPanelButton = computed(
() =>
hasAnyError.value &&
!isRightSidePanelOpen.value &&
!isErrorOverlayOpen.value
)
// Right side panel toggle
const { isOpen: isRightSidePanelOpen } = storeToRefs(rightSidePanelStore)

View File

@@ -119,14 +119,9 @@ import { cn } from '@/utils/tailwindUtil'
import ComfyRunButton from './ComfyRunButton'
const {
topMenuContainer,
queueOverlayExpanded = false,
hasAnyError = false
} = defineProps<{
const { topMenuContainer, queueOverlayExpanded = false } = defineProps<{
topMenuContainer?: HTMLElement | null
queueOverlayExpanded?: boolean
hasAnyError?: boolean
}>()
const emit = defineEmits<{
@@ -440,12 +435,7 @@ const panelClass = computed(() =>
isDragging.value && 'pointer-events-none select-none',
isDocked.value
? 'static border-none bg-transparent p-0'
: [
'fixed shadow-interface',
hasAnyError
? 'border-destructive-background-hover'
: 'border-interface-stroke'
]
: ['fixed shadow-interface', 'border-interface-stroke']
)
)
</script>

View File

@@ -9,7 +9,7 @@
role="alert"
aria-live="assertive"
data-testid="error-overlay"
class="pointer-events-auto flex w-80 min-w-72 flex-col overflow-hidden rounded-lg border border-interface-stroke bg-comfy-menu-bg shadow-interface transition-colors duration-200 ease-in-out"
class="pointer-events-auto flex w-80 min-w-72 flex-col overflow-hidden rounded-lg border border-destructive-background bg-comfy-menu-bg shadow-interface transition-colors duration-200 ease-in-out"
>
<!-- Header -->
<div class="flex h-12 items-center gap-2 px-4">
@@ -27,10 +27,10 @@
</div>
<!-- Body -->
<div class="px-4 pb-3">
<div class="px-4 pb-3" data-testid="error-overlay-messages">
<ul class="m-0 flex list-none flex-col gap-1.5 p-0">
<li
v-for="(message, idx) in groupedErrorMessages"
v-for="(message, idx) in overlayMessages"
:key="idx"
class="flex min-w-0 items-baseline gap-2 text-sm/snug text-muted-foreground"
>
@@ -46,7 +46,12 @@
<!-- Footer -->
<div class="flex items-center justify-end gap-4 px-4 py-3">
<Button variant="muted-textonly" size="unset" @click="dismiss">
<Button
variant="muted-textonly"
size="unset"
data-testid="error-overlay-dismiss"
@click="dismiss"
>
{{ t('g.dismiss') }}
</Button>
<Button
@@ -55,9 +60,7 @@
data-testid="error-overlay-see-errors"
@click="seeErrors"
>
{{
appMode ? t('linearMode.error.goto') : t('errorOverlay.seeErrors')
}}
{{ appMode ? t('linearMode.error.goto') : seeErrorsLabel }}
</Button>
</div>
</div>
@@ -84,7 +87,54 @@ const rightSidePanelStore = useRightSidePanelStore()
const canvasStore = useCanvasStore()
const { totalErrorCount, isErrorOverlayOpen } = storeToRefs(executionErrorStore)
const { groupedErrorMessages } = useErrorGroups(ref(''), t)
const { allErrorGroups, missingModelGroups } = useErrorGroups(ref(''), t)
const singleErrorType = computed(() => {
const types = new Set(allErrorGroups.value.map((g) => g.type))
return types.size === 1 ? [...types][0] : null
})
function toFriendlyMessage(group: (typeof allErrorGroups.value)[number]) {
if (group.type === 'missing_node') return t('errorOverlay.missingNodes')
if (group.type === 'swap_nodes') return t('errorOverlay.swapNodes')
if (group.type === 'missing_model') {
const modelCount = missingModelGroups.value.reduce(
(count, g) => count + g.models.length,
0
)
return t('errorOverlay.missingModels', { count: modelCount }, modelCount)
}
return null
}
const overlayMessages = computed<string[]>(() => {
const messages = new Set<string>()
for (const group of allErrorGroups.value) {
const friendly = toFriendlyMessage(group)
if (friendly) {
messages.add(friendly)
} else if (group.type === 'execution') {
for (const card of group.cards) {
for (const err of card.errors) {
messages.add(err.message)
}
}
}
}
return Array.from(messages)
})
const seeErrorsLabel = computed(() => {
const labelMap: Record<string, string> = {
missing_node: t('errorOverlay.showMissingNodes'),
missing_model: t('errorOverlay.showMissingModels'),
swap_nodes: t('errorOverlay.showSwapNodes')
}
if (singleErrorType.value) {
return labelMap[singleErrorType.value] ?? t('errorOverlay.seeErrors')
}
return t('errorOverlay.seeErrors')
})
const errorCountLabel = computed(() =>
t(

View File

@@ -682,7 +682,6 @@ export function useErrorGroups(
}
}
} else {
// Groups without cards (e.g. missing_node) surface their title as the message.
messages.add(group.title)
}
}

View File

@@ -3530,7 +3530,13 @@
},
"errorOverlay": {
"errorCount": "{count} ERROR | {count} ERRORS",
"seeErrors": "See Errors"
"seeErrors": "See Errors",
"showMissingNodes": "Show missing nodes",
"showMissingModels": "Show missing models",
"showSwapNodes": "Show swap nodes",
"missingNodes": "Some nodes are missing and need to be installed",
"missingModels": "{count} required model is missing | {count} required models are missing",
"swapNodes": "Some nodes can be replaced with alternatives"
},
"help": {
"recentReleases": "Recent releases",