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:
Chenlei Hu
2024-08-06 20:11:05 -04:00
committed by GitHub
parent 6fe2297cc1
commit 79469bd2b1
10 changed files with 599 additions and 55 deletions

View File

@@ -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>(() =>

View 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>

View 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>

View File

@@ -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
})

View 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
View 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
}
}
})