mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-03 20:51:58 +00:00
## Summary Add node replacement UI to the missing nodes dialog. Users can select and replace deprecated/missing nodes with compatible alternatives directly from the dialog. ## Changes - Classify missing nodes into **Replaceable** (quick fix) and **Install Required** sections - Add select-all checkbox + per-node checkboxes for batch replacement - `useNodeReplacement` composable handles in-place node replacement on the graph: - Simple replacement (configure+copy) for nodes without mapping - Input/output connection remapping for nodes with mapping - Widget value transfer via `old_widget_ids` - Dot-notation input handling for Autogrow/DynamicCombo - Undo/redo support via `changeTracker` (try/finally) - Title and properties preservation - Footer UX: "Skip for Now" button when all nodes are replaceable (cloud + OSS) - Auto-close dialog when all replaceable nodes are replaced and no non-replaceable remain - Settings navigation link from "Don't show again" checkbox - 505-line unit test suite for `useNodeReplacement` ## Review Focus - `useNodeReplacement.ts` — core graph manipulation logic - `MissingNodesContent.vue` — checkbox selection state management - `MissingNodesFooter.vue` — conditional button rendering (cloud vs OSS vs all-replaceable) [screen-capture.webm](https://github.com/user-attachments/assets/7dae891c-926c-4f26-987f-9637c4a2ca16) ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-8604-feat-Node-replacement-UI-2fd6d73d36508148a371dabb8f4115af) by [Unito](https://www.unito.io) --------- Co-authored-by: github-actions <github-actions@github.com>
200 lines
6.3 KiB
Vue
200 lines
6.3 KiB
Vue
<template>
|
|
<div class="flex w-full flex-col gap-2 py-2 px-4">
|
|
<div class="flex flex-col gap-1 text-sm text-muted-foreground">
|
|
<div class="flex items-center gap-1">
|
|
<input
|
|
id="doNotAskAgainNodes"
|
|
v-model="doNotAskAgain"
|
|
type="checkbox"
|
|
class="h-4 w-4 cursor-pointer"
|
|
/>
|
|
<label for="doNotAskAgainNodes">{{
|
|
$t('missingModelsDialog.doNotAskAgain')
|
|
}}</label>
|
|
</div>
|
|
<i18n-t
|
|
v-if="doNotAskAgain"
|
|
keypath="missingModelsDialog.reEnableInSettings"
|
|
tag="span"
|
|
class="text-sm text-muted-foreground ml-6"
|
|
>
|
|
<template #link>
|
|
<Button
|
|
variant="textonly"
|
|
class="underline cursor-pointer p-0 text-sm text-muted-foreground hover:bg-transparent"
|
|
@click="openShowMissingNodesSetting"
|
|
>
|
|
{{ $t('missingModelsDialog.reEnableInSettingsLink') }}
|
|
</Button>
|
|
</template>
|
|
</i18n-t>
|
|
</div>
|
|
|
|
<!-- All nodes replaceable: Skip button (cloud + OSS) -->
|
|
<div v-if="!hasNonReplaceableNodes" class="flex justify-end gap-1">
|
|
<Button variant="secondary" size="md" @click="handleGotItClick">
|
|
{{ $t('nodeReplacement.skipForNow') }}
|
|
</Button>
|
|
</div>
|
|
|
|
<!-- Cloud mode: Learn More + Got It buttons -->
|
|
<div
|
|
v-else-if="isCloud"
|
|
class="flex w-full items-center justify-between gap-2"
|
|
>
|
|
<Button
|
|
variant="textonly"
|
|
size="sm"
|
|
as="a"
|
|
href="https://www.comfy.org/cloud"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
>
|
|
<i class="icon-[lucide--info]"></i>
|
|
<span>{{ $t('missingNodes.cloud.learnMore') }}</span>
|
|
</Button>
|
|
<Button variant="secondary" size="md" @click="handleGotItClick">{{
|
|
$t('missingNodes.cloud.gotIt')
|
|
}}</Button>
|
|
</div>
|
|
|
|
<!-- OSS mode: Manager buttons -->
|
|
<div v-else-if="showManagerButtons" class="flex justify-end gap-1">
|
|
<Button variant="textonly" @click="handleOpenManager">{{
|
|
$t('g.openManager')
|
|
}}</Button>
|
|
<PackInstallButton
|
|
v-if="showInstallAllButton"
|
|
type="secondary"
|
|
size="md"
|
|
:disabled="
|
|
isLoading || !!error || missingNodePacks.length === 0 || isInstalling
|
|
"
|
|
:is-loading="isLoading"
|
|
:node-packs="missingNodePacks"
|
|
:label="
|
|
isLoading
|
|
? $t('manager.gettingInfo')
|
|
: $t('manager.installAllMissingNodes')
|
|
"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { computed, nextTick, ref, watch } from 'vue'
|
|
import { useI18n } from 'vue-i18n'
|
|
|
|
import Button from '@/components/ui/button/Button.vue'
|
|
import { isCloud } from '@/platform/distribution/types'
|
|
import { useSettingStore } from '@/platform/settings/settingStore'
|
|
import { useToastStore } from '@/platform/updates/common/toastStore'
|
|
import { useSettingsDialog } from '@/platform/settings/composables/useSettingsDialog'
|
|
import { useDialogStore } from '@/stores/dialogStore'
|
|
import type { MissingNodeType } from '@/types/comfy'
|
|
import PackInstallButton from '@/workbench/extensions/manager/components/manager/button/PackInstallButton.vue'
|
|
import { useMissingNodes } from '@/workbench/extensions/manager/composables/nodePack/useMissingNodes'
|
|
import { useManagerState } from '@/workbench/extensions/manager/composables/useManagerState'
|
|
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
|
|
import { ManagerTab } from '@/workbench/extensions/manager/types/comfyManagerTypes'
|
|
|
|
const { missingNodeTypes } = defineProps<{
|
|
missingNodeTypes?: MissingNodeType[]
|
|
}>()
|
|
|
|
const dialogStore = useDialogStore()
|
|
const { t } = useI18n()
|
|
|
|
const doNotAskAgain = ref(false)
|
|
|
|
watch(doNotAskAgain, (value) => {
|
|
void useSettingStore().set('Comfy.Workflow.ShowMissingNodesWarning', !value)
|
|
})
|
|
|
|
const handleGotItClick = () => {
|
|
dialogStore.closeDialog({ key: 'global-missing-nodes' })
|
|
}
|
|
|
|
function openShowMissingNodesSetting() {
|
|
dialogStore.closeDialog({ key: 'global-missing-nodes' })
|
|
useSettingsDialog().show(undefined, 'Comfy.Workflow.ShowMissingNodesWarning')
|
|
}
|
|
|
|
const { missingNodePacks, isLoading, error } = useMissingNodes()
|
|
const comfyManagerStore = useComfyManagerStore()
|
|
const managerState = useManagerState()
|
|
function handleOpenManager() {
|
|
managerState.openManager({
|
|
initialTab: ManagerTab.Missing,
|
|
showToastOnLegacyError: true
|
|
})
|
|
}
|
|
|
|
// Check if any of the missing packs are currently being installed
|
|
const isInstalling = computed(() => {
|
|
if (!missingNodePacks.value?.length) return false
|
|
return missingNodePacks.value.some((pack) =>
|
|
comfyManagerStore.isPackInstalling(pack.id)
|
|
)
|
|
})
|
|
|
|
// Show manager buttons unless manager is disabled
|
|
const showManagerButtons = computed(() => {
|
|
return managerState.shouldShowManagerButtons.value
|
|
})
|
|
|
|
// Only show Install All button for NEW_UI (new manager with v4 support)
|
|
const showInstallAllButton = computed(() => {
|
|
return managerState.shouldShowInstallButton.value
|
|
})
|
|
|
|
const hasNonReplaceableNodes = computed(
|
|
() =>
|
|
missingNodeTypes?.some(
|
|
(n) =>
|
|
typeof n === 'string' || (typeof n === 'object' && !n.isReplaceable)
|
|
) ?? false
|
|
)
|
|
|
|
// Track whether missingNodePacks was ever non-empty (i.e. there were packs to install)
|
|
const hadMissingPacks = ref(false)
|
|
|
|
watch(
|
|
missingNodePacks,
|
|
(packs) => {
|
|
if (packs && packs.length > 0) hadMissingPacks.value = true
|
|
},
|
|
{ immediate: true }
|
|
)
|
|
|
|
// Only consider "all installed" when packs transitioned from non-empty to empty
|
|
// (actual installation happened). Replaceable-only case is handled by Content auto-close.
|
|
const allMissingNodesInstalled = computed(() => {
|
|
if (!hadMissingPacks.value) return false
|
|
return (
|
|
!isLoading.value &&
|
|
!isInstalling.value &&
|
|
missingNodePacks.value?.length === 0
|
|
)
|
|
})
|
|
|
|
// Watch for completion and close dialog (OSS mode only)
|
|
watch(allMissingNodesInstalled, async (allInstalled) => {
|
|
if (!isCloud && allInstalled && showInstallAllButton.value) {
|
|
// Use nextTick to ensure state updates are complete
|
|
await nextTick()
|
|
|
|
dialogStore.closeDialog({ key: 'global-missing-nodes' })
|
|
|
|
// Show success toast
|
|
useToastStore().add({
|
|
severity: 'success',
|
|
summary: t('g.success'),
|
|
detail: t('manager.allMissingNodesInstalled'),
|
|
life: 3000
|
|
})
|
|
}
|
|
})
|
|
</script>
|