mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-07-09 16:48:06 +00:00
## 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>
176 lines
5.1 KiB
Vue
176 lines
5.1 KiB
Vue
<template>
|
|
<div class="px-3">
|
|
<div
|
|
v-if="importableModelRows.length > 0"
|
|
data-testid="missing-model-importable-rows"
|
|
class="flex flex-col gap-1 overflow-hidden"
|
|
>
|
|
<MissingModelRow
|
|
v-for="row in importableModelRows"
|
|
:key="row.key"
|
|
:model="row.model"
|
|
:directory="row.directory"
|
|
:is-asset-supported="row.isAssetSupported"
|
|
:can-cloud-import="true"
|
|
@locate-model="emit('locateModel', $event)"
|
|
/>
|
|
</div>
|
|
|
|
<div
|
|
v-if="unsupportedModelRows.length > 0"
|
|
data-testid="missing-model-import-not-supported-section"
|
|
class="flex flex-col gap-1 border-t border-secondary-background pt-3"
|
|
>
|
|
<div class="mb-1">
|
|
<p class="m-0 text-sm font-semibold text-warning-background">
|
|
{{ t('rightSidePanel.missingModels.importNotSupported') }}
|
|
</p>
|
|
<p class="m-0 mt-1 text-xs/relaxed text-muted-foreground">
|
|
{{ t('rightSidePanel.missingModels.customNodeDownloadDisabled') }}
|
|
</p>
|
|
</div>
|
|
<MissingModelRow
|
|
v-for="row in unsupportedModelRows"
|
|
:key="row.key"
|
|
:model="row.model"
|
|
:directory="row.directory"
|
|
:is-asset-supported="row.isAssetSupported"
|
|
:can-cloud-import="false"
|
|
@locate-model="emit('locateModel', $event)"
|
|
/>
|
|
</div>
|
|
|
|
<div
|
|
v-if="downloadableModels.length > 0"
|
|
data-testid="missing-model-actions"
|
|
class="flex items-center pt-2"
|
|
>
|
|
<Button
|
|
data-testid="missing-model-download-all"
|
|
variant="secondary"
|
|
size="sm"
|
|
class="h-8 min-w-0 flex-1 rounded-md text-xs"
|
|
@click="downloadAllModels"
|
|
>
|
|
<i aria-hidden="true" class="icon-[lucide--download] size-4 shrink-0" />
|
|
<span class="truncate">{{ downloadAllLabel }}</span>
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { computed } from 'vue'
|
|
import { useI18n } from 'vue-i18n'
|
|
import type { MissingModelGroup } from '@/platform/missingModel/types'
|
|
import { isCloud } from '@/platform/distribution/types'
|
|
import MissingModelRow from '@/platform/missingModel/components/MissingModelRow.vue'
|
|
import Button from '@/components/ui/button/Button.vue'
|
|
import { downloadModel } from '@/platform/missingModel/missingModelDownload'
|
|
import { getDownloadableModels } from '@/platform/missingModel/missingModelViewUtils'
|
|
import { useMissingModelStore } from '@/platform/missingModel/missingModelStore'
|
|
import { formatSize } from '@/utils/formatUtil'
|
|
|
|
interface MissingModelRowEntry {
|
|
key: string
|
|
model: MissingModelGroup['models'][number]
|
|
directory: string | null
|
|
isAssetSupported: boolean
|
|
}
|
|
|
|
const MODEL_TYPE_SORT_ORDER = [
|
|
'checkpoints',
|
|
'loras',
|
|
'vae',
|
|
'text_encoders',
|
|
'diffusion_models'
|
|
] as const
|
|
|
|
const { missingModelGroups } = defineProps<{
|
|
missingModelGroups: MissingModelGroup[]
|
|
}>()
|
|
|
|
const emit = defineEmits<{
|
|
locateModel: [nodeId: string]
|
|
}>()
|
|
|
|
const { t } = useI18n()
|
|
const missingModelStore = useMissingModelStore()
|
|
|
|
const sortedModelRows = computed(() =>
|
|
missingModelGroups
|
|
.flatMap((group) =>
|
|
group.models.map((model, index) => ({
|
|
key: getModelRowKey(group, model, index),
|
|
model,
|
|
directory: group.directory,
|
|
isAssetSupported: group.isAssetSupported
|
|
}))
|
|
)
|
|
.sort((a, b) => compareModelRows(a, b))
|
|
)
|
|
|
|
const importableModelRows = computed(() =>
|
|
sortedModelRows.value.filter((row) => !isCloud || canCloudImport(row))
|
|
)
|
|
|
|
const unsupportedModelRows = computed(() =>
|
|
isCloud ? sortedModelRows.value.filter((row) => !canCloudImport(row)) : []
|
|
)
|
|
|
|
const downloadableModels = computed(() => {
|
|
if (isCloud) return []
|
|
|
|
return getDownloadableModels(missingModelGroups)
|
|
})
|
|
|
|
const downloadAllLabel = computed(() => {
|
|
const base = t('rightSidePanel.missingModels.downloadAll')
|
|
const total = downloadableModels.value.reduce(
|
|
(sum, model) => sum + (missingModelStore.fileSizes[model.url] ?? 0),
|
|
0
|
|
)
|
|
return total > 0 ? `${base} (${formatSize(total)})` : base
|
|
})
|
|
|
|
function downloadAllModels() {
|
|
for (const model of downloadableModels.value) {
|
|
downloadModel(model, missingModelStore.folderPaths)
|
|
}
|
|
}
|
|
|
|
function getModelRowKey(
|
|
group: MissingModelGroup,
|
|
model: MissingModelGroup['models'][number],
|
|
index: number
|
|
) {
|
|
const supportKey = group.isAssetSupported ? 'supported' : 'unsupported'
|
|
return [
|
|
supportKey,
|
|
group.directory ?? '__unknown__',
|
|
model.name,
|
|
String(index)
|
|
].join('::')
|
|
}
|
|
|
|
function compareModelRows(a: MissingModelRowEntry, b: MissingModelRowEntry) {
|
|
return (
|
|
getModelTypeSortIndex(a.directory) - getModelTypeSortIndex(b.directory) ||
|
|
(a.directory ?? '').localeCompare(b.directory ?? '') ||
|
|
a.model.name.localeCompare(b.model.name)
|
|
)
|
|
}
|
|
|
|
function getModelTypeSortIndex(directory: string | null) {
|
|
if (directory === null) return Number.MAX_SAFE_INTEGER
|
|
const index = MODEL_TYPE_SORT_ORDER.indexOf(
|
|
directory as (typeof MODEL_TYPE_SORT_ORDER)[number]
|
|
)
|
|
return index === -1 ? MODEL_TYPE_SORT_ORDER.length : index
|
|
}
|
|
|
|
function canCloudImport(row: MissingModelRowEntry) {
|
|
return row.isAssetSupported && row.directory !== null
|
|
}
|
|
</script>
|