mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-24 16:54:03 +00:00
## 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>
287 lines
8.3 KiB
Vue
287 lines
8.3 KiB
Vue
<template>
|
|
<div
|
|
class="subgraph-breadcrumb flex w-auto drop-shadow-[var(--interface-panel-drop-shadow)] items-center"
|
|
:class="{
|
|
'subgraph-breadcrumb-collapse': collapseTabs,
|
|
'subgraph-breadcrumb-overflow': overflowingTabs
|
|
}"
|
|
:style="{
|
|
'--p-breadcrumb-gap': `0px`,
|
|
'--p-breadcrumb-item-margin': `${ITEM_GAP / 2}px`,
|
|
'--p-breadcrumb-item-min-width': `${MIN_WIDTH}px`,
|
|
'--p-breadcrumb-item-padding': `${ITEM_PADDING}px`,
|
|
'--p-breadcrumb-icon-width': `${ICON_WIDTH}px`
|
|
}"
|
|
>
|
|
<WorkflowActionsDropdown source="breadcrumb_subgraph_menu_selected" />
|
|
<Button
|
|
v-if="isInSubgraph"
|
|
class="back-button pointer-events-auto h-8 w-8 shrink-0 border border-transparent bg-transparent p-0 ml-1.5 transition-all hover:rounded-lg hover:border-interface-stroke hover:bg-comfy-menu-bg"
|
|
text
|
|
severity="secondary"
|
|
size="small"
|
|
@click="handleBackClick"
|
|
>
|
|
<i class="icon-[lucide--undo-2]" />
|
|
</Button>
|
|
<Breadcrumb
|
|
ref="breadcrumbRef"
|
|
class="w-fit rounded-lg p-0"
|
|
:class="{ hidden: !isInSubgraph }"
|
|
:model="items"
|
|
:pt="{ item: { class: 'pointer-events-auto' } }"
|
|
:aria-label="$t('g.graphNavigation')"
|
|
>
|
|
<template #item="{ item }">
|
|
<SubgraphBreadcrumbItem
|
|
:item="item"
|
|
:is-active="item.key === activeItemKey"
|
|
/>
|
|
</template>
|
|
<template #separator
|
|
><span style="transform: scale(1.5)"> / </span></template
|
|
>
|
|
</Breadcrumb>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import Breadcrumb from 'primevue/breadcrumb'
|
|
import Button from 'primevue/button'
|
|
import type { MenuItem } from 'primevue/menuitem'
|
|
import { computed, onUpdated, ref, watch } from 'vue'
|
|
|
|
import SubgraphBreadcrumbItem from '@/components/breadcrumb/SubgraphBreadcrumbItem.vue'
|
|
import WorkflowActionsDropdown from '@/components/common/WorkflowActionsDropdown.vue'
|
|
import { useOverflowObserver } from '@/composables/element/useOverflowObserver'
|
|
import { useTelemetry } from '@/platform/telemetry'
|
|
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
|
|
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
|
import { useCommandStore } from '@/stores/commandStore'
|
|
import { useSubgraphNavigationStore } from '@/stores/subgraphNavigationStore'
|
|
import { useSubgraphStore } from '@/stores/subgraphStore'
|
|
import { forEachSubgraphNode } from '@/utils/graphTraversalUtil'
|
|
|
|
const MIN_WIDTH = 28
|
|
const ITEM_GAP = 8
|
|
const ITEM_PADDING = 8
|
|
const ICON_WIDTH = 20
|
|
|
|
const workflowStore = useWorkflowStore()
|
|
const navigationStore = useSubgraphNavigationStore()
|
|
const breadcrumbRef = ref<InstanceType<typeof Breadcrumb>>()
|
|
const workflowName = computed(() => workflowStore.activeWorkflow?.filename)
|
|
const isBlueprint = computed(() =>
|
|
useSubgraphStore().isSubgraphBlueprint(workflowStore.activeWorkflow)
|
|
)
|
|
const collapseTabs = ref(false)
|
|
const overflowingTabs = ref(false)
|
|
|
|
const isInSubgraph = computed(() => navigationStore.navigationStack.length > 0)
|
|
|
|
const home = computed(() => ({
|
|
label: workflowName.value,
|
|
icon: 'pi pi-home',
|
|
key: 'root',
|
|
isBlueprint: isBlueprint.value,
|
|
command: () => {
|
|
useTelemetry()?.trackUiButtonClicked({
|
|
button_id: 'breadcrumb_subgraph_root_selected'
|
|
})
|
|
const canvas = useCanvasStore().getCanvas()
|
|
if (!canvas.graph) throw new TypeError('Canvas has no graph')
|
|
|
|
canvas.setGraph(canvas.graph.rootGraph)
|
|
}
|
|
}))
|
|
|
|
const items = computed(() => {
|
|
const items = navigationStore.navigationStack.map<MenuItem>((subgraph) => ({
|
|
label: subgraph.name,
|
|
key: `subgraph-${subgraph.id}`,
|
|
command: () => {
|
|
useTelemetry()?.trackUiButtonClicked({
|
|
button_id: 'breadcrumb_subgraph_item_selected'
|
|
})
|
|
const canvas = useCanvasStore().getCanvas()
|
|
if (!canvas.graph) throw new TypeError('Canvas has no graph')
|
|
|
|
canvas.setGraph(subgraph)
|
|
},
|
|
updateTitle: (title: string) => {
|
|
const rootGraph = useCanvasStore().getCanvas().graph?.rootGraph
|
|
if (!rootGraph) return
|
|
|
|
forEachSubgraphNode(rootGraph, subgraph.id, (node) => {
|
|
node.title = title
|
|
})
|
|
}
|
|
}))
|
|
|
|
return [home.value, ...items]
|
|
})
|
|
|
|
const activeItemKey = computed(() => items.value.at(-1)?.key)
|
|
|
|
const handleBackClick = () => {
|
|
void useCommandStore().execute('Comfy.Graph.ExitSubgraph')
|
|
}
|
|
|
|
const breadcrumbElement = computed(() => {
|
|
if (!breadcrumbRef.value) return null
|
|
|
|
const el = (breadcrumbRef.value as unknown as { $el: HTMLElement }).$el
|
|
const list = el?.querySelector('.p-breadcrumb-list') as HTMLElement
|
|
return list
|
|
})
|
|
|
|
// Check for overflow on breadcrumb items and collapse/expand the breadcrumb to fit
|
|
let overflowObserver: ReturnType<typeof useOverflowObserver> | undefined
|
|
watch(breadcrumbElement, (el) => {
|
|
overflowObserver?.dispose()
|
|
overflowObserver = undefined
|
|
|
|
if (!el) return
|
|
|
|
overflowObserver = useOverflowObserver(el, {
|
|
onCheck: (isOverflowing) => {
|
|
overflowingTabs.value = isOverflowing
|
|
|
|
if (collapseTabs.value) {
|
|
// Items are currently hidden, check if we can show them
|
|
if (!isOverflowing) {
|
|
const items = [
|
|
...el.querySelectorAll('.p-breadcrumb-item')
|
|
] as HTMLElement[]
|
|
|
|
if (items.length < 3) return
|
|
|
|
const itemsWithIcon = items.filter((item) =>
|
|
item.querySelector('.p-breadcrumb-item-link-icon-visible')
|
|
).length
|
|
const separators = el.querySelectorAll(
|
|
'.p-breadcrumb-separator'
|
|
) as NodeListOf<HTMLElement>
|
|
const separator = separators[separators.length - 1] as HTMLElement
|
|
const separatorWidth = separator.offsetWidth
|
|
|
|
// items + separators + gaps + icons
|
|
const itemsWidth =
|
|
(MIN_WIDTH + ITEM_PADDING + ITEM_PADDING) * items.length +
|
|
itemsWithIcon * ICON_WIDTH
|
|
const separatorsWidth = (items.length - 1) * separatorWidth
|
|
const gapsWidth = (items.length - 1) * (ITEM_GAP * 2)
|
|
const totalWidth = itemsWidth + separatorsWidth + gapsWidth
|
|
const containerWidth = el.clientWidth
|
|
|
|
if (totalWidth <= containerWidth) {
|
|
collapseTabs.value = false
|
|
}
|
|
}
|
|
} else if (isOverflowing) {
|
|
collapseTabs.value = true
|
|
}
|
|
}
|
|
})
|
|
})
|
|
|
|
// If e.g. the workflow name changes, we need to check the overflow again
|
|
onUpdated(() => {
|
|
if (!overflowObserver?.disposed.value) {
|
|
overflowObserver?.checkOverflow()
|
|
}
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
@reference '../../assets/css/style.css';
|
|
|
|
.subgraph-breadcrumb:not(:empty) {
|
|
flex: auto;
|
|
flex-shrink: 10000;
|
|
min-width: 120px;
|
|
}
|
|
|
|
.subgraph-breadcrumb,
|
|
:deep(.p-breadcrumb) {
|
|
@apply overflow-hidden;
|
|
}
|
|
|
|
:deep(.p-breadcrumb) {
|
|
width: 100%;
|
|
background-color: transparent;
|
|
}
|
|
|
|
:deep(.p-breadcrumb-item) {
|
|
@apply flex items-center overflow-hidden h-8;
|
|
min-width: calc(var(--p-breadcrumb-item-min-width) + 1rem);
|
|
border: 1px solid transparent;
|
|
background-color: transparent;
|
|
transition: all 0.2s;
|
|
/* Collapse middle items first */
|
|
flex-shrink: 10000;
|
|
}
|
|
|
|
:deep(.p-breadcrumb-separator) {
|
|
border: 1px solid transparent;
|
|
background-color: transparent;
|
|
display: flex;
|
|
padding: 0 var(--p-breadcrumb-item-margin);
|
|
}
|
|
|
|
:deep(.p-breadcrumb-item-link) {
|
|
padding: 0
|
|
calc(var(--p-breadcrumb-item-margin) + var(--p-breadcrumb-item-padding));
|
|
}
|
|
|
|
:deep(.p-breadcrumb-item:hover) {
|
|
@apply rounded-lg;
|
|
border-color: var(--interface-stroke);
|
|
background-color: var(--comfy-menu-bg);
|
|
}
|
|
|
|
:deep(.p-breadcrumb-item:has(.p-breadcrumb-item-link-icon-visible)) {
|
|
min-width: calc(var(--p-breadcrumb-item-min-width) + 1rem + 20px);
|
|
}
|
|
|
|
:deep(.p-breadcrumb-item:first-child) {
|
|
/* Then collapse the root workflow */
|
|
flex-shrink: 5000;
|
|
|
|
.p-breadcrumb-item-link {
|
|
padding-left: var(--p-breadcrumb-item-padding);
|
|
}
|
|
}
|
|
|
|
:deep(.p-breadcrumb-item:last-child) {
|
|
/* Then collapse the active item */
|
|
flex-shrink: 1;
|
|
}
|
|
|
|
:deep(.p-breadcrumb-item-link-menu-visible) {
|
|
background-color: color-mix(
|
|
in srgb,
|
|
var(--fg-color) 10%,
|
|
var(--comfy-menu-bg)
|
|
) !important;
|
|
color: var(--fg-color);
|
|
}
|
|
</style>
|
|
|
|
<style>
|
|
@reference '../../assets/css/style.css';
|
|
|
|
.subgraph-breadcrumb-collapse .p-breadcrumb-list {
|
|
.p-breadcrumb-item,
|
|
.p-breadcrumb-separator {
|
|
@apply hidden;
|
|
}
|
|
|
|
.p-breadcrumb-item:nth-last-child(3),
|
|
.p-breadcrumb-separator:nth-last-child(2),
|
|
.p-breadcrumb-item:nth-last-child(1) {
|
|
@apply flex;
|
|
}
|
|
}
|
|
</style>
|