{
- let filename = 'workflow.json'
- if (useSettingStore().get('Comfy.PromptFilename')) {
- filename = prompt('Save workflow as:', filename)
- if (!filename) return
- if (!filename.toLowerCase().endsWith('.json')) {
- filename += '.json'
- }
- }
- app.graphToPrompt().then((p) => {
- const json = JSON.stringify(p.workflow, null, 2) // convert the data to a JSON string
- const blob = new Blob([json], { type: 'application/json' })
- const url = URL.createObjectURL(blob)
- const a = $el('a', {
- href: url,
- download: filename,
- style: { display: 'none' },
- parent: document.body
- })
- a.click()
- setTimeout(function () {
- a.remove()
- window.URL.revokeObjectURL(url)
- }, 0)
- })
+ useCommandStore().execute('Comfy.ExportWorkflow')
}
}),
$el('button', {
@@ -616,30 +594,7 @@ export class ComfyUI {
textContent: 'Save (API Format)',
style: { width: '100%', display: 'none' },
onclick: () => {
- let filename = 'workflow_api.json'
- if (useSettingStore().get('Comfy.PromptFilename')) {
- filename = prompt('Save workflow (API) as:', filename)
- if (!filename) return
- if (!filename.toLowerCase().endsWith('.json')) {
- filename += '.json'
- }
- }
- app.graphToPrompt().then((p) => {
- const json = JSON.stringify(p.output, null, 2) // convert the data to a JSON string
- const blob = new Blob([json], { type: 'application/json' })
- const url = URL.createObjectURL(blob)
- const a = $el('a', {
- href: url,
- download: filename,
- style: { display: 'none' },
- parent: document.body
- })
- a.click()
- setTimeout(function () {
- a.remove()
- window.URL.revokeObjectURL(url)
- }, 0)
- })
+ useCommandStore().execute('Comfy.ExportWorkflowAPI')
}
}),
$el('button', {
diff --git a/src/scripts/ui/menu/index.ts b/src/scripts/ui/menu/index.ts
index 0e708e443..17dd88af3 100644
--- a/src/scripts/ui/menu/index.ts
+++ b/src/scripts/ui/menu/index.ts
@@ -2,6 +2,8 @@ import type { ComfyApp } from '@/scripts/app'
import { $el } from '../../ui'
import { downloadBlob } from '../../utils'
import { ComfyButtonGroup } from '../components/buttonGroup'
+import { showPromptDialog } from '@/services/dialogService'
+import { useSettingStore } from '@/stores/settingStore'
import './menu.css'
// Export to make sure following components are shimmed and exported by vite
@@ -32,13 +34,18 @@ export class ComfyAppMenu {
])
}
- getFilename(defaultName: string) {
- if (this.app.ui.settings.getSettingValue('Comfy.PromptFilename', true)) {
- defaultName = prompt('Save workflow as:', defaultName)
- if (!defaultName) return
- if (!defaultName.toLowerCase().endsWith('.json')) {
- defaultName += '.json'
+ async getFilename(defaultName: string): Promise {
+ if (useSettingStore().get('Comfy.PromptFilename')) {
+ let filename = await showPromptDialog({
+ title: 'Export Workflow',
+ message: 'Enter the filename:',
+ defaultValue: defaultName
+ })
+ if (!filename) return null
+ if (!filename.toLowerCase().endsWith('.json')) {
+ filename += '.json'
}
+ return filename
}
return defaultName
}
@@ -46,14 +53,14 @@ export class ComfyAppMenu {
async exportWorkflow(
filename: string,
promptProperty: 'workflow' | 'output'
- ) {
+ ): Promise {
if (this.app.workflowManager.activeWorkflow?.path) {
filename = this.app.workflowManager.activeWorkflow.name
}
const p = await this.app.graphToPrompt()
const json = JSON.stringify(p[promptProperty], null, 2)
const blob = new Blob([json], { type: 'application/json' })
- const file = this.getFilename(filename)
+ const file = await this.getFilename(filename)
if (!file) return
downloadBlob(file, blob)
}
diff --git a/src/services/dialogService.ts b/src/services/dialogService.ts
index 86cc8fd4a..6bda88f7a 100644
--- a/src/services/dialogService.ts
+++ b/src/services/dialogService.ts
@@ -9,6 +9,7 @@ import SettingDialogHeader from '@/components/dialog/header/SettingDialogHeader.
import type { ExecutionErrorWsMessage } from '@/types/apiTypes'
import ExecutionErrorDialogContent from '@/components/dialog/content/ExecutionErrorDialogContent.vue'
import TemplateWorkflowsContent from '@/components/templates/TemplateWorkflowsContent.vue'
+import PromptDialogContent from '@/components/dialog/content/PromptDialogContent.vue'
import { i18n } from '@/i18n'
export function showLoadWorkflowWarning(props: {
@@ -19,7 +20,10 @@ export function showLoadWorkflowWarning(props: {
const dialogStore = useDialogStore()
dialogStore.showDialog({
component: LoadWorkflowWarning,
- props
+ props,
+ dialogComponentProps: {
+ maximizable: true
+ }
})
}
@@ -31,7 +35,10 @@ export function showMissingModelsWarning(props: {
const dialogStore = useDialogStore()
dialogStore.showDialog({
component: MissingModelsWarning,
- props
+ props,
+ dialogComponentProps: {
+ maximizable: true
+ }
})
}
@@ -57,3 +64,34 @@ export function showTemplateWorkflowsDialog() {
component: TemplateWorkflowsContent
})
}
+
+export async function showPromptDialog({
+ title,
+ message,
+ defaultValue = ''
+}: {
+ title: string
+ message: string
+ defaultValue?: string
+}): Promise {
+ const dialogStore = useDialogStore()
+
+ return new Promise((resolve) => {
+ dialogStore.showDialog({
+ title,
+ component: PromptDialogContent,
+ props: {
+ message,
+ defaultValue,
+ onConfirm: (value: string) => {
+ resolve(value)
+ }
+ },
+ dialogComponentProps: {
+ onClose: () => {
+ resolve(null)
+ }
+ }
+ })
+ })
+}
diff --git a/src/stores/commandStore.ts b/src/stores/commandStore.ts
index 7263d2b9a..918a7bebd 100644
--- a/src/stores/commandStore.ts
+++ b/src/stores/commandStore.ts
@@ -320,6 +320,13 @@ export const useCommandStore = defineStore('command', () => {
return commandsById.value[command]
}
+ const execute = (commandId: string) => {
+ const command = getCommand(commandId)
+ if (command) {
+ command.function()
+ }
+ }
+
const isRegistered = (command: string) => {
return !!commandsById.value[command]
}
@@ -334,6 +341,7 @@ export const useCommandStore = defineStore('command', () => {
return {
commands,
+ execute,
getCommand,
getCommandFunction,
registerCommand,
diff --git a/src/stores/dialogStore.ts b/src/stores/dialogStore.ts
index 67ec6dab0..86ba93e32 100644
--- a/src/stores/dialogStore.ts
+++ b/src/stores/dialogStore.ts
@@ -2,14 +2,22 @@
// Currently we need to bridge between legacy app code and Vue app with a Pinia store.
import { defineStore } from 'pinia'
-import { type Component, markRaw } from 'vue'
+import { type Component, markRaw, nextTick } from 'vue'
interface DialogState {
isVisible: boolean
title: string
headerComponent: Component | null
component: Component | null
+ // Props passing to the component
props: Record
+ // Props passing to the Dialog component
+ dialogComponentProps: DialogComponentProps
+}
+
+interface DialogComponentProps {
+ maximizable?: boolean
+ onClose?: () => void
}
export const useDialogStore = defineStore('dialog', {
@@ -18,7 +26,8 @@ export const useDialogStore = defineStore('dialog', {
title: '',
headerComponent: null,
component: null,
- props: {}
+ props: {},
+ dialogComponentProps: {}
}),
actions: {
@@ -27,15 +36,22 @@ export const useDialogStore = defineStore('dialog', {
headerComponent?: Component
component: Component
props?: Record
+ dialogComponentProps?: DialogComponentProps
}) {
- this.title = options.title
- this.headerComponent = markRaw(options.headerComponent)
- this.component = markRaw(options.component)
- this.props = options.props || {}
this.isVisible = true
+ nextTick(() => {
+ this.title = options.title
+ this.headerComponent = markRaw(options.headerComponent)
+ this.component = markRaw(options.component)
+ this.props = options.props || {}
+ this.dialogComponentProps = options.dialogComponentProps || {}
+ })
},
closeDialog() {
+ if (this.dialogComponentProps.onClose) {
+ this.dialogComponentProps.onClose()
+ }
this.isVisible = false
}
}
diff --git a/tests-ui/globalSetup.ts b/tests-ui/globalSetup.ts
index 16c6d9c22..b9c81d287 100644
--- a/tests-ui/globalSetup.ts
+++ b/tests-ui/globalSetup.ts
@@ -16,7 +16,15 @@ module.exports = async function () {
jest.mock('@/services/dialogService', () => {
return {
showLoadWorkflowWarning: jest.fn(),
- showMissingModelsWarning: jest.fn()
+ showMissingModelsWarning: jest.fn(),
+ showSettingsDialog: jest.fn(),
+ showExecutionErrorDialog: jest.fn(),
+ showTemplateWorkflowsDialog: jest.fn(),
+ showPromptDialog: jest
+ .fn()
+ .mockImplementation((message, defaultValue) => {
+ return Promise.resolve(defaultValue)
+ })
}
})
}