Files
ComfyUI_frontend/src/composables/useWorkflowActionsMenu.ts
pythongosssss d601aba721 App mode - Unify menus - 2 (#9023)
## Summary

Updates subgraph breadcrumbs menu, workflow tabs context menu & linear
mode menu to use a single implementation.
Adds new menu items for enter/exit app mode  
Hides menu when in builder mode

## Changes

- **What**: Changes the components to use either a reka-ui context menu
or dropdown, with a standard inner list
- **Breaking**: Remove existing linear toggle from sidebar as it is now
in the menu


## Screenshots (if applicable)
It looks basically identical other than the icon changes based on mode:

In Graph Mode:
<img width="261" height="497" alt="image"
src="https://github.com/user-attachments/assets/eb9968a2-b528-4e21-9e14-ab4a67e717ae"
/>

In App Mode:
<img width="254" height="499" alt="image"
src="https://github.com/user-attachments/assets/54a89fab-e7b2-4cb0-bcb7-43d6d076ac83"
/>

Right click tab:
<img width="321" height="564" alt="image"
src="https://github.com/user-attachments/assets/c12c7d64-2dba-45bb-be76-2615f3e38cc6"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9023-App-mode-Unify-menus-2-30d6d73d36508162bfc0e308d5f705de)
by [Unito](https://www.unito.io)

---------

Co-authored-by: github-actions <github-actions@github.com>
2026-02-23 09:49:52 -08:00

233 lines
6.5 KiB
TypeScript

import type { ComputedRef, Ref } from 'vue'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { useFeatureFlags } from '@/composables/useFeatureFlags'
import { useWorkflowService } from '@/platform/workflow/core/services/workflowService'
import type { ComfyWorkflow } from '@/platform/workflow/management/stores/workflowStore'
import {
useWorkflowBookmarkStore,
useWorkflowStore
} from '@/platform/workflow/management/stores/workflowStore'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { useCommandStore } from '@/stores/commandStore'
import { useMenuItemStore } from '@/stores/menuItemStore'
import { useSubgraphStore } from '@/stores/subgraphStore'
import type {
WorkflowMenuAction,
WorkflowMenuItem
} from '@/types/workflowMenuItem'
interface WorkflowActionsMenuOptions {
/** Whether this is the root workflow level. Defaults to true. */
isRoot?: boolean
/** Whether to include the delete workflow action. Defaults to true. */
includeDelete?: boolean
/** Override the workflow to operate on. If not provided, uses activeWorkflow. */
workflow?: Ref<ComfyWorkflow | null> | ComputedRef<ComfyWorkflow | null>
}
interface AddItemOptions {
label: string
icon: string
command: () => void
visible?: boolean
disabled?: boolean
prependSeparator?: boolean
isNew?: boolean
}
export function useWorkflowActionsMenu(
startRename: () => void,
options: WorkflowActionsMenuOptions = {}
) {
const { isRoot = true, includeDelete = true, workflow } = options
const { t } = useI18n()
const workflowStore = useWorkflowStore()
const workflowService = useWorkflowService()
const bookmarkStore = useWorkflowBookmarkStore()
const commandStore = useCommandStore()
const subgraphStore = useSubgraphStore()
const menuItemStore = useMenuItemStore()
const canvasStore = useCanvasStore()
const { flags } = useFeatureFlags()
const targetWorkflow = computed(
() => workflow?.value ?? workflowStore.activeWorkflow
)
/** Switch to the target workflow tab if it's not already active */
const ensureWorkflowActive = async (wf: ComfyWorkflow | null) => {
if (!wf || wf === workflowStore.activeWorkflow) return
await workflowService.openWorkflow(wf)
}
const menuItems = computed<WorkflowMenuItem[]>(() => {
const workflow = targetWorkflow.value
const isBlueprint = workflow
? subgraphStore.isSubgraphBlueprint(workflow)
: false
const items: WorkflowMenuItem[] = []
const addItem = ({
label,
icon,
command,
visible = true,
disabled = false,
prependSeparator = false,
isNew = false
}: AddItemOptions) => {
if (!visible) return
if (prependSeparator) items.push({ separator: true })
const item: WorkflowMenuAction = { label, icon, command, disabled }
if (isNew) {
item.badge = t('contextMenu.new')
}
items.push(item)
}
const isLinearMode = canvasStore.linearMode
const showAppModeItems =
isRoot && (menuItemStore.hasSeenLinear || flags.linearToggleEnabled)
const isBookmarked = bookmarkStore.isBookmarked(workflow?.path ?? '')
addItem({
label: t('g.rename'),
icon: 'pi pi-pencil',
command: async () => {
await ensureWorkflowActive(targetWorkflow.value)
startRename()
},
disabled: isRoot && !workflow?.isPersisted
})
addItem({
label: t('breadcrumbsMenu.duplicate'),
icon: 'pi pi-copy',
command: async () => {
if (workflow) {
await workflowService.duplicateWorkflow(workflow)
}
},
visible: isRoot && !isBlueprint
})
addItem({
label: isBookmarked
? t('tabMenu.removeFromBookmarks')
: t('tabMenu.addToBookmarks'),
icon: 'pi pi-bookmark' + (isBookmarked ? '-fill' : ''),
command: async () => {
if (workflow?.path) {
await bookmarkStore.toggleBookmarked(workflow.path)
}
},
visible: isRoot,
disabled: workflow?.isTemporary ?? false
})
addItem({
label: t('menuLabels.Save'),
icon: 'pi pi-save',
command: async () => {
await ensureWorkflowActive(workflow)
await commandStore.execute('Comfy.SaveWorkflow')
},
visible: isRoot,
prependSeparator: true
})
addItem({
label: t('menuLabels.Save As'),
icon: 'pi pi-save',
command: async () => {
await ensureWorkflowActive(workflow)
await commandStore.execute('Comfy.SaveWorkflowAs')
},
visible: isRoot
})
addItem({
label: t('menuLabels.Export'),
icon: 'pi pi-download',
command: async () => {
await ensureWorkflowActive(workflow)
await commandStore.execute('Comfy.ExportWorkflow')
},
visible: isRoot,
prependSeparator: true
})
addItem({
label: t('menuLabels.Export (API)'),
icon: 'pi pi-download',
command: async () => {
await ensureWorkflowActive(workflow)
await commandStore.execute('Comfy.ExportWorkflowAPI')
},
visible: isRoot
})
addItem({
label: isLinearMode
? t('breadcrumbsMenu.exitAppMode')
: t('breadcrumbsMenu.enterAppMode'),
icon: isLinearMode
? 'icon-[comfy--workflow]'
: 'icon-[lucide--panels-top-left]',
command: async () => {
await commandStore.execute('Comfy.ToggleLinear', {
metadata: { source: 'breadcrumb_menu' }
})
},
visible: showAppModeItems,
prependSeparator: true,
isNew: !isLinearMode
})
addItem({
label: t('breadcrumbsMenu.clearWorkflow'),
icon: 'pi pi-trash',
command: async () => {
await ensureWorkflowActive(workflow)
await commandStore.execute('Comfy.ClearWorkflow')
},
prependSeparator: true
})
addItem({
label: t('subgraphStore.publish'),
icon: 'pi pi-upload',
command: async () => {
if (workflow) {
await workflowService.saveWorkflowAs(workflow)
}
},
visible: isRoot && isBlueprint,
prependSeparator: true
})
addItem({
label: isBlueprint
? t('breadcrumbsMenu.deleteBlueprint')
: t('breadcrumbsMenu.deleteWorkflow'),
icon: 'pi pi-times',
command: async () => {
if (workflow) {
await workflowService.deleteWorkflow(workflow)
}
},
visible: isRoot && includeDelete,
prependSeparator: true
})
return items
})
return {
menuItems
}
}