mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-19 22:09:37 +00:00
Backport of #10992 to `cloud/1.43` Automatically created by backport workflow. ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-11158-backport-cloud-1-43-fix-use-standard-size-4-for-blueprint-action-icons-33f6d73d36508159921fd8f26e05c1f7) by [Unito](https://www.unito.io) Co-authored-by: Christian Byrne <cbyrne@comfy.org>
209 lines
6.2 KiB
Vue
209 lines
6.2 KiB
Vue
<template>
|
|
<div
|
|
ref="container"
|
|
class="node-lib-node-container size-full"
|
|
data-testid="node-tree-leaf"
|
|
:data-node-name="nodeDef.display_name"
|
|
>
|
|
<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="isUserBlueprint" #actions>
|
|
<Button
|
|
variant="destructive"
|
|
size="icon-sm"
|
|
:aria-label="$t('g.delete')"
|
|
@click.stop="deleteBlueprint"
|
|
>
|
|
<i class="icon-[lucide--trash-2] size-4" />
|
|
</Button>
|
|
<Button
|
|
variant="muted-textonly"
|
|
size="icon-sm"
|
|
:aria-label="$t('g.edit')"
|
|
@click.stop="editBlueprint"
|
|
>
|
|
<i class="icon-[lucide--square-pen] size-4" />
|
|
</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 subgraphStore = useSubgraphStore()
|
|
const isUserBlueprint = computed(() => {
|
|
const name = nodeDef.value.name
|
|
if (!name.startsWith(subgraphStore.typePrefix)) return false
|
|
return !subgraphStore.isGlobalBlueprint(
|
|
name.slice(subgraphStore.typePrefix.length)
|
|
)
|
|
})
|
|
const menuItems = computed<MenuItem[]>(() => {
|
|
if (!isUserBlueprint.value) return []
|
|
|
|
return [
|
|
{
|
|
label: t('g.delete'),
|
|
icon: 'pi pi-trash',
|
|
severity: 'error',
|
|
command: deleteBlueprint
|
|
}
|
|
]
|
|
})
|
|
function handleContextMenu(event: Event) {
|
|
if (!isUserBlueprint.value) return
|
|
menu.value?.show(event)
|
|
}
|
|
function deleteBlueprint() {
|
|
if (!props.node.data) return
|
|
void subgraphStore.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>
|