mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-12 00:20:15 +00:00
Missing node dialog revamp (#322)
* Basic rework of load workflow warning dialog * Better style * Add vue jest support * Mock vue component in jest test * nit * Make dialog maximizable
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<ProgressSpinner v-if="isLoading" class="spinner"></ProgressSpinner>
|
||||
<BlockUI full-screen :blocked="isLoading" />
|
||||
<GlobalDialog />
|
||||
<GraphCanvas />
|
||||
</template>
|
||||
|
||||
@@ -15,6 +16,7 @@ import { useSettingStore } from './stores/settingStore'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useWorkspaceStore } from './stores/workspaceStateStore'
|
||||
import NodeLibrarySideBarTab from './components/sidebar/tabs/NodeLibrarySideBarTab.vue'
|
||||
import GlobalDialog from './components/dialog/GlobalDialog.vue'
|
||||
|
||||
const isLoading = computed<boolean>(() => useWorkspaceStore().spinner)
|
||||
const theme = computed<string>(() =>
|
||||
|
||||
34
src/components/dialog/GlobalDialog.vue
Normal file
34
src/components/dialog/GlobalDialog.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<!-- The main global dialog to show various things -->
|
||||
<template>
|
||||
<Dialog
|
||||
v-model:visible="dialogStore.isVisible"
|
||||
modal
|
||||
closable
|
||||
closeOnEscape
|
||||
dismissableMask
|
||||
:maximizable="dialogStore.props.maximizable ?? false"
|
||||
@hide="dialogStore.closeDialog"
|
||||
@maximize="maximized = true"
|
||||
@unmaximize="maximized = false"
|
||||
>
|
||||
<template #header v-if="dialogStore.title">
|
||||
<h3>{{ dialogStore.title }}</h3>
|
||||
</template>
|
||||
|
||||
<component
|
||||
:is="dialogStore.component"
|
||||
v-bind="dialogStore.props"
|
||||
:maximized="maximized"
|
||||
/>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
import Dialog from 'primevue/dialog'
|
||||
|
||||
const dialogStore = useDialogStore()
|
||||
|
||||
const maximized = ref(false)
|
||||
</script>
|
||||
138
src/components/dialog/content/LoadWorkflowWarning.vue
Normal file
138
src/components/dialog/content/LoadWorkflowWarning.vue
Normal file
@@ -0,0 +1,138 @@
|
||||
<template>
|
||||
<div class="comfy-missing-nodes">
|
||||
<h4 class="warning-title">Warning: Missing Node Types</h4>
|
||||
<p class="warning-description">
|
||||
When loading the graph, the following node types were not found:
|
||||
</p>
|
||||
<ListBox
|
||||
:options="uniqueNodes"
|
||||
optionLabel="label"
|
||||
scrollHeight="100%"
|
||||
:class="'missing-nodes-list' + (props.maximized ? ' maximized' : '')"
|
||||
:pt="{
|
||||
list: { class: 'border-none' }
|
||||
}"
|
||||
>
|
||||
<template #option="slotProps">
|
||||
<div class="missing-node-item">
|
||||
<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"
|
||||
@click="slotProps.option.action.callback"
|
||||
:label="slotProps.option.action.text"
|
||||
class="p-button-sm p-button-outlined"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</ListBox>
|
||||
<p v-if="hasAddedNodes" class="added-nodes-warning">
|
||||
Nodes that have failed to load will show as red on the graph.
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import ListBox from 'primevue/listbox'
|
||||
import Button from 'primevue/button'
|
||||
|
||||
interface NodeType {
|
||||
type: string
|
||||
hint?: string
|
||||
action?: {
|
||||
text: string
|
||||
callback: () => void
|
||||
}
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
missingNodeTypes: (string | NodeType)[]
|
||||
hasAddedNodes: boolean
|
||||
maximized: boolean
|
||||
}>()
|
||||
|
||||
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>
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--red-600: #dc3545;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style scoped>
|
||||
.comfy-missing-nodes {
|
||||
font-family: monospace;
|
||||
color: var(--red-600);
|
||||
padding: 1.5rem;
|
||||
background-color: var(--surface-ground);
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: var(--card-shadow);
|
||||
}
|
||||
|
||||
.warning-title {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.warning-description {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.missing-nodes-list {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.missing-nodes-list.maximized {
|
||||
max-height: unset;
|
||||
}
|
||||
|
||||
.missing-node-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.node-type {
|
||||
font-weight: 600;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.node-hint {
|
||||
margin-left: 0.5rem;
|
||||
font-style: italic;
|
||||
color: var(--text-color-secondary);
|
||||
}
|
||||
|
||||
:deep(.p-button) {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.added-nodes-warning {
|
||||
margin-top: 1rem;
|
||||
font-style: italic;
|
||||
}
|
||||
</style>
|
||||
@@ -43,6 +43,7 @@ import {
|
||||
} from '@/stores/nodeDefStore'
|
||||
import { Vector2 } from '@comfyorg/litegraph'
|
||||
import _ from 'lodash'
|
||||
import { showLoadWorkflowWarning } from '@/services/dialogService'
|
||||
|
||||
export const ANIM_PREVIEW_WIDGET = '$$comfy_animation_preview'
|
||||
|
||||
@@ -2123,62 +2124,13 @@ export class ComfyApp {
|
||||
}
|
||||
|
||||
showMissingNodesError(missingNodeTypes, hasAddedNodes = true) {
|
||||
let seenTypes = new Set()
|
||||
if (this.vueAppReady)
|
||||
showLoadWorkflowWarning({
|
||||
missingNodeTypes,
|
||||
hasAddedNodes,
|
||||
maximizable: true
|
||||
})
|
||||
|
||||
this.ui.dialog.show(
|
||||
$el('div.comfy-missing-nodes', [
|
||||
$el('span', {
|
||||
textContent:
|
||||
'When loading the graph, the following node types were not found: '
|
||||
}),
|
||||
$el(
|
||||
'ul',
|
||||
Array.from(new Set(missingNodeTypes))
|
||||
.map((t) => {
|
||||
let children = []
|
||||
if (typeof t === 'object') {
|
||||
// @ts-expect-error
|
||||
if (seenTypes.has(t.type)) return null
|
||||
// @ts-expect-error
|
||||
seenTypes.add(t.type)
|
||||
// @ts-expect-error
|
||||
children.push($el('span', { textContent: t.type }))
|
||||
// @ts-expect-error
|
||||
if (t.hint) {
|
||||
// @ts-expect-error
|
||||
children.push($el('span', { textContent: t.hint }))
|
||||
}
|
||||
// @ts-expect-error
|
||||
if (t.action) {
|
||||
children.push(
|
||||
$el('button', {
|
||||
// @ts-expect-error
|
||||
onclick: t.action.callback,
|
||||
// @ts-expect-error
|
||||
textContent: t.action.text
|
||||
})
|
||||
)
|
||||
}
|
||||
} else {
|
||||
if (seenTypes.has(t)) return null
|
||||
seenTypes.add(t)
|
||||
// @ts-expect-error
|
||||
children.push($el('span', { textContent: t }))
|
||||
}
|
||||
return $el('li', children)
|
||||
})
|
||||
.filter(Boolean)
|
||||
),
|
||||
...(hasAddedNodes
|
||||
? [
|
||||
$el('span', {
|
||||
textContent:
|
||||
'Nodes that have failed to load will show as red on the graph.'
|
||||
})
|
||||
]
|
||||
: [])
|
||||
])
|
||||
)
|
||||
this.logging.addEntry('Comfy.App', 'warn', {
|
||||
MissingNodes: missingNodeTypes
|
||||
})
|
||||
|
||||
15
src/services/dialogService.ts
Normal file
15
src/services/dialogService.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
import LoadWorkflowWarning from '@/components/dialog/content/LoadWorkflowWarning.vue'
|
||||
import { markRaw } from 'vue'
|
||||
|
||||
export function showLoadWorkflowWarning(props: {
|
||||
missingNodeTypes: any[]
|
||||
hasAddedNodes: boolean
|
||||
[key: string]: any
|
||||
}) {
|
||||
const dialogStore = useDialogStore()
|
||||
dialogStore.showDialog({
|
||||
component: markRaw(LoadWorkflowWarning),
|
||||
props
|
||||
})
|
||||
}
|
||||
38
src/stores/dialogStore.ts
Normal file
38
src/stores/dialogStore.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
// We should consider moving to https://primevue.org/dynamicdialog/ once everything is in Vue.
|
||||
// Currently we need to bridge between legacy app code and Vue app with a Pinia store.
|
||||
|
||||
import { defineStore } from 'pinia'
|
||||
import { Component } from 'vue'
|
||||
|
||||
interface DialogState {
|
||||
isVisible: boolean
|
||||
title: string
|
||||
component: Component | null
|
||||
props: Record<string, any>
|
||||
}
|
||||
|
||||
export const useDialogStore = defineStore('dialog', {
|
||||
state: (): DialogState => ({
|
||||
isVisible: false,
|
||||
title: '',
|
||||
component: null,
|
||||
props: {}
|
||||
}),
|
||||
|
||||
actions: {
|
||||
showDialog(options: {
|
||||
title?: string
|
||||
component: Component
|
||||
props?: Record<string, any>
|
||||
}) {
|
||||
this.title = options.title
|
||||
this.component = options.component
|
||||
this.props = options.props || {}
|
||||
this.isVisible = true
|
||||
},
|
||||
|
||||
closeDialog() {
|
||||
this.isVisible = false
|
||||
}
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user