Add cloud-specific missing nodes warning dialog (#6659)

## Summary
Implements a cloud-specific dialog to warn users when loading workflows
with unsupported custom nodes in Comfy Cloud. The new dialog follows the
visual style of the node conflict dialog and provides appropriate
messaging and actions.

## Changes
- Add `CloudMissingNodesHeader`, `CloudMissingNodesContent`, and
`CloudMissingNodesFooter` components
- Add `showCloudLoadWorkflowWarning` to dialogService
- Update app.ts to show cloud dialog when in cloud environment  
- Add `cloud.missingNodes` translations

## Screenshots
The dialog displays:
- Warning icon and title
- Description of the issue
- List of missing nodes
- "Learn more" link and "Ok, got it" button

## Test plan
1. Load a workflow with custom nodes in cloud environment
2. Verify cloud-specific dialog appears with appropriate styling
3. Verify "Learn more" button opens cloud documentation
4. Verify "Ok, got it" button closes dialog

## Notes
- Two unused i18n keys (`cloud.missingNodes.cannotRun` and
`cloud.missingNodes.missingNodes`) are included for future PR that will
add breadcrumb warning icons and run button disable functionality

<img width="1367" height="988" alt="스크린샷 2025-11-12 오후 4 33 38"
src="https://github.com/user-attachments/assets/75a6fced-959f-4e93-9b82-4e61b53a9ee4"
/>

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

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6659-Add-cloud-specific-missing-nodes-warning-dialog-2a96d73d36508161ae55fe157f55cd17)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Jin Yi
2025-11-12 19:03:09 +09:00
committed by GitHub
parent 2cf3739236
commit f97cf77e75
6 changed files with 164 additions and 1 deletions

View File

@@ -0,0 +1,70 @@
<template>
<div class="flex w-[490px] flex-col">
<ContentDivider :width="1" />
<div class="flex h-full w-full flex-col gap-4 p-4">
<!-- Description -->
<div>
<p class="m-0 text-sm leading-4 text-muted-foreground">
{{ $t('cloud.missingNodes.description') }}
<br /><br />
{{ $t('cloud.missingNodes.priorityMessage') }}
</p>
</div>
<!-- Missing Nodes List Wrapper -->
<div
class="flex flex-col max-h-[256px] rounded-lg py-2 scrollbar-custom bg-component-node-widget-background"
>
<div
v-for="(node, i) in uniqueNodes"
:key="i"
class="flex min-h-8 items-center justify-between px-4 py-2 bg-component-node-widget-background text-text-secondary"
>
<span class="text-xs">
{{ node.label }}
</span>
</div>
</div>
<!-- Bottom instruction -->
<div>
<p class="m-0 text-sm leading-4 text-muted-foreground">
{{ $t('cloud.missingNodes.replacementInstruction') }}
</p>
</div>
</div>
<ContentDivider :width="1" />
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import ContentDivider from '@/components/common/ContentDivider.vue'
import type { MissingNodeType } from '@/types/comfy'
const props = defineProps<{
missingNodeTypes: MissingNodeType[]
}>()
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 }
})
})
</script>

View File

@@ -0,0 +1,37 @@
<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

@@ -0,0 +1,10 @@
<template>
<div class="flex w-full items-center justify-between p-4">
<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') }}
</p>
</div>
</div>
</template>

View File

@@ -2049,6 +2049,18 @@
"vueNodesBanner": {
"message": "Nodes just got a new look and feel",
"tryItOut": "Try it out"
},
"cloud": {
"missingNodes": {
"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. "
}
}
}

View File

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

View File

@@ -2,6 +2,9 @@ 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 ConfirmationDialogContent from '@/components/dialog/content/ConfirmationDialogContent.vue'
import ErrorDialogContent from '@/components/dialog/content/ErrorDialogContent.vue'
import LoadWorkflowWarning from '@/components/dialog/content/LoadWorkflowWarning.vue'
@@ -32,6 +35,7 @@ import NodeConflictDialogContent from '@/workbench/extensions/manager/components
import NodeConflictFooter from '@/workbench/extensions/manager/components/manager/NodeConflictFooter.vue'
import NodeConflictHeader from '@/workbench/extensions/manager/components/manager/NodeConflictHeader.vue'
import type { ConflictDetectionResult } from '@/workbench/extensions/manager/types/conflictDetectionTypes'
import type { ComponentProps } from 'vue-component-type-helpers'
export type ConfirmationDialogType =
| 'default'
@@ -53,6 +57,31 @@ export const useDialogService = () => {
})
}
function showCloudLoadWorkflowWarning(
props: ComponentProps<typeof CloudMissingNodesContent>
) {
dialogStore.showDialog({
key: 'global-cloud-missing-nodes',
headerComponent: CloudMissingNodesHeader,
footerComponent: CloudMissingNodesFooter,
component: CloudMissingNodesContent,
dialogComponentProps: {
closable: true,
pt: {
header: { class: '!p-0 !m-0' },
content: { class: '!p-0 overflow-y-hidden' },
footer: { class: '!p-0' },
pcCloseButton: {
root: {
class: '!w-7 !h-7 !border-none !outline-none !p-2 !m-1.5'
}
}
}
},
props
})
}
function showMissingModelsWarning(
props: InstanceType<typeof MissingModelsWarning>['$props']
) {
@@ -520,6 +549,7 @@ export const useDialogService = () => {
return {
showLoadWorkflowWarning,
showCloudLoadWorkflowWarning,
showMissingModelsWarning,
showSettingsDialog,
showAboutDialog,