mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-12 16:40:05 +00:00
Confirm delete workflow (#1772)
* Add confirm delete workflow prompt * Add confirm delete workflow setting * Add delete workflow tests * Change dialog to modal, set default cancel * Fix setting version key * Rename for clarity * Fix tests - Move into correct section - Add confirm control * Export type: ShowDialogOptions * Replace workflow overwrite with new dialog * Add delete workflow confirmation dialog * Update i18n * Add item list support to confirmation dialog * Prevent multiple dialogs for same action * Add confirm close file when dirty * Add i18n for overwrite dialog * Fix regression: confirm dialog setting ignored * Fix delete last workflow does not open replacement * Update tests
This commit is contained in:
@@ -75,6 +75,28 @@ type FolderStructure = {
|
||||
[key: string]: FolderStructure | string
|
||||
}
|
||||
|
||||
type KeysOfType<T, Match> = {
|
||||
[K in keyof T]: T[K] extends Match ? K : never
|
||||
}[keyof T]
|
||||
|
||||
class ConfirmDialog {
|
||||
public readonly delete: Locator
|
||||
public readonly overwrite: Locator
|
||||
public readonly reject: Locator
|
||||
|
||||
constructor(public readonly page: Page) {
|
||||
this.delete = page.locator('button.p-button[aria-label="Delete"]')
|
||||
this.overwrite = page.locator('button.p-button[aria-label="Overwrite"]')
|
||||
this.reject = page.locator('button.p-button[aria-label="Cancel"]')
|
||||
}
|
||||
|
||||
async click(locator: KeysOfType<ConfirmDialog, Locator>) {
|
||||
const loc = this[locator]
|
||||
await expect(loc).toBeVisible()
|
||||
await loc.click()
|
||||
}
|
||||
}
|
||||
|
||||
export class ComfyPage {
|
||||
public readonly url: string
|
||||
// All canvas position operations are based on default view of canvas.
|
||||
@@ -94,6 +116,7 @@ export class ComfyPage {
|
||||
public readonly actionbar: ComfyActionbar
|
||||
public readonly templates: ComfyTemplates
|
||||
public readonly settingDialog: SettingDialog
|
||||
public readonly confirmDialog: ConfirmDialog
|
||||
|
||||
/** Worker index to test user ID */
|
||||
public readonly userIds: string[] = []
|
||||
@@ -118,6 +141,7 @@ export class ComfyPage {
|
||||
this.actionbar = new ComfyActionbar(page)
|
||||
this.templates = new ComfyTemplates(page)
|
||||
this.settingDialog = new SettingDialog(page)
|
||||
this.confirmDialog = new ConfirmDialog(page)
|
||||
}
|
||||
|
||||
convertLeafToContent(structure: FolderStructure): FolderStructure {
|
||||
@@ -740,14 +764,14 @@ export class ComfyPage {
|
||||
)
|
||||
}
|
||||
|
||||
async confirmDialog(prompt: string, text: string = 'Yes') {
|
||||
async clickDialogButton(prompt: string, buttonText: string = 'Yes') {
|
||||
const modal = this.page.locator(
|
||||
`.comfy-modal-content:has-text("${prompt}")`
|
||||
)
|
||||
await expect(modal).toBeVisible()
|
||||
await modal
|
||||
.locator('.comfyui-button', {
|
||||
hasText: text
|
||||
hasText: buttonText
|
||||
})
|
||||
.click()
|
||||
await expect(modal).toBeHidden()
|
||||
|
||||
@@ -442,7 +442,7 @@ test.describe('Menu', () => {
|
||||
).toEqual(['workflow5.json'])
|
||||
|
||||
await comfyPage.menu.topbar.saveWorkflowAs('workflow5.json')
|
||||
await comfyPage.confirmDialog('Overwrite existing file?', 'Yes')
|
||||
await comfyPage.confirmDialog.click('overwrite')
|
||||
expect(
|
||||
await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()
|
||||
).toEqual(['workflow5.json'])
|
||||
@@ -477,7 +477,7 @@ test.describe('Menu', () => {
|
||||
)
|
||||
|
||||
await topbar.saveWorkflowAs('workflow1.json')
|
||||
await comfyPage.confirmDialog('Overwrite existing file?', 'Yes')
|
||||
await comfyPage.confirmDialog.click('overwrite')
|
||||
// The old workflow1.json should be deleted and the new one should be saved.
|
||||
expect(
|
||||
await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()
|
||||
@@ -519,6 +519,43 @@ test.describe('Menu', () => {
|
||||
await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()
|
||||
).toEqual(['*Unsaved Workflow.json'])
|
||||
})
|
||||
|
||||
test('Can delete workflows (confirm disabled)', async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.Workflow.ConfirmDelete', false)
|
||||
|
||||
const { topbar, workflowsTab } = comfyPage.menu
|
||||
|
||||
const filename = 'workflow18.json'
|
||||
await topbar.saveWorkflow(filename)
|
||||
expect(await workflowsTab.getOpenedWorkflowNames()).toEqual([filename])
|
||||
|
||||
await workflowsTab.getOpenedItem(filename).click({ button: 'right' })
|
||||
await comfyPage.nextFrame()
|
||||
await comfyPage.clickContextMenuItem('Delete')
|
||||
|
||||
await expect(workflowsTab.getOpenedItem(filename)).not.toBeVisible()
|
||||
expect(await workflowsTab.getOpenedWorkflowNames()).toEqual([
|
||||
'*Unsaved Workflow.json'
|
||||
])
|
||||
})
|
||||
|
||||
test('Can delete workflows', async ({ comfyPage }) => {
|
||||
const { topbar, workflowsTab } = comfyPage.menu
|
||||
|
||||
const filename = 'workflow18.json'
|
||||
await topbar.saveWorkflow(filename)
|
||||
expect(await workflowsTab.getOpenedWorkflowNames()).toEqual([filename])
|
||||
|
||||
await workflowsTab.getOpenedItem(filename).click({ button: 'right' })
|
||||
await comfyPage.clickContextMenuItem('Delete')
|
||||
|
||||
await comfyPage.confirmDialog.click('delete')
|
||||
|
||||
await expect(workflowsTab.getOpenedItem(filename)).not.toBeVisible()
|
||||
expect(await workflowsTab.getOpenedWorkflowNames()).toEqual([
|
||||
'*Unsaved Workflow.json'
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Workflows topbar tabs', () => {
|
||||
|
||||
64
src/components/dialog/content/ConfirmationDialogContent.vue
Normal file
64
src/components/dialog/content/ConfirmationDialogContent.vue
Normal file
@@ -0,0 +1,64 @@
|
||||
<template>
|
||||
<section class="prompt-dialog-content flex flex-col gap-6 m-2 mt-4">
|
||||
<span>{{ message }}</span>
|
||||
<ul v-if="itemList?.length" class="pl-4 m-0 flex flex-col gap-2">
|
||||
<li v-for="item of itemList" :key="item">{{ item }}</li>
|
||||
</ul>
|
||||
<div class="flex gap-4 justify-end">
|
||||
<Button
|
||||
:label="$t('cancel')"
|
||||
icon="pi pi-undo"
|
||||
severity="secondary"
|
||||
@click="onCancel"
|
||||
autofocus
|
||||
/>
|
||||
<Button
|
||||
v-if="type === 'delete'"
|
||||
:label="$t('delete')"
|
||||
severity="danger"
|
||||
@click="onConfirm"
|
||||
icon="pi pi-trash"
|
||||
/>
|
||||
<Button
|
||||
v-else-if="type === 'overwrite'"
|
||||
:label="$t('overwrite')"
|
||||
severity="warn"
|
||||
@click="onConfirm"
|
||||
icon="pi pi-save"
|
||||
/>
|
||||
<template v-else>
|
||||
<Button
|
||||
:label="$t('no')"
|
||||
severity="secondary"
|
||||
@click="onDeny"
|
||||
icon="pi pi-times"
|
||||
/>
|
||||
<Button :label="$t('save')" @click="onConfirm" icon="pi pi-save" />
|
||||
</template>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
|
||||
const props = defineProps<{
|
||||
message: string
|
||||
type: 'overwrite' | 'delete' | 'dirtyClose'
|
||||
onConfirm: (value?: boolean) => void
|
||||
itemList?: string[]
|
||||
}>()
|
||||
|
||||
const onCancel = () => useDialogStore().closeDialog()
|
||||
|
||||
const onDeny = () => {
|
||||
props.onConfirm(false)
|
||||
useDialogStore().closeDialog()
|
||||
}
|
||||
|
||||
const onConfirm = () => {
|
||||
props.onConfirm(true)
|
||||
useDialogStore().closeDialog()
|
||||
}
|
||||
</script>
|
||||
@@ -123,6 +123,7 @@
|
||||
</div>
|
||||
</template>
|
||||
</SidebarTabTemplate>
|
||||
<ConfirmDialog />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -150,6 +151,9 @@ import { workflowService } from '@/services/workflowService'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||
import { appendJsonExt } from '@/utils/formatUtil'
|
||||
import { buildTree, sortedTree } from '@/utils/treeUtil'
|
||||
import { useConfirm } from 'primevue/useconfirm'
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
import ConfirmDialog from 'primevue/confirmdialog'
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
const workflowTabsPosition = computed(() =>
|
||||
@@ -218,6 +222,9 @@ const openWorkflowsTree = computed(() =>
|
||||
buildTree(workflowStore.openWorkflows, (workflow) => [workflow.key])
|
||||
)
|
||||
|
||||
const confirm = useConfirm()
|
||||
const toast = useToast()
|
||||
|
||||
const renderTreeNode = (
|
||||
node: TreeNode,
|
||||
type: WorkflowTreeType
|
||||
@@ -236,6 +243,7 @@ const renderTreeNode = (
|
||||
toggleNodeOnEvent(e, node)
|
||||
}
|
||||
}
|
||||
|
||||
const actions = node.leaf
|
||||
? {
|
||||
handleClick,
|
||||
@@ -252,8 +260,8 @@ const renderTreeNode = (
|
||||
},
|
||||
handleDelete: workflow.isTemporary
|
||||
? undefined
|
||||
: () => {
|
||||
workflowService.deleteWorkflow(workflow)
|
||||
: async () => {
|
||||
await workflowService.deleteWorkflow(workflow)
|
||||
},
|
||||
contextMenuItems: (node: TreeExplorerNode<ComfyWorkflow>) => {
|
||||
return [
|
||||
|
||||
@@ -658,5 +658,12 @@ export const CORE_SETTINGS: SettingParams[] = [
|
||||
type: 'boolean',
|
||||
defaultValue: true,
|
||||
versionModified: '1.5.4'
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Workflow.ConfirmDelete',
|
||||
name: 'Show confirmation when deleting workflows',
|
||||
type: 'boolean',
|
||||
defaultValue: true,
|
||||
versionAdded: '1.5.6'
|
||||
}
|
||||
]
|
||||
|
||||
@@ -16,3 +16,6 @@ export const i18n = createI18n({
|
||||
ja
|
||||
}
|
||||
})
|
||||
|
||||
/** Convenience shorthand: i18n.global should be . */
|
||||
export const { t } = i18n.global
|
||||
|
||||
@@ -128,6 +128,10 @@
|
||||
"reconnected": "Reconnected",
|
||||
"delete": "Delete",
|
||||
"rename": "Rename",
|
||||
"save": "Save",
|
||||
"no": "No",
|
||||
"cancel": "Cancel",
|
||||
"overwrite": "Overwrite",
|
||||
"customize": "Customize",
|
||||
"experimental": "BETA",
|
||||
"deprecated": "DEPR",
|
||||
@@ -454,6 +458,17 @@
|
||||
"hideCached": "Hide Cached",
|
||||
"hideCanceled": "Hide Canceled"
|
||||
}
|
||||
},
|
||||
"workflowTab": {
|
||||
"confirmDeleteTitle": "Delete workflow?",
|
||||
"confirmDelete": "Are you sure you want to delete this workflow?",
|
||||
"deleted": "Workflow deleted",
|
||||
"deleteFailedTitle": "Delete failed",
|
||||
"deleteFailed": "Attempt to delete the workflow failed.",
|
||||
"dirtyCloseTitle": "Save Changes?",
|
||||
"dirtyClose": "The files below have been changed. Would you like to save them before closing?",
|
||||
"confirmOverwriteTitle": "Overwrite existing file?",
|
||||
"confirmOverwrite": "The file below already exists. Would you like to overwrite it?"
|
||||
}
|
||||
},
|
||||
"menu": {
|
||||
@@ -573,5 +588,9 @@
|
||||
"Feedback": "Feedback",
|
||||
"Reinstall": "Reinstall",
|
||||
"Restart": "Restart"
|
||||
},
|
||||
"desktopMenu": {
|
||||
"reinstall": "Reinstall",
|
||||
"confirmReinstall": "This will clear your extra_models_config.yaml file, and begin installation again. Are you sure?"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"bookmark": "ブックマーク",
|
||||
"box": "ボックス",
|
||||
"briefcase": "ブリーフケース",
|
||||
"cancel": "キャンセル",
|
||||
"color": "色",
|
||||
"comingSoon": "近日公開",
|
||||
"confirm": "確認",
|
||||
@@ -13,6 +14,10 @@
|
||||
"customizeFolder": "フォルダーをカスタマイズ",
|
||||
"delete": "削除",
|
||||
"deprecated": "非推奨",
|
||||
"desktopMenu": {
|
||||
"confirmReinstall": "これにより、extra_models_config.yamlファイルがクリアされ、再インストールが開始されます。本当によろしいですか?",
|
||||
"reinstall": "再インストール"
|
||||
},
|
||||
"devices": "デバイス",
|
||||
"download": "ダウンロード",
|
||||
"downloadGit": {
|
||||
@@ -184,6 +189,7 @@
|
||||
"Zoom Out": "ズームアウト"
|
||||
},
|
||||
"newFolder": "新しいフォルダー",
|
||||
"no": "いいえ",
|
||||
"noResultsFound": "結果が見つかりませんでした",
|
||||
"noTasksFound": "タスクが見つかりませんでした",
|
||||
"noTasksFoundMessage": "キューにタスクがありません。",
|
||||
@@ -199,6 +205,7 @@
|
||||
"title": "お使いのデバイスはサポートされていません"
|
||||
},
|
||||
"openNewIssue": "新しいIssueを開く",
|
||||
"overwrite": "上書き",
|
||||
"reconnected": "再接続しました",
|
||||
"reconnecting": "再接続中",
|
||||
"refresh": "更新",
|
||||
@@ -209,6 +216,7 @@
|
||||
"reportSent": "レポートを送信しました",
|
||||
"reset": "リセット",
|
||||
"resetKeybindingsTooltip": "キーバインドをデフォルトに戻す",
|
||||
"save": "保存",
|
||||
"searchExtensions": "拡張機能を検索",
|
||||
"searchFailedMessage": "検索条件に一致する設定が見つかりませんでした。条件を変更して再試行してください。",
|
||||
"searchKeybindings": "キーバインドを検索",
|
||||
@@ -541,6 +549,17 @@
|
||||
"showFlatList": "フラットリストを表示"
|
||||
},
|
||||
"themeToggle": "テーマの切り替え",
|
||||
"workflowTab": {
|
||||
"confirmDelete": "このワークフローを削除してもよろしいですか?",
|
||||
"confirmDeleteTitle": "ワークフローを削除しますか?",
|
||||
"confirmOverwrite": "以下のファイルはすでに存在します。上書きしますか?",
|
||||
"confirmOverwriteTitle": "既存のファイルを上書きしますか?",
|
||||
"deleteFailed": "ワークフローの削除を試みましたが、失敗しました。",
|
||||
"deleteFailedTitle": "削除に失敗しました",
|
||||
"deleted": "ワークフローが削除されました",
|
||||
"dirtyClose": "以下のファイルが変更されました。閉じる前に保存しますか?",
|
||||
"dirtyCloseTitle": "変更を保存しますか?"
|
||||
},
|
||||
"workflows": "ワークフロー"
|
||||
},
|
||||
"star": "スター",
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"bookmark": "Закладка",
|
||||
"box": "Ящик",
|
||||
"briefcase": "Чемодан",
|
||||
"cancel": "Отмена",
|
||||
"color": "Цвет",
|
||||
"comingSoon": "Скоро",
|
||||
"confirm": "Подтвердить",
|
||||
@@ -13,6 +14,10 @@
|
||||
"customizeFolder": "Настроить папку",
|
||||
"delete": "Удалить",
|
||||
"deprecated": "УСТАР",
|
||||
"desktopMenu": {
|
||||
"confirmReinstall": "Это очистит ваш файл extra_models_config.yaml и начнет установку заново. Вы уверены?",
|
||||
"reinstall": "Переустановить"
|
||||
},
|
||||
"devices": "Устройства",
|
||||
"download": "Скачать",
|
||||
"downloadGit": {
|
||||
@@ -184,6 +189,7 @@
|
||||
"Zoom Out": "Уменьшить"
|
||||
},
|
||||
"newFolder": "Новая папка",
|
||||
"no": "Нет",
|
||||
"noResultsFound": "Ничего не найдено",
|
||||
"noTasksFound": "Задачи не найдены",
|
||||
"noTasksFoundMessage": "В очереди нет задач.",
|
||||
@@ -199,6 +205,7 @@
|
||||
"title": "Ваше устройство не поддерживается"
|
||||
},
|
||||
"openNewIssue": "Открыть новый Issue",
|
||||
"overwrite": "Перезаписать",
|
||||
"reconnected": "Переподключено",
|
||||
"reconnecting": "Переподключение",
|
||||
"refresh": "Обновить",
|
||||
@@ -209,6 +216,7 @@
|
||||
"reportSent": "Отчет отправлен",
|
||||
"reset": "Сбросить",
|
||||
"resetKeybindingsTooltip": "Сбросить сочетания клавиш по умолчанию",
|
||||
"save": "Сохранить",
|
||||
"searchExtensions": "Поиск расширений",
|
||||
"searchFailedMessage": "Не удалось найти ни одной настройки, соответствующей вашему запросу. Попробуйте скорректировать поисковый запрос.",
|
||||
"searchKeybindings": "Поиск сочетаний клавиш",
|
||||
@@ -541,6 +549,17 @@
|
||||
"showFlatList": "Показать плоский список"
|
||||
},
|
||||
"themeToggle": "Переключить тему",
|
||||
"workflowTab": {
|
||||
"confirmDelete": "Вы уверены, что хотите удалить этот рабочий процесс?",
|
||||
"confirmDeleteTitle": "Удалить рабочий процесс?",
|
||||
"confirmOverwrite": "Файл ниже уже существует. Вы хотите его перезаписать?",
|
||||
"confirmOverwriteTitle": "Перезаписать существующий файл?",
|
||||
"deleteFailed": "Попытка удалить рабочий процесс не удалась.",
|
||||
"deleteFailedTitle": "Не удалось удалить",
|
||||
"deleted": "Рабочий процесс удален",
|
||||
"dirtyClose": "Файлы ниже были изменены. Вы хотите сохранить их перед закрытием?",
|
||||
"dirtyCloseTitle": "Сохранить изменения?"
|
||||
},
|
||||
"workflows": "Рабочие процессы"
|
||||
},
|
||||
"star": "Звёздочка",
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"bookmark": "书签",
|
||||
"box": "盒子",
|
||||
"briefcase": "公文包",
|
||||
"cancel": "取消",
|
||||
"color": "颜色",
|
||||
"comingSoon": "敬请期待",
|
||||
"confirm": "确认",
|
||||
@@ -13,6 +14,10 @@
|
||||
"customizeFolder": "定制文件夹",
|
||||
"delete": "删除",
|
||||
"deprecated": "弃用",
|
||||
"desktopMenu": {
|
||||
"confirmReinstall": "这将清除您的 extra_models_config.yaml 文件,并重新开始安装。您确定吗?",
|
||||
"reinstall": "重新安装"
|
||||
},
|
||||
"devices": "设备",
|
||||
"download": "下载",
|
||||
"downloadGit": {
|
||||
@@ -184,6 +189,7 @@
|
||||
"Zoom Out": "缩小"
|
||||
},
|
||||
"newFolder": "新建文件夹",
|
||||
"no": "不",
|
||||
"noResultsFound": "未找到结果",
|
||||
"noTasksFound": "未找到任务",
|
||||
"noTasksFoundMessage": "队列中没有任务。",
|
||||
@@ -199,6 +205,7 @@
|
||||
"title": "您的设备不受支持"
|
||||
},
|
||||
"openNewIssue": "开启新 Issue",
|
||||
"overwrite": "覆盖",
|
||||
"reconnected": "已重新连接",
|
||||
"reconnecting": "重新连接中",
|
||||
"refresh": "刷新",
|
||||
@@ -209,6 +216,7 @@
|
||||
"reportSent": "报告已提交",
|
||||
"reset": "重置",
|
||||
"resetKeybindingsTooltip": "重置键位",
|
||||
"save": "保存",
|
||||
"searchExtensions": "搜索插件",
|
||||
"searchFailedMessage": "我们找不到与您的搜索匹配的任何设置。请尝试调整搜索条件。",
|
||||
"searchKeybindings": "搜索键位",
|
||||
@@ -541,6 +549,17 @@
|
||||
"showFlatList": "平铺结果"
|
||||
},
|
||||
"themeToggle": "主题切换",
|
||||
"workflowTab": {
|
||||
"confirmDelete": "您确定要删除此工作流吗?",
|
||||
"confirmDeleteTitle": "删除工作流?",
|
||||
"confirmOverwrite": "下面的文件已经存在。您想要覆盖它吗?",
|
||||
"confirmOverwriteTitle": "覆盖现有文件?",
|
||||
"deleteFailed": "尝试删除工作流失败。",
|
||||
"deleteFailedTitle": "删除失败",
|
||||
"deleted": "工作流已删除",
|
||||
"dirtyClose": "以下文件已被更改。您想在关闭之前保存它们吗?",
|
||||
"dirtyCloseTitle": "保存更改?"
|
||||
},
|
||||
"workflows": "工作流"
|
||||
},
|
||||
"star": "星星",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// This module is mocked in tests-ui/
|
||||
// Import vue components here to avoid tests-ui/ reporting errors
|
||||
// about importing primevue components.
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
import { useDialogStore, type ShowDialogOptions } from '@/stores/dialogStore'
|
||||
import LoadWorkflowWarning from '@/components/dialog/content/LoadWorkflowWarning.vue'
|
||||
import MissingModelsWarning from '@/components/dialog/content/MissingModelsWarning.vue'
|
||||
import SettingDialogContent from '@/components/dialog/content/SettingDialogContent.vue'
|
||||
@@ -10,6 +10,7 @@ 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 ConfirmationDialogContent from '@/components/dialog/content/ConfirmationDialogContent.vue'
|
||||
import { i18n } from '@/i18n'
|
||||
import type { MissingNodeType } from '@/types/comfy'
|
||||
|
||||
@@ -95,3 +96,44 @@ export async function showPromptDialog({
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @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
|
||||
*/
|
||||
export async function showConfirmationDialog({
|
||||
title,
|
||||
type,
|
||||
message,
|
||||
itemList = []
|
||||
}: {
|
||||
/** Dialog heading */
|
||||
title: string
|
||||
/** Pre-configured dialog type */
|
||||
type: 'overwrite' | 'delete' | 'dirtyClose'
|
||||
/** The main message body */
|
||||
message: string
|
||||
/** Displayed as an unorderd list immediately below the message body */
|
||||
itemList?: string[]
|
||||
}): Promise<boolean | null> {
|
||||
return new Promise((resolve) => {
|
||||
const options: ShowDialogOptions = {
|
||||
key: 'global-prompt',
|
||||
title,
|
||||
component: ConfirmationDialogContent,
|
||||
props: {
|
||||
message,
|
||||
type,
|
||||
itemList,
|
||||
onConfirm: resolve
|
||||
},
|
||||
dialogComponentProps: {
|
||||
onClose: () => resolve(null)
|
||||
}
|
||||
}
|
||||
|
||||
useDialogStore().showDialog(options)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { downloadBlob } from '@/scripts/utils'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { ComfyAsyncDialog } from '@/scripts/ui/components/asyncDialog'
|
||||
import { useWorkflowStore, ComfyWorkflow } from '@/stores/workflowStore'
|
||||
import { showPromptDialog } from './dialogService'
|
||||
import { showConfirmationDialog, showPromptDialog } from './dialogService'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||
import { LGraphCanvas } from '@comfyorg/litegraph'
|
||||
@@ -10,6 +9,8 @@ import { toRaw } from 'vue'
|
||||
import { ComfyWorkflowJSON } from '@/types/comfyWorkflow'
|
||||
import { blankGraph, defaultGraph } from '@/scripts/defaultGraph'
|
||||
import { appendJsonExt } from '@/utils/formatUtil'
|
||||
import { t } from '@/i18n'
|
||||
import { useToastStore } from '@/stores/toastStore'
|
||||
|
||||
async function getFilename(defaultName: string): Promise<string | null> {
|
||||
if (useSettingStore().get('Comfy.PromptFilename')) {
|
||||
@@ -67,19 +68,20 @@ export const workflowService = {
|
||||
const existingWorkflow = workflowStore.getWorkflowByPath(newPath)
|
||||
|
||||
if (existingWorkflow && !existingWorkflow.isTemporary) {
|
||||
const res = (await ComfyAsyncDialog.prompt({
|
||||
title: 'Overwrite existing file?',
|
||||
message: `"${newPath}" already exists. Do you want to overwrite it?`,
|
||||
actions: ['Yes', 'No']
|
||||
})) as 'Yes' | 'No'
|
||||
const res = await showConfirmationDialog({
|
||||
title: t('sideToolbar.workflowTab.confirmOverwriteTitle'),
|
||||
type: 'overwrite',
|
||||
message: t('sideToolbar.workflowTab.confirmOverwrite'),
|
||||
itemList: [newPath]
|
||||
})
|
||||
|
||||
if (res === 'No') return
|
||||
if (res !== true) return
|
||||
|
||||
if (existingWorkflow.path === workflow.path) {
|
||||
await this.saveWorkflow(workflow)
|
||||
return
|
||||
}
|
||||
const deleted = await this.deleteWorkflow(existingWorkflow)
|
||||
const deleted = await this.deleteWorkflow(existingWorkflow, true)
|
||||
if (!deleted) return
|
||||
}
|
||||
|
||||
@@ -156,16 +158,17 @@ export const workflowService = {
|
||||
}
|
||||
|
||||
if (workflow.isModified && options.warnIfUnsaved) {
|
||||
const res = (await ComfyAsyncDialog.prompt({
|
||||
title: 'Save Changes?',
|
||||
message: `Do you want to save changes to "${workflow.path}" before closing?`,
|
||||
actions: ['Yes', 'No', 'Cancel']
|
||||
})) as 'Yes' | 'No' | 'Cancel'
|
||||
const confirmed = await showConfirmationDialog({
|
||||
title: t('sideToolbar.workflowTab.dirtyCloseTitle'),
|
||||
type: 'dirtyClose',
|
||||
message: t('sideToolbar.workflowTab.dirtyClose'),
|
||||
itemList: [workflow.path]
|
||||
})
|
||||
// Cancel
|
||||
if (confirmed === null) return false
|
||||
|
||||
if (res === 'Yes') {
|
||||
if (confirmed === true) {
|
||||
await this.saveWorkflow(workflow)
|
||||
} else if (res === 'Cancel') {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,15 +193,40 @@ export const workflowService = {
|
||||
/**
|
||||
* Delete a workflow
|
||||
* @param workflow The workflow to delete
|
||||
* @returns true if the workflow was deleted, false if the user cancelled
|
||||
* @returns `true` if the workflow was deleted, `false` if the user cancelled
|
||||
*/
|
||||
async deleteWorkflow(workflow: ComfyWorkflow): Promise<boolean> {
|
||||
async deleteWorkflow(
|
||||
workflow: ComfyWorkflow,
|
||||
silent = false
|
||||
): Promise<boolean> {
|
||||
const bypassConfirm = !useSettingStore().get('Comfy.Workflow.ConfirmDelete')
|
||||
let confirmed: boolean | null = bypassConfirm || silent
|
||||
|
||||
if (!confirmed) {
|
||||
confirmed = await showConfirmationDialog({
|
||||
title: t('sideToolbar.workflowTab.confirmDeleteTitle'),
|
||||
type: 'delete',
|
||||
message: t('sideToolbar.workflowTab.confirmDelete'),
|
||||
itemList: [workflow.path]
|
||||
})
|
||||
if (!confirmed) return false
|
||||
}
|
||||
|
||||
const workflowStore = useWorkflowStore()
|
||||
if (workflowStore.isOpen(workflow)) {
|
||||
const closed = await this.closeWorkflow(workflow)
|
||||
const closed = await this.closeWorkflow(workflow, {
|
||||
warnIfUnsaved: !confirmed
|
||||
})
|
||||
if (!closed) return false
|
||||
}
|
||||
await workflowStore.deleteWorkflow(workflow)
|
||||
if (!silent) {
|
||||
useToastStore().add({
|
||||
severity: 'info',
|
||||
summary: t('sideToolbar.workflowTab.deleted'),
|
||||
life: 1000
|
||||
})
|
||||
}
|
||||
return true
|
||||
},
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@ export interface ComfyCommand {
|
||||
/** Menubar item label, if different from command label */
|
||||
menubarLabel?: string | (() => string)
|
||||
versionAdded?: string
|
||||
/** If non-nullish, this command will prompt for confirmation. */
|
||||
confirmation?: string
|
||||
}
|
||||
|
||||
export class ComfyCommandImpl implements ComfyCommand {
|
||||
@@ -24,6 +26,7 @@ export class ComfyCommandImpl implements ComfyCommand {
|
||||
_tooltip?: string | (() => string)
|
||||
_menubarLabel?: string | (() => string)
|
||||
versionAdded?: string
|
||||
confirmation?: string
|
||||
|
||||
constructor(command: ComfyCommand) {
|
||||
this.id = command.id
|
||||
@@ -33,6 +36,7 @@ export class ComfyCommandImpl implements ComfyCommand {
|
||||
this._tooltip = command.tooltip
|
||||
this._menubarLabel = command.menubarLabel ?? command.label
|
||||
this.versionAdded = command.versionAdded
|
||||
this.confirmation = command.confirmation
|
||||
}
|
||||
|
||||
get label() {
|
||||
|
||||
@@ -19,6 +19,15 @@ interface DialogInstance {
|
||||
dialogComponentProps: Record<string, any>
|
||||
}
|
||||
|
||||
export interface ShowDialogOptions {
|
||||
key?: string
|
||||
title?: string
|
||||
headerComponent?: Component
|
||||
component: Component
|
||||
props?: Record<string, any>
|
||||
dialogComponentProps?: DialogComponentProps
|
||||
}
|
||||
|
||||
export const useDialogStore = defineStore('dialog', () => {
|
||||
const dialogStack = ref<DialogInstance[]>([])
|
||||
|
||||
@@ -102,14 +111,7 @@ export const useDialogStore = defineStore('dialog', () => {
|
||||
return dialog
|
||||
}
|
||||
|
||||
function showDialog(options: {
|
||||
key?: string
|
||||
title?: string
|
||||
headerComponent?: Component
|
||||
component: Component
|
||||
props?: Record<string, any>
|
||||
dialogComponentProps?: DialogComponentProps
|
||||
}) {
|
||||
function showDialog(options: ShowDialogOptions) {
|
||||
const dialogKey = options.key || genDialogKey()
|
||||
|
||||
let dialog = dialogStack.value.find((d) => d.key === dialogKey)
|
||||
|
||||
@@ -525,7 +525,8 @@ const zSettings = z.record(z.any()).and(
|
||||
'Comfy.Node.SnapHighlightsNode': z.boolean(),
|
||||
'Comfy.Server.ServerConfigValues': z.record(z.string(), z.any()),
|
||||
'Comfy.Server.LaunchArgs': z.record(z.string(), z.string()),
|
||||
'LiteGraph.Canvas.MaximumFps': z.number()
|
||||
'LiteGraph.Canvas.MaximumFps': z.number(),
|
||||
'Comfy.Workflow.ConfirmDelete': z.boolean()
|
||||
})
|
||||
.optional()
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user