Files
ComfyUI_frontend/src/components/error/useErrorOverlayState.ts
jaeone94 f9cbaf750f fix: simplify error overlay messaging (#12598)
## Summary

Simplifies the error overlay so it presents one clear title, one clear
message, and one stable details action instead of rendering a list of
per-error messages.

## Changes

- **What**: Extracts the error overlay view model into
`useErrorOverlayState`, adds focused unit coverage for the overlay copy
resolution rules, and updates the overlay E2E coverage to match the new
behavior.
- **Breaking**: None.
- **Dependencies**: None.

### Behavior changes

- The overlay body no longer renders a `<ul>` of individual error
messages. It now always renders a single paragraph message.
- Single-error overlays now prefer toast-specific copy when it exists.
For execution errors, the overlay resolves the message in this order:
`toastMessage`, `displayMessage`, raw `message`, group `displayMessage`,
then group `displayTitle`. The title resolves from `toastTitle`, then
`displayTitle`, then the group title.
- Single non-execution groups use group-level toast/display copy. This
lets grouped error types supply overlay-friendly copy without the
overlay needing to understand each card implementation.
- Multiple-error overlays now ignore individual error item copy in the
overlay itself. The header becomes the pluralized count title, for
example `7 errors found`, and the body becomes the fixed guidance
message: `Resolve them before running the workflow.`
- The overlay is hidden if the store reports an error count but no
resolved overlay message exists. This avoids rendering a visible shell
with an empty body.
- The action button no longer varies by error type in normal app mode.
Missing nodes, missing models, missing media, swap nodes, validation
errors, and runtime errors all use `View details` instead of labels like
`Show missing nodes`, `Show missing models`, `Show missing inputs`, or
`See Errors`.
- App mode keeps its existing `Show errors in graph` action label.
- The overlay width now keeps the previous width as its minimum and
allows a wider maximum, reducing avoidable wrapping in longer error
headers.
- The live region was softened from an assertive alert-style
announcement to `role="status"` with `aria-live="polite"` so updates
such as count changes are less disruptive.

### Tests

- Adds component coverage for the rendered overlay shape and app-mode
action label.
- Adds composable coverage for single execution errors, runtime errors,
grouped missing media errors, multiple-error aggregate copy, hidden
empty-message state, and display-copy fallback behavior.
- Updates `errorOverlay.spec.ts` so the E2E suite checks the new
single-message overlay, the stable `View details` action, and the fixed
multiple-error body guidance.
- Removes the old type-specific button-label E2E expectations because
that branch no longer exists in product behavior.

### Follow-up PR

A follow-up PR is stacked on top of this one:
`jaeone/fe-816-missing-resource-error-messaging`.

That follow-up will wire missing resource error resolvers into the copy
model consumed here. It covers missing node packs, missing models,
missing media, and swap-node groups, including the group-level
`toastTitle`, `toastMessage`, `displayMessage`, `displayDetails`, and
item label copy those cards need. This PR intentionally keeps the
overlay behavior separate so it can merge first without depending on the
missing-resource resolver copy.

## Review Focus

- Please check the single-error versus multiple-error overlay behavior,
especially the fallback order for execution error copy.
- Please check that the `View details` action is now intentionally
error-type agnostic in normal app mode while app mode keeps `Show errors
in graph`.
- Please check the empty-message guard and the requirement that a
single-error overlay only resolves a single group when the total error
count and group list agree.
- Please check the E2E reduction: the old type-specific action-label
assertions were removed because the UI branch they tested was removed.

## Screenshots (if applicable)

N/A
2026-06-04 06:28:14 +00:00

104 lines
2.6 KiB
TypeScript

import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { storeToRefs } from 'pinia'
import { useExecutionErrorStore } from '@/stores/executionErrorStore'
import { useErrorGroups } from '@/components/rightSidePanel/errors/useErrorGroups'
import type { ErrorGroup } from '@/components/rightSidePanel/errors/types'
function resolveSingleOverlayCopy(
group: ErrorGroup
): { title?: string; message: string } | undefined {
if (group.type === 'execution') {
const [card] = group.cards
const [error] = card?.errors ?? []
const message =
error?.toastMessage ??
error?.displayMessage ??
error?.message ??
group.displayMessage ??
group.displayTitle
if (!message) return undefined
return {
title: error?.toastTitle ?? error?.displayTitle ?? group.displayTitle,
message
}
}
const message =
group.toastMessage ?? group.displayMessage ?? group.displayTitle
if (!message) return undefined
return {
title: group.toastTitle ?? group.displayTitle,
message
}
}
export function useErrorOverlayState() {
const { t } = useI18n()
const executionErrorStore = useExecutionErrorStore()
const { totalErrorCount, isErrorOverlayOpen } =
storeToRefs(executionErrorStore)
const { allErrorGroups } = useErrorGroups('')
const hasExactlyOneError = computed(() => totalErrorCount.value === 1)
const hasMultipleErrors = computed(() => totalErrorCount.value > 1)
const singleErrorGroup = computed(() =>
hasExactlyOneError.value && allErrorGroups.value.length === 1
? allErrorGroups.value[0]
: undefined
)
const errorCountLabel = computed(() =>
t(
'errorOverlay.errorCount',
{ count: totalErrorCount.value },
totalErrorCount.value
)
)
const multipleErrorCountLabel = computed(() =>
t(
'errorOverlay.multipleErrorCount',
{ count: totalErrorCount.value },
totalErrorCount.value
)
)
const singleOverlayCopy = computed(() =>
singleErrorGroup.value
? resolveSingleOverlayCopy(singleErrorGroup.value)
: undefined
)
const overlayMessage = computed(() => {
if (hasMultipleErrors.value) {
return t('errorOverlay.multipleErrorsMessage')
}
return singleOverlayCopy.value?.message ?? ''
})
const overlayTitle = computed(() =>
hasMultipleErrors.value
? multipleErrorCountLabel.value
: (singleOverlayCopy.value?.title ?? errorCountLabel.value)
)
const isVisible = computed(
() =>
isErrorOverlayOpen.value &&
totalErrorCount.value > 0 &&
overlayMessage.value.trim().length > 0
)
return {
isVisible,
overlayMessage,
overlayTitle
}
}