Files
ComfyUI_frontend/src/components/topbar/WorkflowTab.vue
pythongosssss dfb78b2e87 Subgraph/workflow breadcrumbs menu updates (#7852)
## Summary
For users who don't use subgraphs, the workflow name in the top left can
be unnecessarily obstructive so this updated collapses it to a simple
icon until a subgraph is entered.

## Changes

- Add menu button to WorkflowTab for quick workflow actions
- Add menu and back button to SubgraphBreadcrumb
- Extract shared menu items to useBreadcrumbMenu composable
- Add Comfy.RenameWorkflow command for renaming persisted workflows
- Menu always shows root workflow menu, even when in subgraph

## Screenshots (if applicable)

<img width="399" height="396" alt="image"
src="https://github.com/user-attachments/assets/701ab60e-790f-4d1e-a817-dc42b2d98712"
/>
<img width="569" height="381" alt="image"
src="https://github.com/user-attachments/assets/fcea3ab0-8388-4c72-a649-1428c1defd6a"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7852-Subgraph-workflow-breadcrumbs-menu-updates-2df6d73d3650815b8490ca0a9a92d540)
by [Unito](https://www.unito.io)

---------

Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: GitHub Action <action@github.com>
2026-01-12 16:08:28 -07:00

229 lines
6.3 KiB
Vue

<template>
<div
ref="workflowTabRef"
class="workflow-tab group flex gap-2 p-2"
v-bind="$attrs"
@mouseenter="handleMouseEnter"
@mouseleave="handleMouseLeave"
@click="handleClick"
>
<Button
v-if="isActiveTab"
class="context-menu-button -mx-1 w-auto px-1 py-0"
variant="muted-textonly"
size="icon-sm"
@click.stop="handleMenuClick"
>
<i class="pi pi-bars" />
</Button>
<span class="workflow-label inline-block max-w-[150px] truncate text-sm">
{{ workflowOption.workflow.filename }}
</span>
<div class="relative">
<span
v-if="shouldShowStatusIndicator"
class="absolute top-1/2 left-1/2 z-10 w-4 -translate-1/2 bg-(--comfy-menu-bg) text-2xl font-bold group-hover:hidden"
></span
>
<Button
class="close-button invisible w-auto p-0"
variant="muted-textonly"
size="icon-sm"
:aria-label="t('g.close')"
@click.stop="onCloseWorkflow(workflowOption)"
>
<i class="pi pi-times" />
</Button>
</div>
</div>
<WorkflowTabPopover
ref="popoverRef"
:workflow-filename="workflowOption.workflow.filename"
:thumbnail-url="thumbnailUrl"
:is-active-tab="isActiveTab"
/>
<Menu
v-if="isActiveTab"
ref="menu"
:model="menuItems"
:popup="true"
:pt="{
root: {
style: 'background-color: var(--comfy-menu-bg)'
},
itemLink: {
class: 'py-2'
}
}"
/>
</template>
<script setup lang="ts">
import type { MenuState } from 'primevue/menu'
import Menu from 'primevue/menu'
import { computed, onUnmounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import Button from '@/components/ui/button/Button.vue'
import {
usePragmaticDraggable,
usePragmaticDroppable
} from '@/composables/usePragmaticDragAndDrop'
import { useWorkflowActionsMenu } from '@/composables/useWorkflowActionsMenu'
import { useSettingStore } from '@/platform/settings/settingStore'
import { useTelemetry } from '@/platform/telemetry'
import { useWorkflowService } from '@/platform/workflow/core/services/workflowService'
import type { ComfyWorkflow } from '@/platform/workflow/management/stores/workflowStore'
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
import { useWorkflowThumbnail } from '@/renderer/core/thumbnail/useWorkflowThumbnail'
import { useCommandStore } from '@/stores/commandStore'
import { useWorkspaceStore } from '@/stores/workspaceStore'
import WorkflowTabPopover from './WorkflowTabPopover.vue'
interface WorkflowOption {
value: string
workflow: ComfyWorkflow
}
const props = defineProps<{
workflowOption: WorkflowOption
}>()
const { t } = useI18n()
const workspaceStore = useWorkspaceStore()
const workflowStore = useWorkflowStore()
const settingStore = useSettingStore()
const workflowTabRef = ref<HTMLElement | null>(null)
const popoverRef = ref<InstanceType<typeof WorkflowTabPopover> | null>(null)
const workflowThumbnail = useWorkflowThumbnail()
// Use computed refs to cache autosave settings
const autoSaveSetting = computed(() =>
settingStore.get('Comfy.Workflow.AutoSave')
)
const autoSaveDelay = computed(() =>
settingStore.get('Comfy.Workflow.AutoSaveDelay')
)
const shouldShowStatusIndicator = computed(() => {
if (workspaceStore.shiftDown) {
// Branch 1: Shift key is held down, do not show the status indicator.
return false
}
if (!props.workflowOption.workflow.isPersisted) {
// Branch 2: Workflow is not persisted, show the status indicator.
return true
}
if (props.workflowOption.workflow.isModified) {
// Branch 3: Workflow is modified.
if (autoSaveSetting.value === 'off') {
// Sub-branch 3a: Autosave is off, so show the status indicator.
return true
}
if (autoSaveSetting.value === 'after delay' && autoSaveDelay.value > 3000) {
// Sub-branch 3b: Autosave delay is too high, so show the status indicator.
return true
}
// Sub-branch 3c: Workflow is modified but no condition applies, do not show the status indicator.
return false
}
// Default: do not show the status indicator. This should not be reachable.
return false
})
const isActiveTab = computed(() => {
return workflowStore.activeWorkflow?.key === props.workflowOption.workflow.key
})
const thumbnailUrl = computed(() => {
return workflowThumbnail.getThumbnail(props.workflowOption.workflow.key)
})
const menu = ref<InstanceType<typeof Menu> & MenuState>()
const { menuItems } = useWorkflowActionsMenu(() =>
useCommandStore().execute('Comfy.RenameWorkflow')
)
// Event handlers that delegate to the popover component
const handleMouseEnter = (event: Event) => {
popoverRef.value?.showPopover(event)
}
const handleMouseLeave = () => {
popoverRef.value?.hidePopover()
}
const handleClick = (event: Event) => {
popoverRef.value?.togglePopover(event)
}
const handleMenuClick = (event: MouseEvent) => {
useTelemetry()?.trackUiButtonClicked({
button_id: 'workflow_tab_menu_selected'
})
// Show breadcrumb menu instead of emitting context click
menu.value?.toggle(event)
}
const closeWorkflows = async (options: WorkflowOption[]) => {
for (const opt of options) {
if (
!(await useWorkflowService().closeWorkflow(opt.workflow, {
warnIfUnsaved: !workspaceStore.shiftDown,
hint: t('sideToolbar.workflowTab.dirtyCloseHint')
}))
) {
// User clicked cancel
break
}
}
}
const onCloseWorkflow = async (option: WorkflowOption) => {
await closeWorkflows([option])
}
const tabGetter = () => workflowTabRef.value as HTMLElement
usePragmaticDraggable(tabGetter, {
getInitialData: () => {
return {
workflowKey: props.workflowOption.workflow.key
}
}
})
usePragmaticDroppable(tabGetter, {
getData: () => {
return {
workflowKey: props.workflowOption.workflow.key
}
},
onDrop: (e) => {
const fromIndex = workflowStore.openWorkflows.findIndex(
(wf) => wf.key === e.source.data.workflowKey
)
const toIndex = workflowStore.openWorkflows.findIndex(
(wf) => wf.key === e.location.current.dropTargets[0]?.data.workflowKey
)
if (fromIndex !== toIndex) {
workflowStore.reorderWorkflows(fromIndex, toIndex)
}
}
})
onUnmounted(() => {
popoverRef.value?.hidePopover()
})
</script>
<style>
.p-tooltip.workflow-tab-tooltip {
z-index: 1200 !important;
}
</style>