Add PromptDialog to replace window.prompt (#1104)

* Save file prompt dialog

* Don't download if dialog dismissed

* refactor

* style dialog

* nit

* Autofocus
This commit is contained in:
Chenlei Hu
2024-10-04 15:33:27 -04:00
committed by GitHub
parent 39d68bcdc4
commit ebc71b0e46
12 changed files with 148 additions and 76 deletions

View File

@@ -10,7 +10,7 @@
<script setup lang="ts">
import config from '@/config'
import { computed, onMounted, onBeforeUnmount } from 'vue'
import { computed, onMounted } from 'vue'
import { useWorkspaceStore } from '@/stores/workspaceStateStore'
import BlockUI from 'primevue/blockui'
import ProgressSpinner from 'primevue/progressspinner'

View File

@@ -11,6 +11,7 @@
@hide="dialogStore.closeDialog"
@maximize="onMaximize"
@unmaximize="onUnmaximize"
:pt="{ header: 'pb-0' }"
>
<template #header>
<component
@@ -30,7 +31,9 @@ import { useDialogStore } from '@/stores/dialogStore'
import Dialog from 'primevue/dialog'
const dialogStore = useDialogStore()
const maximizable = computed(() => dialogStore.props.maximizable ?? false)
const maximizable = computed(
() => dialogStore.dialogComponentProps.maximizable ?? false
)
const maximized = ref(false)
const onMaximize = () => {

View File

@@ -191,8 +191,4 @@ const openNewGithubIssue = async () => {
white-space: pre-wrap;
word-wrap: break-word;
}
.no-results-placeholder {
padding-top: 0;
}
</style>

View File

@@ -0,0 +1,43 @@
<template>
<div class="prompt-dialog-content flex flex-col gap-2 pt-8">
<FloatLabel>
<InputText
ref="inputRef"
v-model="inputValue"
@keyup.enter="onConfirm"
@focus="selectAllText"
autofocus
/>
<label>{{ message }}</label>
</FloatLabel>
<Button @click="onConfirm">{{ $t('confirm') }}</Button>
</div>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import InputText from 'primevue/inputtext'
import FloatLabel from 'primevue/floatlabel'
import { ref } from 'vue'
import { useDialogStore } from '@/stores/dialogStore'
const props = defineProps<{
message: string
defaultValue: string
onConfirm: (value: string) => void
}>()
const inputValue = ref<string>(props.defaultValue)
const onConfirm = () => {
props.onConfirm(inputValue.value)
useDialogStore().closeDialog()
}
const inputRef = ref(null)
const selectAllText = () => {
if (!inputRef.value) return
const inputElement = inputRef.value.$el
inputElement.setSelectionRange(0, inputElement.value.length)
}
</script>

View File

@@ -1,6 +1,6 @@
<template>
<div
class="flex flex-wrap content-around justify-around gap-4"
class="flex flex-wrap content-around justify-around gap-4 mt-4"
data-testid="template-workflows-content"
>
<div

View File

@@ -2231,8 +2231,7 @@ export class ComfyApp {
if (useSettingStore().get('Comfy.Workflow.ShowMissingNodesWarning')) {
showLoadWorkflowWarning({
missingNodeTypes,
hasAddedNodes,
maximizable: true
hasAddedNodes
})
}
@@ -2245,8 +2244,7 @@ export class ComfyApp {
if (useSettingStore().get('Comfy.Workflow.ShowMissingModelsWarning')) {
showMissingModelsWarning({
missingModels,
paths,
maximizable: true
paths
})
}

View File

@@ -6,6 +6,7 @@ import { ComfyApp, app } from './app'
import { TaskItem } from '@/types/apiTypes'
import { showSettingsDialog } from '@/services/dialogService'
import { useSettingStore } from '@/stores/settingStore'
import { useCommandStore } from '@/stores/commandStore'
export const ComfyDialog = _ComfyDialog
@@ -585,30 +586,7 @@ export class ComfyUI {
id: 'comfy-save-button',
textContent: 'Save',
onclick: () => {
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', {

View File

@@ -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<string | null> {
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<void> {
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)
}

View File

@@ -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<string | null> {
const dialogStore = useDialogStore()
return new Promise((resolve) => {
dialogStore.showDialog({
title,
component: PromptDialogContent,
props: {
message,
defaultValue,
onConfirm: (value: string) => {
resolve(value)
}
},
dialogComponentProps: {
onClose: () => {
resolve(null)
}
}
})
})
}

View File

@@ -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,

View File

@@ -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<string, any>
// 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<string, any>
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
}
}

View File

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