Files
ComfyUI_frontend/src/components/sidebar/tabs/nodeLibrary/NodeTreeLeaf.vue
pythongosssss 3bbae61763 Decouple node help between sidebar and right panel (#8110)
## Summary

When the node library is open and you click on the node toolbar info
button, this causes the node library info panel & right panel node info
to show the same details.

## Changes

- Extract useNodeHelpContent composable so NodeHelpContent fetches its
own content, allowing multiple panels to show help independently
- Remove sync behavior from NodeHelpPage that caused left sidebar to
change when selecting different graph nodes since we want to prioritise
right panel for this behavior
- Add telemetry tracking for node library help button to identify how
frequently this is used

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8110-Decouple-node-help-between-sidebar-and-right-panel-2ea6d73d365081a9b3afd25aa51b34bd)
by [Unito](https://www.unito.io)
2026-01-16 22:13:23 -07:00

206 lines
6.0 KiB
Vue

<template>
<div ref="container" class="node-lib-node-container">
<TreeExplorerTreeNode :node="node" @contextmenu="handleContextMenu">
<template #before-label>
<Tag
v-if="nodeDef.experimental"
:value="$t('g.experimental')"
severity="primary"
/>
<Tag
v-if="nodeDef.deprecated"
:value="$t('g.deprecated')"
severity="danger"
/>
</template>
<template
v-if="nodeDef.name.startsWith(useSubgraphStore().typePrefix)"
#actions
>
<Button
variant="destructive"
size="icon-sm"
:aria-label="$t('g.delete')"
@click.stop="deleteBlueprint"
>
<i class="icon-[lucide--trash-2] size-3.5" />
</Button>
<Button
variant="muted-textonly"
size="icon-sm"
:aria-label="$t('g.edit')"
@click.stop="editBlueprint"
>
<i class="icon-[lucide--square-pen] size-3.5" />
</Button>
</template>
<template v-else #actions>
<Button
class="bookmark-button"
variant="muted-textonly"
size="icon-sm"
:aria-label="$t('icon.bookmark')"
@click.stop="toggleBookmark"
>
<i
:class="
cn(
isBookmarked ? 'pi pi-bookmark-fill' : 'pi pi-bookmark',
'size-3.5'
)
"
/>
</Button>
<Button
v-tooltip.bottom="$t('g.learnMore')"
class="help-button"
variant="muted-textonly"
size="icon-sm"
:aria-label="$t('g.learnMore')"
@click.stop="onHelpClick"
>
<i class="pi pi-question size-3.5" />
</Button>
</template>
</TreeExplorerTreeNode>
<teleport v-if="isHovered" to="#node-library-node-preview-container">
<div class="node-lib-node-preview" :style="nodePreviewStyle">
<NodePreview :node-def="nodeDef" />
</div>
</teleport>
</div>
<ContextMenu ref="menu" :model="menuItems" />
</template>
<script setup lang="ts">
import ContextMenu from 'primevue/contextmenu'
import type { MenuItem } from 'primevue/menuitem'
import Tag from 'primevue/tag'
import type { CSSProperties } from 'vue'
import { computed, nextTick, onMounted, onUnmounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import TreeExplorerTreeNode from '@/components/common/TreeExplorerTreeNode.vue'
import NodePreview from '@/components/node/NodePreview.vue'
import Button from '@/components/ui/button/Button.vue'
import { useSettingStore } from '@/platform/settings/settingStore'
import { useTelemetry } from '@/platform/telemetry'
import { useNodeBookmarkStore } from '@/stores/nodeBookmarkStore'
import type { ComfyNodeDefImpl } from '@/stores/nodeDefStore'
import { useSubgraphStore } from '@/stores/subgraphStore'
import type { RenderedTreeExplorerNode } from '@/types/treeExplorerTypes'
import { cn } from '@/utils/tailwindUtil'
const { t } = useI18n()
const props = defineProps<{
node: RenderedTreeExplorerNode<ComfyNodeDefImpl>
openNodeHelp: (nodeDef: ComfyNodeDefImpl) => void
}>()
// Note: node.data should be present for leaf nodes.
const nodeDef = computed(() => props.node.data!)
const nodeBookmarkStore = useNodeBookmarkStore()
const isBookmarked = computed(() =>
nodeBookmarkStore.isBookmarked(nodeDef.value)
)
const settingStore = useSettingStore()
const sidebarLocation = computed<'left' | 'right'>(() =>
settingStore.get('Comfy.Sidebar.Location')
)
const toggleBookmark = async () => {
await nodeBookmarkStore.toggleBookmark(nodeDef.value)
}
const onHelpClick = () => {
useTelemetry()?.trackUiButtonClicked({
button_id: 'node_library_help_button'
})
props.openNodeHelp(nodeDef.value)
}
const editBlueprint = async () => {
if (!props.node.data)
throw new Error(
'Failed to edit subgraph blueprint lacking backing node data'
)
await useSubgraphStore().editBlueprint(props.node.data.name)
}
const menu = ref<InstanceType<typeof ContextMenu> | null>(null)
const menuItems = computed<MenuItem[]>(() => {
const items: MenuItem[] = [
{
label: t('g.delete'),
icon: 'pi pi-trash',
severity: 'error',
command: deleteBlueprint
}
]
return items
})
function handleContextMenu(event: Event) {
if (!nodeDef.value.name.startsWith(useSubgraphStore().typePrefix)) return
menu.value?.show(event)
}
function deleteBlueprint() {
if (!props.node.data) return
void useSubgraphStore().deleteBlueprint(props.node.data.name)
}
const nodePreviewStyle = ref<CSSProperties>({
position: 'fixed',
top: '0px',
left: '0px',
pointerEvents: 'none',
zIndex: 1001
})
const handleNodeHover = async () => {
const hoverTarget = nodeContentElement.value
if (!hoverTarget) return
const targetRect = hoverTarget.getBoundingClientRect()
const margin = 40
nodePreviewStyle.value.top = `${targetRect.top}px`
nodePreviewStyle.value.left =
sidebarLocation.value === 'left'
? `${targetRect.right + margin}px`
: `${targetRect.left - margin}px`
nodePreviewStyle.value.transform =
sidebarLocation.value === 'right' ? 'translateX(-100%)' : undefined
}
const container = ref<HTMLElement | null>(null)
const nodeContentElement = ref<HTMLElement | null>(null)
const isHovered = ref(false)
const handleMouseEnter = async () => {
isHovered.value = true
await nextTick()
await handleNodeHover()
}
const handleMouseLeave = () => {
isHovered.value = false
}
onMounted(() => {
nodeContentElement.value =
container.value?.closest('.p-tree-node-content') ?? null
nodeContentElement.value?.addEventListener('mouseenter', handleMouseEnter)
nodeContentElement.value?.addEventListener('mouseleave', handleMouseLeave)
})
onUnmounted(() => {
nodeContentElement.value?.removeEventListener('mouseenter', handleMouseEnter)
nodeContentElement.value?.removeEventListener('mouseleave', handleMouseLeave)
})
</script>
<style scoped>
@reference '../../../../assets/css/style.css';
.node-lib-node-container {
@apply h-full w-full;
}
</style>