Files
ComfyUI_frontend/src/services/dialogService.ts
Johnpaul Chiwetelu d954336973 New Workflow Templates Modal (#5142)
This pull request refactors and simplifies the template workflow card
components and related UI in the codebase. The main changes focus on
removing unused or redundant components, improving visual and
interaction consistency, and enhancing error handling for images. Below
are the most important changes grouped by theme:

**Template Workflow Card Refactor and Cleanup**

* Removed the `TemplateWorkflowCard.vue` component and its associated
test file `TemplateWorkflowCard.spec.ts`, as well as the
`TemplateWorkflowCardSkeleton.vue` and `TemplateWorkflowList.vue`
components, indicating a shift away from the previous card-based
template workflow UI.
[[1]](diffhunk://#diff-49569af0404058e8257f3cc0716b066517ce7397dd58744b02aa0d0c61f2a815L1-L139)
[[2]](diffhunk://#diff-9fa6fc1470371f0b520d4deda4129fb313b1bea69888a376556f4bd824f9d751L1-L263)
[[3]](diffhunk://#diff-bc35b6f77d1cee6e86b05d0da80b7bd40013c7a6a97a89706d3bc52573e1c574L1-L30)
[[4]](diffhunk://#diff-48171f792b22022526fca411d3c3a366d48b675dab77943a20846ae079cbaf3bL1-L68)
* Removed the `TemplateSearchBar.vue` component, suggesting a redesign
or replacement of the search/filter UI for templates.

**UI and Interaction Improvements**

* Improved the `CardBottom.vue` component by making its height
configurable via a `fullHeight` prop, enhancing layout flexibility.
* Updated the `CardContainer.vue` component to add hover effects
(background, border, shadow, and padding) and support a new `none`
aspect ratio for more flexible card layouts.

**Image and Input Enhancements**

* Enhanced the `LazyImage.vue` component to display a default
placeholder image when an image fails to load, improving error handling
and user experience.
* Improved the `SearchBox.vue` component by making the input focusable
when clicking anywhere on the wrapper, and added a template ref for
better accessibility and usability.
[[1]](diffhunk://#diff-08f3b0c51fbfe63171509b9944bf7558228f6c2596a1ef5338e88ab64585791bL2-R5)
[[2]](diffhunk://#diff-08f3b0c51fbfe63171509b9944bf7558228f6c2596a1ef5338e88ab64585791bL16-R17)
[[3]](diffhunk://#diff-08f3b0c51fbfe63171509b9944bf7558228f6c2596a1ef5338e88ab64585791bR33-R39)

**Minor UI Tweaks**

* Adjusted label styling in `SingleSelect.vue` to remove unnecessary
overflow handling, simplifying the visual layout.

---------

Co-authored-by: Benjamin Lu <benceruleanlu@proton.me>
Co-authored-by: Alexander Brown <drjkl@comfy.org>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Christian Byrne <cbyrne@comfy.org>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: snomiao <snomiao@gmail.com>
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: filtered <176114999+webfiltered@users.noreply.github.com>
Co-authored-by: Comfy Org PR Bot <snomiao+comfy-pr@gmail.com>
Co-authored-by: Jin Yi <jin12cc@gmail.com>
2025-09-26 11:52:19 -07:00

511 lines
14 KiB
TypeScript

import { merge } from 'es-toolkit/compat'
import type { Component } from 'vue'
import ApiNodesSignInContent from '@/components/dialog/content/ApiNodesSignInContent.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'
import TopUpCreditsDialogContent from '@/components/dialog/content/TopUpCreditsDialogContent.vue'
import UpdatePasswordContent from '@/components/dialog/content/UpdatePasswordContent.vue'
import ComfyOrgHeader from '@/components/dialog/header/ComfyOrgHeader.vue'
import SettingDialogHeader from '@/components/dialog/header/SettingDialogHeader.vue'
import { t } from '@/i18n'
import SettingDialogContent from '@/platform/settings/components/SettingDialogContent.vue'
import type { ExecutionErrorWsMessage } from '@/schemas/apiSchema'
import {
type DialogComponentProps,
type ShowDialogOptions,
useDialogStore
} from '@/stores/dialogStore'
import ManagerProgressDialogContent from '@/workbench/extensions/manager/components/ManagerProgressDialogContent.vue'
import ManagerProgressFooter from '@/workbench/extensions/manager/components/ManagerProgressFooter.vue'
import ManagerProgressHeader from '@/workbench/extensions/manager/components/ManagerProgressHeader.vue'
import ManagerDialogContent from '@/workbench/extensions/manager/components/manager/ManagerDialogContent.vue'
import ManagerHeader from '@/workbench/extensions/manager/components/manager/ManagerHeader.vue'
import NodeConflictDialogContent from '@/workbench/extensions/manager/components/manager/NodeConflictDialogContent.vue'
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'
export type ConfirmationDialogType =
| 'default'
| 'overwrite'
| 'overwriteBlueprint'
| 'delete'
| 'dirtyClose'
| 'reinstall'
export const useDialogService = () => {
const dialogStore = useDialogStore()
function showLoadWorkflowWarning(
props: InstanceType<typeof LoadWorkflowWarning>['$props']
) {
dialogStore.showDialog({
key: 'global-load-workflow-warning',
component: LoadWorkflowWarning,
props
})
}
function showMissingModelsWarning(
props: InstanceType<typeof MissingModelsWarning>['$props']
) {
dialogStore.showDialog({
key: 'global-missing-models-warning',
component: MissingModelsWarning,
props
})
}
function showSettingsDialog(
panel?:
| 'about'
| 'keybinding'
| 'extension'
| 'server-config'
| 'user'
| 'credits'
) {
const props = panel ? { props: { defaultPanel: panel } } : undefined
dialogStore.showDialog({
key: 'global-settings',
headerComponent: SettingDialogHeader,
component: SettingDialogContent,
...props
})
}
function showAboutDialog() {
dialogStore.showDialog({
key: 'global-settings',
headerComponent: SettingDialogHeader,
component: SettingDialogContent,
props: {
defaultPanel: 'about'
}
})
}
function showExecutionErrorDialog(executionError: ExecutionErrorWsMessage) {
const props: InstanceType<typeof ErrorDialogContent>['$props'] = {
error: {
exceptionType: executionError.exception_type,
exceptionMessage: executionError.exception_message,
nodeId: executionError.node_id?.toString(),
nodeType: executionError.node_type,
traceback: executionError.traceback.join('\n'),
reportType: 'graphExecutionError'
}
}
dialogStore.showDialog({
key: 'global-execution-error',
component: ErrorDialogContent,
props
})
}
function showManagerDialog(
props: InstanceType<typeof ManagerDialogContent>['$props'] = {}
) {
dialogStore.showDialog({
key: 'global-manager',
component: ManagerDialogContent,
headerComponent: ManagerHeader,
dialogComponentProps: {
closable: true,
pt: {
pcCloseButton: {
root: {
class:
'bg-gray-500 dark-theme:bg-neutral-700 w-9 h-9 p-1.5 rounded-full text-white'
}
},
header: { class: 'py-0! px-6 m-0! h-[68px]' },
content: {
class: 'p-0! h-full w-[90vw] max-w-full flex-1 overflow-hidden'
},
root: { class: 'manager-dialog' }
}
},
props
})
}
function parseError(error: Error) {
const filename =
'fileName' in error
? (error.fileName as string)
: error.stack?.match(/(\/extensions\/.*\.js)/)?.[1]
const extensionFile = filename
? filename.substring(filename.indexOf('/extensions/'))
: undefined
return {
errorMessage: error.toString(),
stackTrace: error.stack,
extensionFile
}
}
/**
* Show a error dialog to the user when an error occurs.
* @param error The error to show
* @param options The options for the dialog
*/
function showErrorDialog(
error: unknown,
options: {
title?: string
reportType?: string
} = {}
) {
const errorProps: {
errorMessage: string
stackTrace?: string
extensionFile?: string
} =
error instanceof Error
? parseError(error)
: {
errorMessage: String(error)
}
const props: InstanceType<typeof ErrorDialogContent>['$props'] = {
error: {
exceptionType: options.title ?? 'Unknown Error',
exceptionMessage: errorProps.errorMessage,
traceback: errorProps.stackTrace ?? t('errorDialog.noStackTrace'),
reportType: options.reportType
}
}
dialogStore.showDialog({
key: 'global-error',
component: ErrorDialogContent,
props
})
}
function showManagerProgressDialog(options?: {
props?: InstanceType<typeof ManagerProgressDialogContent>['$props']
}) {
return dialogStore.showDialog({
key: 'global-manager-progress-dialog',
component: ManagerProgressDialogContent,
headerComponent: ManagerProgressHeader,
footerComponent: ManagerProgressFooter,
props: options?.props,
priority: 2,
dialogComponentProps: {
closable: false,
modal: false,
position: 'bottom',
pt: {
root: { class: 'w-[80%] max-w-2xl mx-auto border-none' },
content: { class: 'p-0!' },
header: { class: 'p-0! border-none' },
footer: { class: 'p-0! border-none' }
}
}
})
}
/**
* Shows a dialog requiring sign in for API nodes
* @returns Promise that resolves to true if user clicks login, false if cancelled
*/
async function showApiNodesSignInDialog(
apiNodeNames: string[]
): Promise<boolean> {
return new Promise<boolean>((resolve) => {
dialogStore.showDialog({
key: 'api-nodes-signin',
component: ApiNodesSignInContent,
props: {
apiNodeNames,
onLogin: () => showSignInDialog().then((result) => resolve(result)),
onCancel: () => resolve(false)
},
headerComponent: ComfyOrgHeader,
dialogComponentProps: {
closable: false,
onClose: () => resolve(false)
}
})
}).then((result) => {
dialogStore.closeDialog({ key: 'api-nodes-signin' })
return result
})
}
async function showSignInDialog(): Promise<boolean> {
return new Promise<boolean>((resolve) => {
dialogStore.showDialog({
key: 'global-signin',
component: SignInContent,
headerComponent: ComfyOrgHeader,
props: {
onSuccess: () => resolve(true)
},
dialogComponentProps: {
closable: true,
onClose: () => resolve(false)
}
})
}).then((result) => {
dialogStore.closeDialog({ key: 'global-signin' })
return result
})
}
async function prompt({
title,
message,
defaultValue = ''
}: {
title: string
message: string
defaultValue?: string
}): Promise<string | null> {
return new Promise((resolve) => {
dialogStore.showDialog({
key: 'global-prompt',
title,
component: PromptDialogContent,
props: {
message,
defaultValue,
onConfirm: (value: string) => {
resolve(value)
}
},
dialogComponentProps: {
onClose: () => {
resolve(null)
}
}
})
})
}
/**
* @returns `true` if the user confirms the dialog,
* `false` if denied (e.g. no in yes/no/cancel), or
* `null` if the dialog is cancelled or closed
*/
async function confirm({
title,
message,
type = 'default',
itemList = [],
hint
}: {
/** Dialog heading */
title: string
/** The main message body */
message: string
/** Pre-configured dialog type */
type?: ConfirmationDialogType
/** Displayed as an unordered list immediately below the message body */
itemList?: string[]
hint?: string
}): Promise<boolean | null> {
return new Promise((resolve) => {
const options: ShowDialogOptions = {
key: 'global-prompt',
title,
component: ConfirmationDialogContent,
props: {
message,
type,
itemList,
onConfirm: resolve,
hint
},
dialogComponentProps: {
onClose: () => resolve(null)
}
}
dialogStore.showDialog(options)
})
}
function showTopUpCreditsDialog(options?: {
isInsufficientCredits?: boolean
}) {
return dialogStore.showDialog({
key: 'top-up-credits',
component: TopUpCreditsDialogContent,
headerComponent: ComfyOrgHeader,
props: options,
dialogComponentProps: {
pt: {
header: { class: 'p-3!' }
}
}
})
}
/**
* Shows a dialog for updating the current user's password.
*/
function showUpdatePasswordDialog() {
return dialogStore.showDialog({
key: 'global-update-password',
component: UpdatePasswordContent,
headerComponent: ComfyOrgHeader,
props: {
onSuccess: () =>
dialogStore.closeDialog({ key: 'global-update-password' })
}
})
}
/**
* Shows a dialog from a third party extension.
* @param options - The dialog options.
* @param options.key - The dialog key.
* @param options.title - The dialog title.
* @param options.headerComponent - The dialog header component.
* @param options.footerComponent - The dialog footer component.
* @param options.component - The dialog component.
* @param options.props - The dialog props.
* @returns The dialog instance and a function to close the dialog.
*/
function showExtensionDialog(options: ShowDialogOptions & { key: string }) {
return {
dialog: dialogStore.showExtensionDialog(options),
closeDialog: () => dialogStore.closeDialog({ key: options.key })
}
}
function toggleManagerDialog(
props?: InstanceType<typeof ManagerDialogContent>['$props']
) {
if (dialogStore.isDialogOpen('global-manager')) {
dialogStore.closeDialog({ key: 'global-manager' })
} else {
showManagerDialog(props)
}
}
function toggleManagerProgressDialog(
props?: InstanceType<typeof ManagerProgressDialogContent>['$props']
) {
if (dialogStore.isDialogOpen('global-manager-progress-dialog')) {
dialogStore.closeDialog({ key: 'global-manager-progress-dialog' })
} else {
showManagerProgressDialog({ props })
}
}
function showLayoutDialog(options: {
key: string
component: Component
props: { onClose: () => void }
dialogComponentProps?: DialogComponentProps
}) {
const layoutDefaultProps: DialogComponentProps = {
headless: true,
modal: true,
closable: false,
pt: {
root: {
class: 'rounded-2xl overflow-hidden'
},
header: {
class: 'p-0! hidden'
},
content: {
class: 'p-0! m-0!'
}
}
}
return dialogStore.showDialog({
...options,
dialogComponentProps: merge(
layoutDefaultProps,
options.dialogComponentProps || {}
)
})
}
function showNodeConflictDialog(
options: {
showAfterWhatsNew?: boolean
conflictedPackages?: ConflictDetectionResult[]
dialogComponentProps?: DialogComponentProps
buttonText?: string
onButtonClick?: () => void
} = {}
) {
const {
dialogComponentProps,
buttonText,
onButtonClick,
showAfterWhatsNew,
conflictedPackages
} = options
return dialogStore.showDialog({
key: 'global-node-conflict',
headerComponent: NodeConflictHeader,
footerComponent: NodeConflictFooter,
component: NodeConflictDialogContent,
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 bg-gray-500 dark-theme:bg-neutral-700 text-white'
}
}
},
...dialogComponentProps
},
props: {
showAfterWhatsNew,
conflictedPackages
},
footerProps: {
buttonText,
onButtonClick
}
})
}
return {
showLoadWorkflowWarning,
showMissingModelsWarning,
showSettingsDialog,
showAboutDialog,
showExecutionErrorDialog,
showManagerDialog,
showManagerProgressDialog,
showApiNodesSignInDialog,
showSignInDialog,
showTopUpCreditsDialog,
showUpdatePasswordDialog,
showExtensionDialog,
prompt,
showErrorDialog,
confirm,
toggleManagerDialog,
toggleManagerProgressDialog,
showLayoutDialog,
showNodeConflictDialog
}
}