mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-23 08:14:06 +00:00
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>
511 lines
14 KiB
TypeScript
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
|
|
}
|
|
}
|