Files
ComfyUI_frontend/src/components/sidebar/tabs/WorkflowsSidebarTab.vue
filtered 735153886f 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
2024-12-04 14:11:49 -05:00

300 lines
9.3 KiB
Vue

<template>
<SidebarTabTemplate
:title="$t('sideToolbar.workflows')"
class="bg-[var(--p-tree-background)]"
>
<template #tool-buttons>
<Button
class="browse-templates-button"
icon="pi pi-th-large"
v-tooltip.bottom="$t('sideToolbar.browseTemplates')"
text
@click="() => commandStore.execute('Comfy.BrowseTemplates')"
/>
<Button
class="open-workflow-button"
icon="pi pi-folder-open"
v-tooltip.bottom="$t('sideToolbar.openWorkflow')"
text
@click="() => commandStore.execute('Comfy.OpenWorkflow')"
/>
<Button
class="new-blank-workflow-button"
icon="pi pi-plus"
v-tooltip.bottom="$t('sideToolbar.newBlankWorkflow')"
@click="() => commandStore.execute('Comfy.NewBlankWorkflow')"
text
/>
</template>
<template #header>
<SearchBox
class="workflows-search-box p-2 2xl:p-4"
v-model:modelValue="searchQuery"
@search="handleSearch"
:placeholder="$t('searchWorkflows') + '...'"
/>
</template>
<template #body>
<div class="comfyui-workflows-panel" v-if="!isSearching">
<div
class="comfyui-workflows-open"
v-if="workflowTabsPosition === 'Sidebar'"
>
<TextDivider text="Open" type="dashed" class="ml-2" />
<TreeExplorer
:roots="
renderTreeNode(openWorkflowsTree, WorkflowTreeType.Open).children
"
:selectionKeys="selectionKeys"
>
<template #node="{ node }">
<TreeExplorerTreeNode :node="node">
<template #before-label="{ node }">
<span v-if="node.data.isModified || !node.data.isPersisted"
>*</span
>
</template>
<template #actions="{ node }">
<Button
class="close-workflow-button"
icon="pi pi-times"
text
:severity="
workspaceStore.shiftDown ? 'danger' : 'secondary'
"
size="small"
@click.stop="handleCloseWorkflow(node.data)"
/>
</template>
</TreeExplorerTreeNode>
</template>
</TreeExplorer>
</div>
<div
class="comfyui-workflows-bookmarks"
v-show="workflowStore.bookmarkedWorkflows.length > 0"
>
<TextDivider text="Bookmarks" type="dashed" class="ml-2" />
<TreeExplorer
:roots="
renderTreeNode(
bookmarkedWorkflowsTree,
WorkflowTreeType.Bookmarks
).children
"
>
<template #node="{ node }">
<WorkflowTreeLeaf :node="node" />
</template>
</TreeExplorer>
</div>
<div class="comfyui-workflows-browse">
<TextDivider text="Browse" type="dashed" class="ml-2" />
<TreeExplorer
:roots="
renderTreeNode(workflowsTree, WorkflowTreeType.Browse).children
"
v-model:expandedKeys="expandedKeys"
v-if="workflowStore.persistedWorkflows.length > 0"
>
<template #node="{ node }">
<WorkflowTreeLeaf :node="node" />
</template>
</TreeExplorer>
<NoResultsPlaceholder
v-else
icon="pi pi-folder"
:title="$t('empty')"
:message="$t('noWorkflowsFound')"
/>
</div>
</div>
<div class="comfyui-workflows-search-panel" v-else>
<TreeExplorer
:roots="
renderTreeNode(filteredRoot, WorkflowTreeType.Browse).children
"
v-model:expandedKeys="expandedKeys"
>
<template #node="{ node }">
<WorkflowTreeLeaf :node="node" />
</template>
</TreeExplorer>
</div>
</template>
</SidebarTabTemplate>
<ConfirmDialog />
</template>
<script setup lang="ts">
import SearchBox from '@/components/common/SearchBox.vue'
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
import WorkflowTreeLeaf from '@/components/sidebar/tabs/workflows/WorkflowTreeLeaf.vue'
import SidebarTabTemplate from '@/components/sidebar/tabs/SidebarTabTemplate.vue'
import TreeExplorer from '@/components/common/TreeExplorer.vue'
import TreeExplorerTreeNode from '@/components/common/TreeExplorerTreeNode.vue'
import Button from 'primevue/button'
import TextDivider from '@/components/common/TextDivider.vue'
import { computed, nextTick, onMounted, ref } from 'vue'
import {
useWorkflowBookmarkStore,
useWorkflowStore
} from '@/stores/workflowStore'
import { useCommandStore } from '@/stores/commandStore'
import type { TreeNode } from 'primevue/treenode'
import { TreeExplorerNode } from '@/types/treeExplorerTypes'
import { ComfyWorkflow } from '@/stores/workflowStore'
import { useI18n } from 'vue-i18n'
import { useTreeExpansion } from '@/hooks/treeHooks'
import { useSettingStore } from '@/stores/settingStore'
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(() =>
settingStore.get('Comfy.Workflow.WorkflowTabsPosition')
)
const searchQuery = ref('')
const isSearching = computed(() => searchQuery.value.length > 0)
const filteredWorkflows = ref<ComfyWorkflow[]>([])
const filteredRoot = computed<TreeNode>(() => {
return buildWorkflowTree(filteredWorkflows.value as ComfyWorkflow[])
})
const handleSearch = (query: string) => {
if (query.length === 0) {
filteredWorkflows.value = []
expandedKeys.value = {}
return
}
const lowerQuery = query.toLocaleLowerCase()
filteredWorkflows.value = workflowStore.workflows.filter((workflow) => {
return workflow.path.toLocaleLowerCase().includes(lowerQuery)
})
nextTick(() => {
expandNode(filteredRoot.value)
})
}
const commandStore = useCommandStore()
const workflowStore = useWorkflowStore()
const workspaceStore = useWorkspaceStore()
const { t } = useI18n()
const expandedKeys = ref<Record<string, boolean>>({})
const { expandNode, toggleNodeOnEvent } = useTreeExpansion(expandedKeys)
const handleCloseWorkflow = (workflow?: ComfyWorkflow) => {
if (workflow) {
workflowService.closeWorkflow(workflow, {
warnIfUnsaved: !workspaceStore.shiftDown
})
}
}
enum WorkflowTreeType {
Open = 'Open',
Bookmarks = 'Bookmarks',
Browse = 'Browse'
}
const buildWorkflowTree = (workflows: ComfyWorkflow[]) => {
return buildTree(workflows, (workflow: ComfyWorkflow) =>
workflow.key.split('/')
)
}
const workflowsTree = computed(() =>
sortedTree(buildWorkflowTree(workflowStore.persistedWorkflows), {
groupLeaf: true
})
)
// Bookmarked workflows tree is flat.
const bookmarkedWorkflowsTree = computed(() =>
buildTree(workflowStore.bookmarkedWorkflows, (workflow) => [workflow.key])
)
// Open workflows tree is flat.
const openWorkflowsTree = computed(() =>
buildTree(workflowStore.openWorkflows, (workflow) => [workflow.key])
)
const confirm = useConfirm()
const toast = useToast()
const renderTreeNode = (
node: TreeNode,
type: WorkflowTreeType
): TreeExplorerNode<ComfyWorkflow> => {
const children = node.children?.map((child) => renderTreeNode(child, type))
const workflow: ComfyWorkflow = node.data
const handleClick = (
node: TreeExplorerNode<ComfyWorkflow>,
e: MouseEvent
) => {
if (node.leaf) {
workflowService.openWorkflow(workflow)
} else {
toggleNodeOnEvent(e, node)
}
}
const actions = node.leaf
? {
handleClick,
handleRename: async (
node: TreeExplorerNode<ComfyWorkflow>,
newName: string
) => {
const newPath =
type === WorkflowTreeType.Browse
? workflow.directory + '/' + appendJsonExt(newName)
: ComfyWorkflow.basePath + appendJsonExt(newName)
await workflowService.renameWorkflow(workflow, newPath)
},
handleDelete: workflow.isTemporary
? undefined
: async () => {
await workflowService.deleteWorkflow(workflow)
},
contextMenuItems: (node: TreeExplorerNode<ComfyWorkflow>) => {
return [
{
label: t('insert'),
icon: 'pi pi-file-export',
command: () => {
const workflow = node.data
workflowService.insertWorkflow(workflow)
}
}
]
}
}
: { handleClick }
return {
key: node.key,
label: node.label,
leaf: node.leaf,
data: node.data,
children,
...actions
}
}
const selectionKeys = computed(() => ({
[`root/${workflowStore.activeWorkflow?.key}`]: true
}))
const workflowBookmarkStore = useWorkflowBookmarkStore()
onMounted(async () => {
await workflowBookmarkStore.loadBookmarks()
})
</script>