[refactor] Unify Cloud/OSS Missing Nodes modal (#6673)

## Summary
- Merged separate Cloud and OSS workflow warning modals into single
unified modal
- Removed legacy LoadWorkflowWarning.vue
- Renamed CloudMissingNodes* components to MissingNodes* for clarity
- Environment branching now handled internally via isCloud flag
- Restructured i18n: removed loadWorkflowWarning, added
missingNodes.cloud/oss sections
- Improved OSS button styling to match Cloud consistency

## Key Changes
- **OSS**: "Open Manager" + "Install All" buttons
- **Cloud**: "Learn More" + "Got It" buttons (unchanged)
- Single unified modal displays different UI/text based on environment

## 📝 Note on File Renames

This PR renames the following files:

- `CloudMissingNodesHeader.vue` → `MissingNodesHeader.vue` (R053, 53%
similarity)
- `CloudMissingNodesContent.vue` → `MissingNodesContent.vue` (R067, 67%
similarity)
- `LoadWorkflowWarning.vue` → `MissingNodesFooter.vue` (R051, 51%
similarity)
- `CloudMissingNodesFooter.vue` → Deleted (replaced by new
MissingNodesFooter)

**Why GitHub PR UI doesn't show renames properly:**

GitHub detects renames only when file similarity is above 70%. In this
PR, the Cloud/OSS unification significantly modified file contents,
resulting in 51-67% similarity.

However, **Git history correctly records these as renames**. You can
verify with:

```bash
git show <commit-hash> --name-status
```

While GitHub UI shows "additions/deletions", these are actually rename +
modification operations.

## Test Plan
- [x] Test OSS mode: missing nodes modal shows "Open Manager" and
"Install All" buttons
- [x] Test Cloud mode: missing nodes modal shows "Learn More" and "Got
It" buttons
- [x] Verify Install All button functionality in OSS
- [x] Verify modal closes automatically after all nodes are installed
(OSS)


[missingnodes.webm](https://github.com/user-attachments/assets/36d3b4b0-ff8b-4b45-824c-3bc15d93f1a2)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6673-refactor-Unify-Cloud-OSS-Missing-Nodes-modal-2aa6d73d365081a88827d0fa85db4c63)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Jin Yi
2025-11-16 05:09:44 +09:00
committed by GitHub
parent 7a11dc59b6
commit 8dd5a9900b
10 changed files with 100 additions and 180 deletions

View File

@@ -32,7 +32,7 @@ defineOptions({
interface IconTextButtonProps extends BaseButtonProps {
iconPosition?: 'left' | 'right'
label: string
onClick: () => void
onClick?: () => void
}
const {

View File

@@ -1,37 +0,0 @@
<template>
<div class="flex w-full items-center justify-between gap-2 py-2 px-4">
<IconTextButton
:label="$t('cloud.missingNodes.learnMore')"
type="transparent"
size="sm"
icon-position="left"
@click="handleLearnMoreClick"
>
<template #icon>
<i class="icon-[lucide--info]"></i>
</template>
</IconTextButton>
<TextButton
:label="$t('cloud.missingNodes.gotIt')"
type="secondary"
size="md"
@click="handleGotItClick"
/>
</div>
</template>
<script setup lang="ts">
import IconTextButton from '@/components/button/IconTextButton.vue'
import TextButton from '@/components/button/TextButton.vue'
import { useDialogStore } from '@/stores/dialogStore'
const dialogStore = useDialogStore()
const handleLearnMoreClick = () => {
window.open('https://www.comfy.org/cloud', '_blank')
}
const handleGotItClick = () => {
dialogStore.closeDialog({ key: 'global-cloud-missing-nodes' })
}
</script>

View File

@@ -49,14 +49,14 @@ import { computed } from 'vue'
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
import { useSystemStatsStore } from '@/stores/systemStatsStore'
const props = defineProps<{
const { missingCoreNodes } = defineProps<{
missingCoreNodes: Record<string, LGraphNode[]>
}>()
const systemStatsStore = useSystemStatsStore()
const hasMissingCoreNodes = computed(() => {
return Object.keys(props.missingCoreNodes).length > 0
return Object.keys(missingCoreNodes).length > 0
})
// Use computed for reactive version tracking
@@ -66,7 +66,7 @@ const currentComfyUIVersion = computed<string | null>(() => {
})
const sortedMissingCoreNodes = computed(() => {
return Object.entries(props.missingCoreNodes).sort(([a], [b]) => {
return Object.entries(missingCoreNodes).sort(([a], [b]) => {
// Sort by version in descending order (newest first)
return compare(b, a) // Reversed for descending order
})

View File

@@ -6,15 +6,18 @@
<!-- Description -->
<div>
<p class="m-0 text-sm leading-4 text-muted-foreground">
{{ $t('cloud.missingNodes.description') }}
<br /><br />
{{ $t('cloud.missingNodes.priorityMessage') }}
{{
isCloud
? $t('missingNodes.cloud.description')
: $t('missingNodes.oss.description')
}}
</p>
</div>
<MissingCoreNodesMessage v-if="!isCloud" :missing-core-nodes />
<!-- Missing Nodes List Wrapper -->
<div
class="flex flex-col max-h-[256px] rounded-lg py-2 scrollbar-custom bg-secondary-background"
class="comfy-missing-nodes flex flex-col max-h-[256px] rounded-lg py-2 scrollbar-custom bg-secondary-background"
>
<div
v-for="(node, i) in uniqueNodes"
@@ -24,13 +27,18 @@
<span class="text-xs">
{{ node.label }}
</span>
<span v-if="node.hint" class="text-xs">{{ node.hint }}</span>
</div>
</div>
<!-- Bottom instruction -->
<div>
<p class="m-0 text-sm leading-4 text-muted-foreground">
{{ $t('cloud.missingNodes.replacementInstruction') }}
{{
isCloud
? $t('missingNodes.cloud.replacementInstruction')
: $t('missingNodes.oss.replacementInstruction')
}}
</p>
</div>
</div>
@@ -40,12 +48,18 @@
<script setup lang="ts">
import { computed } from 'vue'
import MissingCoreNodesMessage from '@/components/dialog/content/MissingCoreNodesMessage.vue'
import { isCloud } from '@/platform/distribution/types'
import type { MissingNodeType } from '@/types/comfy'
import { useMissingNodes } from '@/workbench/extensions/manager/composables/nodePack/useMissingNodes'
const props = defineProps<{
missingNodeTypes: MissingNodeType[]
}>()
// Get missing core nodes for OSS mode
const { missingCoreNodes } = useMissingNodes()
const uniqueNodes = computed(() => {
const seenTypes = new Set()
return props.missingNodeTypes

View File

@@ -1,39 +1,42 @@
<template>
<NoResultsPlaceholder
class="pb-0"
icon="pi pi-exclamation-circle"
:title="$t('loadWorkflowWarning.missingNodesTitle')"
:message="$t('loadWorkflowWarning.missingNodesDescription')"
/>
<MissingCoreNodesMessage :missing-core-nodes="missingCoreNodes" />
<ListBox
:options="uniqueNodes"
option-label="label"
scroll-height="100%"
class="comfy-missing-nodes"
:pt="{
list: { class: 'border-none' }
}"
<!-- Cloud mode: Learn More + Got It buttons -->
<div
v-if="isCloud"
class="flex w-full items-center justify-between gap-2 py-2 px-4"
>
<template #option="slotProps">
<div class="align-items-center flex">
<span class="node-type">{{ slotProps.option.label }}</span>
<span v-if="slotProps.option.hint" class="node-hint">{{
slotProps.option.hint
}}</span>
<Button
v-if="slotProps.option.action"
:label="slotProps.option.action.text"
size="small"
outlined
@click="slotProps.option.action.callback"
/>
</div>
</template>
</ListBox>
<div v-if="showManagerButtons" class="flex justify-end py-3">
<IconTextButton
:label="$t('missingNodes.cloud.learnMore')"
type="transparent"
size="sm"
icon-position="left"
as="a"
href="https://www.comfy.org/cloud"
target="_blank"
rel="noopener noreferrer"
>
<template #icon>
<i class="icon-[lucide--info]"></i>
</template>
</IconTextButton>
<TextButton
:label="$t('missingNodes.cloud.gotIt')"
type="secondary"
size="md"
@click="handleGotItClick"
/>
</div>
<!-- OSS mode: Open Manager + Install All buttons -->
<div v-else-if="showManagerButtons" class="flex justify-end gap-1 py-2 px-4">
<TextButton
:label="$t('g.openManager')"
type="transparent"
size="sm"
@click="openManager"
/>
<PackInstallButton
v-if="showInstallAllButton"
type="secondary"
size="md"
:disabled="
isLoading || !!error || missingNodePacks.length === 0 || isInstalling
@@ -46,40 +49,32 @@
: $t('manager.installAllMissingNodes')
"
/>
<Button
:label="$t('g.openManager')"
size="small"
outlined
@click="openManager"
/>
</div>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import ListBox from 'primevue/listbox'
import { computed, nextTick, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
import MissingCoreNodesMessage from '@/components/dialog/content/MissingCoreNodesMessage.vue'
import IconTextButton from '@/components/button/IconTextButton.vue'
import TextButton from '@/components/button/TextButton.vue'
import { isCloud } from '@/platform/distribution/types'
import { useToastStore } from '@/platform/updates/common/toastStore'
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 props = defineProps<{
missingNodeTypes: MissingNodeType[]
}>()
const dialogStore = useDialogStore()
const { t } = useI18n()
// Get missing node packs from workflow with loading and error states
const { missingNodePacks, isLoading, error, missingCoreNodes } =
useMissingNodes()
const handleGotItClick = () => {
dialogStore.closeDialog({ key: 'global-missing-nodes' })
}
const { missingNodePacks, isLoading, error } = useMissingNodes()
const comfyManagerStore = useComfyManagerStore()
const managerState = useManagerState()
@@ -91,27 +86,6 @@ const isInstalling = computed(() => {
)
})
const uniqueNodes = computed(() => {
const seenTypes = new Set()
return props.missingNodeTypes
.filter((node) => {
const type = typeof node === 'object' ? node.type : node
if (seenTypes.has(type)) return false
seenTypes.add(type)
return true
})
.map((node) => {
if (typeof node === 'object') {
return {
label: node.type,
hint: node.hint,
action: node.action
}
}
return { label: node }
})
})
// Show manager buttons unless manager is disabled
const showManagerButtons = computed(() => {
return managerState.shouldShowManagerButtons.value
@@ -129,9 +103,6 @@ const openManager = async () => {
})
}
const { t } = useI18n()
const dialogStore = useDialogStore()
// Computed to check if all missing nodes have been installed
const allMissingNodesInstalled = computed(() => {
return (
@@ -140,13 +111,14 @@ const allMissingNodesInstalled = computed(() => {
missingNodePacks.value?.length === 0
)
})
// Watch for completion and close dialog
// Watch for completion and close dialog (OSS mode only)
watch(allMissingNodesInstalled, async (allInstalled) => {
if (allInstalled && showInstallAllButton.value) {
if (!isCloud && allInstalled && showInstallAllButton.value) {
// Use nextTick to ensure state updates are complete
await nextTick()
dialogStore.closeDialog({ key: 'global-load-workflow-warning' })
dialogStore.closeDialog({ key: 'global-missing-nodes' })
// Show success toast
useToastStore().add({
@@ -158,20 +130,3 @@ watch(allMissingNodesInstalled, async (allInstalled) => {
}
})
</script>
<style scoped>
.comfy-missing-nodes {
max-height: 300px;
overflow-y: auto;
}
.node-hint {
margin-left: 0.5rem;
font-style: italic;
color: var(--text-color-secondary);
}
:deep(.p-button) {
margin-left: auto;
}
</style>

View File

@@ -3,8 +3,16 @@
<div class="flex items-center gap-2">
<i class="icon-[lucide--triangle-alert] text-gold-600"></i>
<p class="m-0 text-sm">
{{ $t('cloud.missingNodes.title') }}
{{
isCloud
? $t('missingNodes.cloud.title')
: $t('missingNodes.oss.title')
}}
</p>
</div>
</div>
</template>
<script setup lang="ts">
import { isCloud } from '@/platform/distribution/types'
</script>

View File

@@ -289,7 +289,7 @@
"lastUpdated": "Last Updated",
"noDescription": "No description available",
"installSelected": "Install Selected",
"installAllMissingNodes": "Install All Missing Nodes",
"installAllMissingNodes": "Install All",
"allMissingNodesInstalled": "All missing nodes have been successfully installed",
"packsSelected": "packs selected",
"mixedSelectionMessage": "Cannot perform bulk action on mixed selection",
@@ -1423,13 +1423,6 @@
"missingModels": "Missing Models",
"missingModelsMessage": "When loading the graph, the following models were not found"
},
"loadWorkflowWarning": {
"missingNodesTitle": "Some Nodes Are Missing",
"missingNodesDescription": "When loading the graph, the following node types were not found.\nThis may also happen if your installed version is lower and that node type cant be found.",
"outdatedVersion": "Some nodes require a newer version of ComfyUI (current: {version}). Please update to use all nodes.",
"outdatedVersionGeneric": "Some nodes require a newer version of ComfyUI. Please update to use all nodes.",
"coreNodesFromVersion": "Requires ComfyUI {version}:"
},
"versionMismatchWarning": {
"title": "Version Compatibility Warning",
"frontendOutdated": "Frontend version {frontendVersion} is outdated. Backend requires version {requiredVersion} or higher.",
@@ -2078,16 +2071,19 @@
"vueNodesMigrationMainMenu": {
"message": "Switch back to Nodes 2.0 anytime from the main menu."
},
"cloud": {
"missingNodes": {
"missingNodes": {
"cloud": {
"title": "These nodes aren't available on Comfy Cloud yet",
"description": "This workflow uses custom nodes that aren't supported in the Cloud version yet.",
"priorityMessage": "We've automatically flagged these nodes so we can prioritize adding them.",
"missingNodes": "Missing Nodes",
"replacementInstruction": "In the meantime, replace these nodes (highlighted red on the canvas) with supported ones if possible, or try a different workflow.",
"learnMore": "Learn more",
"gotIt": "Ok, got it",
"cannotRun": "Workflow contains unsupported nodes (highlighted red). Remove these to run the workflow. "
"gotIt": "Ok, got it"
},
"oss": {
"title": "This workflow has missing nodes",
"description": "This workflow uses custom nodes you haven't installed yet.",
"replacementInstruction": "Install these nodes to run this workflow, or replace them with installed alternatives. Missing nodes are highlighted in red on the canvas."
}
}
}

View File

@@ -1018,11 +1018,7 @@ export class ComfyApp {
private showMissingNodesError(missingNodeTypes: MissingNodeType[]) {
if (useSettingStore().get('Comfy.Workflow.ShowMissingNodesWarning')) {
if (isCloud) {
useDialogService().showCloudLoadWorkflowWarning({ missingNodeTypes })
} else {
useDialogService().showLoadWorkflowWarning({ missingNodeTypes })
}
useDialogService().showLoadWorkflowWarning({ missingNodeTypes })
}
}

View File

@@ -2,12 +2,11 @@ import { merge } from 'es-toolkit/compat'
import type { Component } from 'vue'
import ApiNodesSignInContent from '@/components/dialog/content/ApiNodesSignInContent.vue'
import CloudMissingNodesContent from '@/components/dialog/content/CloudMissingNodesContent.vue'
import CloudMissingNodesFooter from '@/components/dialog/content/CloudMissingNodesFooter.vue'
import CloudMissingNodesHeader from '@/components/dialog/content/CloudMissingNodesHeader.vue'
import MissingNodesContent from '@/components/dialog/content/MissingNodesContent.vue'
import MissingNodesFooter from '@/components/dialog/content/MissingNodesFooter.vue'
import MissingNodesHeader from '@/components/dialog/content/MissingNodesHeader.vue'
import ConfirmationDialogContent from '@/components/dialog/content/ConfirmationDialogContent.vue'
import ErrorDialogContent from '@/components/dialog/content/ErrorDialogContent.vue'
import LoadWorkflowWarning from '@/components/dialog/content/LoadWorkflowWarning.vue'
import MissingModelsWarning from '@/components/dialog/content/MissingModelsWarning.vue'
import PromptDialogContent from '@/components/dialog/content/PromptDialogContent.vue'
import SignInContent from '@/components/dialog/content/SignInContent.vue'
@@ -47,24 +46,15 @@ export type ConfirmationDialogType =
export const useDialogService = () => {
const dialogStore = useDialogStore()
function showLoadWorkflowWarning(
props: InstanceType<typeof LoadWorkflowWarning>['$props']
) {
dialogStore.showDialog({
key: 'global-load-workflow-warning',
component: LoadWorkflowWarning,
props
})
}
function showCloudLoadWorkflowWarning(
props: ComponentProps<typeof CloudMissingNodesContent>
function showLoadWorkflowWarning(
props: ComponentProps<typeof MissingNodesContent>
) {
dialogStore.showDialog({
key: 'global-cloud-missing-nodes',
headerComponent: CloudMissingNodesHeader,
footerComponent: CloudMissingNodesFooter,
component: CloudMissingNodesContent,
key: 'global-missing-nodes',
headerComponent: MissingNodesHeader,
footerComponent: MissingNodesFooter,
component: MissingNodesContent,
dialogComponentProps: {
closable: true,
pt: {
@@ -550,7 +540,6 @@ export const useDialogService = () => {
return {
showLoadWorkflowWarning,
showCloudLoadWorkflowWarning,
showMissingModelsWarning,
showSettingsDialog,
showAboutDialog,

View File

@@ -1,9 +1,8 @@
<template>
<IconTextButton
v-bind="$attrs"
type="transparent"
type="secondary"
:label="computedLabel"
:border="true"
:size="size"
:disabled="isLoading || isInstalling"
@click="installAllPacks"