Files
ComfyUI_frontend/src/components/rightSidePanel/errors/MissingNodeCard.vue
jaeone94 c36da042d0 Redesign error tab cards with summary hero and unified sections (#12828)
## Summary

Redesigns the Errors tab cards to match the new Figma error-panel spec
(file `Czv0JcCfcUiizeEZURevpq`): every error group is now wrapped in a
single bordered card led by an error-count summary hero, with each
category rendered as a collapsible section whose count lives in a
circular badge rather than a parenthetical title suffix.

> Rebased onto `main` after #12793 was merged. This PR now contains only
the error-card redesign slice.

## Changes

- **What**:
- **New `ErrorCardSection.vue`** — the shared section shell used by
every error type. Renders a 32px header (circular count badge + neutral
title + `actions` slot + collapse chevron) and a `TransitionCollapse`
body. Replaces the per-group `PropertiesAccordionItem`, dropping the old
octagon-alert icon + red title + `(n)` suffix and the sticky-header
behavior.
- **`TabErrors.vue`** — wraps all groups in one `rounded-lg` card
bordered with `secondary-background`. Adds a summary **hero** (large
severity-colored total count, vertical divider, "N Errors detected /
Resolve before running the workflow"). Moves the per-group action
buttons (Install All / Replace All / missing-model Refresh) into the
section's `actions` slot. Adds `getGroupCount()` / `totalErrorCount` and
switches content background to `interface-panel-surface`. Most of the
line count here is re-indentation from the template restructure, not
behavior change.
- **`missingErrorResolver.ts`** — drops the `formatCountTitle` helper so
display titles are `"Missing Models"` instead of `"Missing Models (4)"`;
the badge now carries the count. Toast titles/messages are untouched.
- **`ErrorNodeCard.vue`** — restyles the runtime/validation error-log
box to the Figma spec: borderless `base-foreground/5` surface, `ERROR
LOG` header, 12px non-mono body at 50% opacity, inset footer divider
with Get Help / Find on GitHub links.
- **Row components** (`MissingModelRow`, `MissingPackGroupRow`,
`SwapNodeGroupRow`, `MissingMediaCard`, `MissingNodeCard`,
`MissingModelCard`) — align spacing, fonts, badges, and button sizes
with Figma: 12px row labels, `size="sm"` (24px) action buttons, 16px
count badges (`rounded-sm`, `secondary-background-hover`, 9px), 32px
reference-row heights, `px-3` card padding. Model-name wrapping is kept
independent of its count badge and link button so they never reflow into
the metadata sub-label.
- **i18n** — adds `errorsDetected` (pluralized), `resolveBeforeRun`,
`expand`, `collapse` to `en/main.json`.
- **Breaking**: None. No store, composable, action, or data-flow changes
— all handlers and emitted events are preserved. The only user-visible
copy change is the removal of the `(n)` count suffix from section
titles.

## Review Focus

- **Title copy change**: `"Missing Models (4)"` → `"Missing Models"`.
Search-filter matching against the old `(n)` string no longer applies,
but the count is shown by the badge and the hero total.
- **Sticky header removed**: section headers no longer pin to the top on
scroll (intentional per the new design).
- **Collapse click target**: the old single-button header (which nested
action buttons inside a `<button>` — invalid HTML) is split into a
separate title button and chevron button. Behavior is unchanged and
accessibility improves; the empty space beside an action button no
longer toggles collapse.
- All semantic colors map to existing design-system tokens (no `dark:`
variants, no hardcoded hex). Verified the artifact hex values match the
tokens (e.g. `#262729` = `secondary-background`, `#e04e48` =
`destructive-background-hover`, `#171718` = `interface-panel-surface`).

## Follow-up

This PR intentionally keeps the error-count ownership cleanup out of the
current diff so the card redesign remains reviewable. A follow-up PR
will centralize error counting around a single source of truth so the
Errors tab summary hero, section badges, and any overlay surfaces cannot
drift from one another.

That follow-up will also address the current count mismatch in the
ErrorOverlay and continue the ErrorOverlay redesign there, instead of
expanding this PR after review.

## Screenshots (if applicable)
After
<img width="603" height="703" alt="스크린샷 2026-06-13 오후 1 00 02"
src="https://github.com/user-attachments/assets/065d7c19-9748-4e99-9b43-675a31e92949"
/>
<img width="601" height="197" alt="스크린샷 2026-06-13 오후 1 01 07"
src="https://github.com/user-attachments/assets/0fa1fbda-9091-4a45-9eca-e99c43089c0e"
/>
<img width="617" height="612" alt="스크린샷 2026-06-13 오후 1 02 43"
src="https://github.com/user-attachments/assets/3d67a057-bf65-4e51-bcf5-70ecce851826"
/>
<img width="495" height="723" alt="스크린샷 2026-06-13 오후 1 03 28"
src="https://github.com/user-attachments/assets/6dcc4021-0fc3-4955-a68b-c0533c66a3cf"
/>

---------

Co-authored-by: GitHub Action <action@github.com>
2026-06-16 13:32:10 +00:00

172 lines
5.8 KiB
Vue

<template>
<div data-testid="missing-node-card" class="px-3">
<!-- Core node version warning (OSS only) -->
<div
v-if="!isCloud && hasMissingCoreNodes"
role="alert"
class="mb-3 flex gap-2.5 rounded-lg border border-warning-background/30 bg-warning-background/10 p-3"
>
<i
aria-hidden="true"
class="mt-0.5 icon-[lucide--triangle-alert] size-4 shrink-0 text-warning-background"
/>
<div class="flex flex-col gap-1.5 text-xs/relaxed text-muted-foreground">
<p class="m-0">
{{
currentComfyUIVersion
? t('loadWorkflowWarning.outdatedVersion', {
version: currentComfyUIVersion
})
: t('loadWorkflowWarning.outdatedVersionGeneric')
}}
</p>
<div
v-for="[version, nodes] in sortedMissingCoreNodes"
:key="version"
class="ml-2"
>
<span class="font-medium">
{{
t('loadWorkflowWarning.coreNodesFromVersion', {
version: version || t('loadWorkflowWarning.unknownVersion')
})
}}
</span>
<span class="ml-1">{{ getUniqueNodeNames(nodes).join(', ') }}</span>
</div>
</div>
</div>
<!-- Manager disabled hint: shown on OSS when manager is not active -->
<i18n-t
v-if="showManagerHint"
keypath="rightSidePanel.missingNodePacks.ossManagerDisabledHint"
tag="p"
class="m-0 pb-5 text-sm/relaxed text-muted-foreground"
>
<template #pipCmd>
<code
class="rounded-sm bg-comfy-menu-bg px-1 py-0.5 font-mono text-xs text-comfy-input-foreground"
>pip install -U --pre comfyui-manager</code
>
</template>
<template #flag>
<code
class="rounded-sm bg-comfy-menu-bg px-1 py-0.5 font-mono text-xs text-comfy-input-foreground"
>--enable-manager</code
>
</template>
</i18n-t>
<div class="flex flex-col gap-1 overflow-hidden">
<MissingPackGroupRow
v-for="group in missingPackGroups"
:key="group.packId ?? '__unknown__'"
:group="group"
:show-info-button="showInfoButton"
@locate-node="emit('locateNode', $event)"
@open-manager-info="emit('openManagerInfo', $event)"
/>
</div>
</div>
<!-- Apply Changes: shown when manager enabled and at least one pack install succeeded -->
<div v-if="shouldShowManagerButtons" class="px-4">
<Button
v-if="hasInstalledPacksPendingRestart"
variant="secondary"
size="sm"
:disabled="isRestarting"
class="mt-2 h-8 w-full min-w-0 rounded-md text-xs"
@click="applyChanges()"
>
<DotSpinner v-if="isRestarting" duration="1s" :size="12" />
<i
v-else
aria-hidden="true"
class="icon-[lucide--refresh-cw] size-4 shrink-0"
/>
<span class="min-w-0 truncate">{{
t('rightSidePanel.missingNodePacks.applyChanges')
}}</span>
</Button>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { compare, valid } from 'semver'
import Button from '@/components/ui/button/Button.vue'
import DotSpinner from '@/components/common/DotSpinner.vue'
import { useApplyChanges } from '@/workbench/extensions/manager/composables/useApplyChanges'
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
import { isCloud } from '@/platform/distribution/types'
import { useManagerState } from '@/workbench/extensions/manager/composables/useManagerState'
import { useMissingNodes } from '@/workbench/extensions/manager/composables/nodePack/useMissingNodes'
import { useSystemStatsStore } from '@/stores/systemStatsStore'
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
import type { MissingPackGroup } from '@/components/rightSidePanel/errors/useErrorGroups'
import MissingPackGroupRow from '@/components/rightSidePanel/errors/MissingPackGroupRow.vue'
const { showInfoButton, missingPackGroups } = defineProps<{
showInfoButton: boolean
missingPackGroups: MissingPackGroup[]
}>()
const emit = defineEmits<{
locateNode: [nodeId: string]
openManagerInfo: [packId: string]
}>()
const { t } = useI18n()
const { missingCoreNodes } = useMissingNodes()
const systemStatsStore = useSystemStatsStore()
const hasMissingCoreNodes = computed(
() => Object.keys(missingCoreNodes.value).length > 0
)
const currentComfyUIVersion = computed<string | null>(() => {
if (!hasMissingCoreNodes.value) return null
return systemStatsStore.systemStats?.system?.comfyui_version ?? null
})
const sortedMissingCoreNodes = computed(() =>
Object.entries(missingCoreNodes.value).sort(([a], [b]) => {
const aValid = valid(a)
const bValid = valid(b)
if (!aValid && !bValid) return 0
if (!aValid) return 1
if (!bValid) return -1
return compare(b, a)
})
)
function getUniqueNodeNames(nodes: LGraphNode[]): string[] {
const types = new Set(nodes.map((node) => node.type).filter(Boolean))
return [...types].sort()
}
const comfyManagerStore = useComfyManagerStore()
const { isRestarting, applyChanges } = useApplyChanges()
const { shouldShowManagerButtons } = useManagerState()
/**
* Show the --enable-manager hint when:
* - Not on Cloud (OSS/local only)
* - Manager is disabled (showInfoButton is false)
*/
const showManagerHint = computed(() => !isCloud && !showInfoButton)
/**
* Show Apply Changes when any pack from the error group is already installed
* on disk but ComfyUI hasn't restarted yet to load it.
* This is server-state based → persists across browser refreshes.
*/
const hasInstalledPacksPendingRestart = computed(() =>
missingPackGroups.some(
(g) => g.packId !== null && comfyManagerStore.isPackInstalled(g.packId)
)
)
</script>