mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-14 01:20:03 +00:00
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>
This commit is contained in:
@@ -1,36 +1,53 @@
|
||||
<template>
|
||||
<div
|
||||
ref="workflowTabRef"
|
||||
class="workflow-tab group flex gap-2 p-2"
|
||||
v-bind="$attrs"
|
||||
@mouseenter="handleMouseEnter"
|
||||
@mouseleave="handleMouseLeave"
|
||||
@click="handleClick"
|
||||
>
|
||||
<i
|
||||
v-if="workflowOption.workflow.activeState?.extra?.linearMode"
|
||||
class="icon-[lucide--panels-top-left] bg-primary-background"
|
||||
/>
|
||||
<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
|
||||
<ContextMenuRoot>
|
||||
<ContextMenuTrigger as-child>
|
||||
<div
|
||||
ref="workflowTabRef"
|
||||
class="workflow-tab group flex gap-2 p-2"
|
||||
v-bind="$attrs"
|
||||
@mouseenter="handleMouseEnter"
|
||||
@mouseleave="handleMouseLeave"
|
||||
@click="handleClick"
|
||||
>
|
||||
<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
|
||||
v-if="workflowOption.workflow.activeState?.extra?.linearMode"
|
||||
class="icon-[lucide--panels-top-left] bg-primary-background"
|
||||
/>
|
||||
<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>
|
||||
</ContextMenuTrigger>
|
||||
<ContextMenuPortal>
|
||||
<ContextMenuContent
|
||||
class="z-1000 rounded-lg px-2 py-3 min-w-56 bg-base-background shadow-interface border border-border-subtle"
|
||||
>
|
||||
<i class="pi pi-times" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<WorkflowActionsList
|
||||
:items="contextMenuItems"
|
||||
:item-component="ContextMenuItem"
|
||||
:separator-component="ContextMenuSeparator"
|
||||
/>
|
||||
</ContextMenuContent>
|
||||
</ContextMenuPortal>
|
||||
</ContextMenuRoot>
|
||||
|
||||
<WorkflowTabPopover
|
||||
ref="popoverRef"
|
||||
@@ -41,20 +58,32 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
ContextMenuContent,
|
||||
ContextMenuItem,
|
||||
ContextMenuPortal,
|
||||
ContextMenuRoot,
|
||||
ContextMenuSeparator,
|
||||
ContextMenuTrigger
|
||||
} from 'reka-ui'
|
||||
import { computed, onUnmounted, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import WorkflowActionsList from '@/components/common/WorkflowActionsList.vue'
|
||||
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 { 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 type { WorkflowMenuItem } from '@/types/workflowMenuItem'
|
||||
|
||||
import WorkflowTabPopover from './WorkflowTabPopover.vue'
|
||||
|
||||
@@ -65,6 +94,8 @@ interface WorkflowOption {
|
||||
|
||||
const props = defineProps<{
|
||||
workflowOption: WorkflowOption
|
||||
isFirst: boolean
|
||||
isLast: boolean
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
@@ -148,6 +179,67 @@ const closeWorkflows = async (options: WorkflowOption[]) => {
|
||||
const onCloseWorkflow = async (option: WorkflowOption) => {
|
||||
await closeWorkflows([option])
|
||||
}
|
||||
|
||||
const emit = defineEmits<{
|
||||
closeToLeft: []
|
||||
closeToRight: []
|
||||
closeOthers: []
|
||||
}>()
|
||||
|
||||
const commandStore = useCommandStore()
|
||||
const workflow = computed(() => props.workflowOption.workflow)
|
||||
|
||||
const { menuItems: baseMenuItems } = useWorkflowActionsMenu(
|
||||
() => commandStore.execute('Comfy.RenameWorkflow'),
|
||||
{ includeDelete: false, workflow }
|
||||
)
|
||||
|
||||
const contextMenuItems = computed<WorkflowMenuItem[]>(() => [
|
||||
...baseMenuItems.value,
|
||||
{ separator: true },
|
||||
{
|
||||
label: t('tabMenu.closeTab'),
|
||||
icon: 'pi pi-times',
|
||||
command: () => onCloseWorkflow(props.workflowOption)
|
||||
},
|
||||
{
|
||||
label: t('tabMenu.closeTabsToLeft'),
|
||||
overlayIcon: {
|
||||
mainIcon: 'pi pi-times',
|
||||
subIcon: 'pi pi-arrow-left',
|
||||
positionX: 'right',
|
||||
positionY: 'bottom',
|
||||
subIconScale: 0.5
|
||||
},
|
||||
command: () => emit('closeToLeft'),
|
||||
disabled: props.isFirst
|
||||
},
|
||||
{
|
||||
label: t('tabMenu.closeTabsToRight'),
|
||||
overlayIcon: {
|
||||
mainIcon: 'pi pi-times',
|
||||
subIcon: 'pi pi-arrow-right',
|
||||
positionX: 'right',
|
||||
positionY: 'bottom',
|
||||
subIconScale: 0.5
|
||||
},
|
||||
command: () => emit('closeToRight'),
|
||||
disabled: props.isLast
|
||||
},
|
||||
{
|
||||
label: t('tabMenu.closeOtherTabs'),
|
||||
overlayIcon: {
|
||||
mainIcon: 'pi pi-times',
|
||||
subIcon: 'pi pi-arrows-h',
|
||||
positionX: 'right',
|
||||
positionY: 'bottom',
|
||||
subIconScale: 0.5
|
||||
},
|
||||
command: () => emit('closeOthers'),
|
||||
disabled: props.isFirst && props.isLast
|
||||
}
|
||||
])
|
||||
|
||||
const tabGetter = () => workflowTabRef.value as HTMLElement
|
||||
|
||||
usePragmaticDraggable(tabGetter, {
|
||||
|
||||
@@ -32,11 +32,20 @@
|
||||
data-key="value"
|
||||
@update:model-value="onWorkflowChange"
|
||||
>
|
||||
<template #option="{ option }">
|
||||
<template #option="{ option, index }">
|
||||
<WorkflowTab
|
||||
:workflow-option="option"
|
||||
@contextmenu="showContextMenu($event, option)"
|
||||
:is-first="index === 0"
|
||||
:is-last="index === options.length - 1"
|
||||
@click.middle="onCloseWorkflow(option)"
|
||||
@close-to-left="closeWorkflows(options.slice(0, index))"
|
||||
@close-to-right="closeWorkflows(options.slice(index + 1))"
|
||||
@close-others="
|
||||
closeWorkflows([
|
||||
...options.slice(index + 1),
|
||||
...options.slice(0, index)
|
||||
])
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
</SelectButton>
|
||||
@@ -58,7 +67,10 @@
|
||||
:active-workflow="workflowStore.activeWorkflow"
|
||||
/>
|
||||
<Button
|
||||
v-tooltip="{ value: $t('sideToolbar.newBlankWorkflow'), showDelay: 300 }"
|
||||
v-tooltip="{
|
||||
value: $t('sideToolbar.newBlankWorkflow'),
|
||||
showDelay: 300
|
||||
}"
|
||||
class="new-blank-workflow-button no-drag shrink-0 rounded-none h-full w-auto aspect-square"
|
||||
variant="muted-textonly"
|
||||
size="icon"
|
||||
@@ -80,27 +92,17 @@
|
||||
/>
|
||||
<LoginButton v-else-if="isDesktop" class="p-1" />
|
||||
</div>
|
||||
<ContextMenu ref="menu" :model="contextMenuItems">
|
||||
<template #itemicon="{ item }">
|
||||
<OverlayIcon v-if="item.overlayIcon" v-bind="item.overlayIcon" />
|
||||
<i v-else-if="item.icon" :class="item.icon" />
|
||||
</template>
|
||||
</ContextMenu>
|
||||
<div v-if="isDesktop" class="window-actions-spacer app-drag shrink-0" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useScroll } from '@vueuse/core'
|
||||
import ContextMenu from 'primevue/contextmenu'
|
||||
import ScrollPanel from 'primevue/scrollpanel'
|
||||
import SelectButton from 'primevue/selectbutton'
|
||||
import { computed, nextTick, onUpdated, ref, watch } from 'vue'
|
||||
import type { WatchStopHandle } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import OverlayIcon from '@/components/common/OverlayIcon.vue'
|
||||
import type { OverlayIconProps } from '@/components/common/OverlayIcon.vue'
|
||||
import CurrentUserButton from '@/components/topbar/CurrentUserButton.vue'
|
||||
import LoginButton from '@/components/topbar/LoginButton.vue'
|
||||
import TopMenuHelpButton from '@/components/topbar/TopMenuHelpButton.vue'
|
||||
@@ -108,7 +110,6 @@ import WorkflowTab from '@/components/topbar/WorkflowTab.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { useCurrentUser } from '@/composables/auth/useCurrentUser'
|
||||
import { useOverflowObserver } from '@/composables/element/useOverflowObserver'
|
||||
import { useWorkflowActionsMenu } from '@/composables/useWorkflowActionsMenu'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useWorkflowService } from '@/platform/workflow/core/services/workflowService'
|
||||
import type { ComfyWorkflow } from '@/platform/workflow/management/stores/workflowStore'
|
||||
@@ -129,7 +130,6 @@ const props = defineProps<{
|
||||
class?: string
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const settingStore = useSettingStore()
|
||||
const workspaceStore = useWorkspaceStore()
|
||||
const workflowStore = useWorkflowStore()
|
||||
@@ -141,8 +141,6 @@ const isIntegratedTabBar = computed(
|
||||
() => settingStore.get('Comfy.UI.TabBarLayout') === 'Integrated'
|
||||
)
|
||||
|
||||
const rightClickedTab = ref<WorkflowOption | undefined>()
|
||||
const menu = ref()
|
||||
const containerRef = ref<HTMLElement | null>(null)
|
||||
const showOverflowArrows = ref(false)
|
||||
const leftArrowEnabled = ref(false)
|
||||
@@ -192,78 +190,6 @@ const onCloseWorkflow = async (option: WorkflowOption) => {
|
||||
await closeWorkflows([option])
|
||||
}
|
||||
|
||||
const showContextMenu = (event: MouseEvent, option: WorkflowOption) => {
|
||||
rightClickedTab.value = option
|
||||
menu.value.show(event)
|
||||
}
|
||||
|
||||
const rightClickedWorkflow = computed(
|
||||
() => rightClickedTab.value?.workflow ?? null
|
||||
)
|
||||
|
||||
const { menuItems: baseMenuItems } = useWorkflowActionsMenu(
|
||||
() => commandStore.execute('Comfy.RenameWorkflow'),
|
||||
{
|
||||
includeDelete: false,
|
||||
workflow: rightClickedWorkflow
|
||||
}
|
||||
)
|
||||
|
||||
const contextMenuItems = computed(() => {
|
||||
const tab = rightClickedTab.value
|
||||
if (!tab) return []
|
||||
const index = options.value.findIndex((v) => v.workflow === tab.workflow)
|
||||
|
||||
return [
|
||||
...baseMenuItems.value,
|
||||
{
|
||||
label: t('tabMenu.closeTab'),
|
||||
icon: 'pi pi-times',
|
||||
command: () => onCloseWorkflow(tab)
|
||||
},
|
||||
{
|
||||
label: t('tabMenu.closeTabsToLeft'),
|
||||
overlayIcon: {
|
||||
mainIcon: 'pi pi-times',
|
||||
subIcon: 'pi pi-arrow-left',
|
||||
positionX: 'right',
|
||||
positionY: 'bottom',
|
||||
subIconScale: 0.5
|
||||
} as OverlayIconProps,
|
||||
command: () => closeWorkflows(options.value.slice(0, index)),
|
||||
disabled: index <= 0
|
||||
},
|
||||
{
|
||||
label: t('tabMenu.closeTabsToRight'),
|
||||
overlayIcon: {
|
||||
mainIcon: 'pi pi-times',
|
||||
subIcon: 'pi pi-arrow-right',
|
||||
positionX: 'right',
|
||||
positionY: 'bottom',
|
||||
subIconScale: 0.5
|
||||
} as OverlayIconProps,
|
||||
command: () => closeWorkflows(options.value.slice(index + 1)),
|
||||
disabled: index === options.value.length - 1
|
||||
},
|
||||
{
|
||||
label: t('tabMenu.closeOtherTabs'),
|
||||
overlayIcon: {
|
||||
mainIcon: 'pi pi-times',
|
||||
subIcon: 'pi pi-arrows-h',
|
||||
positionX: 'right',
|
||||
positionY: 'bottom',
|
||||
subIconScale: 0.5
|
||||
} as OverlayIconProps,
|
||||
command: () =>
|
||||
closeWorkflows([
|
||||
...options.value.slice(index + 1),
|
||||
...options.value.slice(0, index)
|
||||
]),
|
||||
disabled: options.value.length <= 1
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
// Horizontal scroll on wheel
|
||||
const handleWheel = (event: WheelEvent) => {
|
||||
const scrollElement = event.currentTarget as HTMLElement
|
||||
|
||||
Reference in New Issue
Block a user