mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-23 16:24:06 +00:00
[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:
@@ -32,7 +32,7 @@ defineOptions({
|
||||
interface IconTextButtonProps extends BaseButtonProps {
|
||||
iconPosition?: 'left' | 'right'
|
||||
label: string
|
||||
onClick: () => void
|
||||
onClick?: () => void
|
||||
}
|
||||
|
||||
const {
|
||||
|
||||
@@ -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>
|
||||
@@ -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
|
||||
})
|
||||
|
||||
@@ -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
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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 can’t 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."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
<template>
|
||||
<IconTextButton
|
||||
v-bind="$attrs"
|
||||
type="transparent"
|
||||
type="secondary"
|
||||
:label="computedLabel"
|
||||
:border="true"
|
||||
:size="size"
|
||||
:disabled="isLoading || isInstalling"
|
||||
@click="installAllPacks"
|
||||
|
||||
Reference in New Issue
Block a user