mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-19 22:09:37 +00:00
## Summary - Remove the legacy missing nodes modal dialog and migrate all functionality to the existing Error Overlay / TabErrors system - Migrate core node version warning from `MissingCoreNodesMessage.vue` to `MissingNodeCard` in the errors tab - Remove `Comfy.Workflow.ShowMissingNodesWarning` setting (errors tab always surfaces missing nodes) - Delete 6 legacy files: `useMissingNodesDialog.ts`, `MissingNodesContent.vue`, `MissingNodesFooter.vue`, `MissingNodesHeader.vue`, `MissingCoreNodesMessage.vue` and its test - Rename `showMissingNodesDialog`/`showMissingModelsDialog` params to `showMissingNodes`/`showMissingModels` - Add `errorOverlay` and `missingNodeCard` to centralized `TestIds` - Migrate all E2E tests from legacy dialog selectors to error overlay testIds - Add new E2E test: MissingNodeCard visible via "See Errors" button flow - Add new E2E test: subgraph missing node type verified by expanding pack row - Add `surfaceMissingNodes` unit tests to `executionErrorStore` - Guard `semver.compare` against invalid version strings - Add `role="alert"`, `aria-hidden` for accessibility - Use reactive props destructuring in `MissingNodeCard` and `MissingPackGroupRow` **Net change: -669 lines** (19 files, +323 / -992) <img width="733" height="579" alt="image" src="https://github.com/user-attachments/assets/c497809d-b176-43bf-9872-34bd74c6ea0d" /> ## Test plan - [x] Unit tests: MissingNodeCard core node warning (7 tests) - [x] Unit tests: surfaceMissingNodes (4 tests) - [x] Unit tests: workflowService showPendingWarnings (updated) - [x] E2E: Error overlay visible on missing nodes workflow - [x] E2E: Error overlay visible on subgraph missing nodes - [x] E2E: MissingNodeCard visible via See Errors button - [x] E2E: Subgraph node type visible after expanding pack row - [x] E2E: Error overlay does not resurface on undo/redo - [x] E2E: Error overlay does not reappear on workflow tab switch - [x] Typecheck, lint, knip all passing ## Related issues - Closes #9923 (partially — `errorOverlay` and `missingNodeCard` added to TestIds) - References #10027 (mock hoisting inconsistency) - References #10033 (i18n-based test selectors) - References #10085 (DDD layer violation + focusedErrorNodeId) ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-10102-refactor-remove-legacy-missing-nodes-dialog-3256d73d365081c194d2e90bc6401846) by [Unito](https://www.unito.io)
251 lines
8.3 KiB
Vue
251 lines
8.3 KiB
Vue
<template>
|
|
<div class="mb-2 flex w-full flex-col">
|
|
<!-- Pack header row: pack name + info + chevron -->
|
|
<div class="flex h-8 w-full items-center">
|
|
<!-- Warning icon for unknown packs -->
|
|
<i
|
|
v-if="group.packId === null && !group.isResolving"
|
|
class="mr-1.5 icon-[lucide--triangle-alert] size-4 shrink-0 text-warning-background"
|
|
/>
|
|
<p
|
|
class="min-w-0 flex-1 truncate text-sm font-medium"
|
|
:class="
|
|
group.packId === null && !group.isResolving
|
|
? 'text-warning-background'
|
|
: 'text-foreground'
|
|
"
|
|
>
|
|
<span v-if="group.isResolving" class="text-muted-foreground italic">
|
|
{{ t('g.loading') }}...
|
|
</span>
|
|
<span v-else>
|
|
{{
|
|
`${group.packId ?? t('rightSidePanel.missingNodePacks.unknownPack')} (${group.nodeTypes.length})`
|
|
}}
|
|
</span>
|
|
</p>
|
|
<Button
|
|
v-if="showInfoButton && group.packId !== null"
|
|
variant="textonly"
|
|
size="icon-sm"
|
|
class="size-8 shrink-0 text-muted-foreground hover:text-base-foreground"
|
|
:aria-label="t('rightSidePanel.missingNodePacks.viewInManager')"
|
|
@click="emit('openManagerInfo', group.packId ?? '')"
|
|
>
|
|
<i class="icon-[lucide--info] size-4" />
|
|
</Button>
|
|
<Button
|
|
variant="textonly"
|
|
size="icon-sm"
|
|
:class="
|
|
cn(
|
|
'size-8 shrink-0 transition-transform duration-200 hover:bg-transparent',
|
|
{ 'rotate-180': expanded }
|
|
)
|
|
"
|
|
:aria-label="
|
|
expanded
|
|
? t('rightSidePanel.missingNodePacks.collapse')
|
|
: t('rightSidePanel.missingNodePacks.expand')
|
|
"
|
|
@click="toggleExpand"
|
|
>
|
|
<i
|
|
class="icon-[lucide--chevron-down] size-4 text-muted-foreground group-hover:text-base-foreground"
|
|
/>
|
|
</Button>
|
|
</div>
|
|
|
|
<!-- Sub-labels: individual node instances, each with their own Locate button -->
|
|
<TransitionCollapse>
|
|
<div
|
|
v-if="expanded"
|
|
class="mb-1 flex flex-col gap-0.5 overflow-hidden pl-2"
|
|
>
|
|
<div
|
|
v-for="nodeType in group.nodeTypes"
|
|
:key="getKey(nodeType)"
|
|
class="flex h-7 items-center"
|
|
>
|
|
<span
|
|
v-if="
|
|
showNodeIdBadge &&
|
|
typeof nodeType !== 'string' &&
|
|
nodeType.nodeId != null
|
|
"
|
|
class="mr-1 shrink-0 rounded-md bg-secondary-background-selected px-2 py-0.5 font-mono text-xs font-bold text-muted-foreground"
|
|
>
|
|
#{{ nodeType.nodeId }}
|
|
</span>
|
|
<p class="min-w-0 flex-1 truncate text-xs text-muted-foreground">
|
|
{{ getLabel(nodeType) }}
|
|
</p>
|
|
<Button
|
|
v-if="typeof nodeType !== 'string' && nodeType.nodeId != null"
|
|
variant="textonly"
|
|
size="icon-sm"
|
|
class="mr-1 size-6 shrink-0 text-muted-foreground hover:text-base-foreground"
|
|
:aria-label="t('rightSidePanel.locateNode')"
|
|
@click="handleLocateNode(nodeType)"
|
|
>
|
|
<i class="icon-[lucide--locate] size-3" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</TransitionCollapse>
|
|
|
|
<!-- Install button: shown when manager enabled, registry knows the pack or it's already installed -->
|
|
<div
|
|
v-if="
|
|
shouldShowManagerButtons &&
|
|
group.packId !== null &&
|
|
(nodePack || comfyManagerStore.isPackInstalled(group.packId))
|
|
"
|
|
class="flex w-full items-start py-1"
|
|
>
|
|
<Button
|
|
variant="secondary"
|
|
size="md"
|
|
class="flex w-full flex-1"
|
|
:disabled="
|
|
comfyManagerStore.isPackInstalled(group.packId) || isInstalling
|
|
"
|
|
@click="handlePackInstallClick"
|
|
>
|
|
<DotSpinner
|
|
v-if="isInstalling"
|
|
duration="1s"
|
|
:size="12"
|
|
class="mr-1.5 shrink-0"
|
|
/>
|
|
<i
|
|
v-else-if="comfyManagerStore.isPackInstalled(group.packId)"
|
|
class="text-foreground mr-1 icon-[lucide--check] size-4 shrink-0"
|
|
/>
|
|
<i
|
|
v-else
|
|
class="text-foreground mr-1 icon-[lucide--download] size-4 shrink-0"
|
|
/>
|
|
<span class="text-foreground min-w-0 truncate text-sm">
|
|
{{
|
|
isInstalling
|
|
? t('rightSidePanel.missingNodePacks.installing')
|
|
: comfyManagerStore.isPackInstalled(group.packId)
|
|
? t('rightSidePanel.missingNodePacks.installed')
|
|
: t('rightSidePanel.missingNodePacks.installNodePack')
|
|
}}
|
|
</span>
|
|
</Button>
|
|
</div>
|
|
|
|
<!-- Registry still loading: packId known but result not yet available -->
|
|
<div
|
|
v-else-if="group.packId !== null && shouldShowManagerButtons && isLoading"
|
|
class="flex w-full items-start py-1"
|
|
>
|
|
<div
|
|
class="flex h-8 min-w-0 flex-1 cursor-not-allowed items-center justify-center overflow-hidden rounded-lg bg-secondary-background p-2 opacity-60 select-none"
|
|
>
|
|
<DotSpinner duration="1s" :size="12" class="mr-1.5 shrink-0" />
|
|
<span class="text-foreground min-w-0 truncate text-sm">
|
|
{{ t('g.loading') }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Search in Manager: fetch done but pack not found in registry -->
|
|
<div
|
|
v-else-if="group.packId !== null && shouldShowManagerButtons"
|
|
class="flex w-full items-start py-1"
|
|
>
|
|
<Button
|
|
variant="secondary"
|
|
size="md"
|
|
class="flex w-full flex-1"
|
|
@click="
|
|
openManager({
|
|
initialTab: ManagerTab.All,
|
|
initialPackId: group.packId!
|
|
})
|
|
"
|
|
>
|
|
<i class="text-foreground mr-1 icon-[lucide--search] size-4 shrink-0" />
|
|
<span class="text-foreground min-w-0 truncate text-sm">
|
|
{{ t('rightSidePanel.missingNodePacks.searchInManager') }}
|
|
</span>
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, computed } from 'vue'
|
|
import { cn } from '@/utils/tailwindUtil'
|
|
import { useI18n } from 'vue-i18n'
|
|
import Button from '@/components/ui/button/Button.vue'
|
|
import DotSpinner from '@/components/common/DotSpinner.vue'
|
|
import TransitionCollapse from '@/components/rightSidePanel/layout/TransitionCollapse.vue'
|
|
import { useMissingNodes } from '@/workbench/extensions/manager/composables/nodePack/useMissingNodes'
|
|
import { usePackInstall } from '@/workbench/extensions/manager/composables/nodePack/usePackInstall'
|
|
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
|
|
import { useManagerState } from '@/workbench/extensions/manager/composables/useManagerState'
|
|
import { ManagerTab } from '@/workbench/extensions/manager/types/comfyManagerTypes'
|
|
import type { MissingNodeType } from '@/types/comfy'
|
|
import type { MissingPackGroup } from '@/components/rightSidePanel/errors/useErrorGroups'
|
|
|
|
const { group, showInfoButton, showNodeIdBadge } = defineProps<{
|
|
group: MissingPackGroup
|
|
showInfoButton: boolean
|
|
showNodeIdBadge: boolean
|
|
}>()
|
|
|
|
const emit = defineEmits<{
|
|
locateNode: [nodeId: string]
|
|
openManagerInfo: [packId: string]
|
|
}>()
|
|
|
|
const { t } = useI18n()
|
|
|
|
const { missingNodePacks, isLoading } = useMissingNodes()
|
|
const comfyManagerStore = useComfyManagerStore()
|
|
const { shouldShowManagerButtons, openManager } = useManagerState()
|
|
|
|
const nodePack = computed(() => {
|
|
if (!group.packId) return null
|
|
return missingNodePacks.value.find((p) => p.id === group.packId) ?? null
|
|
})
|
|
|
|
const { isInstalling, installAllPacks } = usePackInstall(() =>
|
|
nodePack.value ? [nodePack.value] : []
|
|
)
|
|
|
|
function handlePackInstallClick() {
|
|
if (!group.packId) return
|
|
if (!comfyManagerStore.isPackInstalled(group.packId)) {
|
|
void installAllPacks()
|
|
}
|
|
}
|
|
|
|
const expanded = ref(false)
|
|
|
|
function toggleExpand() {
|
|
expanded.value = !expanded.value
|
|
}
|
|
|
|
function getKey(nodeType: MissingNodeType): string {
|
|
if (typeof nodeType === 'string') return nodeType
|
|
return nodeType.nodeId != null ? String(nodeType.nodeId) : nodeType.type
|
|
}
|
|
|
|
function getLabel(nodeType: MissingNodeType): string {
|
|
return typeof nodeType === 'string' ? nodeType : nodeType.type
|
|
}
|
|
|
|
function handleLocateNode(nodeType: MissingNodeType) {
|
|
if (typeof nodeType === 'string') return
|
|
if (nodeType.nodeId != null) {
|
|
emit('locateNode', String(nodeType.nodeId))
|
|
}
|
|
}
|
|
</script>
|