Node library side bar tab (#237)

* Basic tree

* Add node filter

* Fix key issue

* Add icons

* Node count

* nit

* Add node preview basics

* Node preview

* Make comfy node in node lib draggable

* Set drop target

* Add node on drop

* Drop on dynamic location

* nit

* nit

* Fix hover preview issue

* nit

* More visual diff between node and folder

* Add playwright test

* Add dep

* Get rid of screenshot test
This commit is contained in:
Chenlei Hu
2024-07-27 21:28:48 -04:00
committed by GitHub
parent 980ed0083d
commit c0875d066a
11 changed files with 376 additions and 6 deletions

View File

@@ -0,0 +1,106 @@
<!-- Tree with all leaf nodes draggable -->
<script>
import Tree from 'primevue/tree'
import { draggable } from '@atlaskit/pragmatic-drag-and-drop/element/adapter'
import { h, onMounted, onBeforeUnmount, computed } from 'vue'
export default {
name: 'TreePlus',
extends: Tree,
props: {
dragSelector: {
type: String,
default: '.p-tree-node'
},
// Explicitly declare all v-model props
expandedKeys: {
type: Object,
default: () => ({})
},
selectionKeys: {
type: Object,
default: () => ({})
}
},
emits: ['update:expandedKeys', 'update:selectionKeys'],
setup(props, context) {
// Create computed properties for each v-model prop
const computedExpandedKeys = computed({
get: () => props.expandedKeys,
set: (value) => context.emit('update:expandedKeys', value)
})
const computedSelectionKeys = computed({
get: () => props.selectionKeys,
set: (value) => context.emit('update:selectionKeys', value)
})
let observer = null
const makeDraggable = (element) => {
if (!element._draggableCleanup) {
element._draggableCleanup = draggable({
element
})
}
}
const observeTreeChanges = (treeElement) => {
observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'childList') {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE) {
node.querySelectorAll(props.dragSelector).forEach(makeDraggable)
}
})
}
})
})
observer.observe(treeElement, { childList: true, subtree: true })
// Make existing nodes draggable
treeElement.querySelectorAll(props.dragSelector).forEach(makeDraggable)
}
onMounted(() => {
const treeElement = document.querySelector('.p-tree')
if (treeElement) {
observeTreeChanges(treeElement)
}
})
onBeforeUnmount(() => {
if (observer) {
observer.disconnect()
}
// Clean up draggable instances if necessary
const treeElement = document.querySelector('.p-tree')
if (treeElement) {
treeElement.querySelectorAll(props.dragSelector).forEach((node) => {
if (node._draggableCleanup) {
node._draggableCleanup()
}
})
}
})
return () =>
h(
Tree,
{
...context.attrs,
...props,
expandedKeys: computedExpandedKeys.value,
selectionKeys: computedSelectionKeys.value,
'onUpdate:expandedKeys': (value) =>
(computedExpandedKeys.value = value),
'onUpdate:selectionKeys': (value) =>
(computedSelectionKeys.value = value)
},
context.slots
)
}
}
</script>

View File

@@ -7,6 +7,7 @@
:icon="tab.icon"
:tooltip="tab.tooltip"
:selected="tab === selectedTab"
:class="tab.id + '-tab-button'"
@click="onTabClick(tab)"
/>
<div class="side-tool-bar-end">

View File

@@ -0,0 +1,99 @@
<template>
<TreePlus
class="node-lib-tree"
v-model:expandedKeys="expandedKeys"
selectionMode="single"
:value="renderedRoot.children"
:filter="true"
filterMode="lenient"
dragSelector=".p-tree-node-leaf"
:pt="{
nodeLabel: 'node-lib-tree-node-label',
nodeChildren: ({ props }) => ({
'data-comfy-node-name': props.node?.data?.name,
onMouseenter: (event: MouseEvent) => {
hoveredComfyNodeName = props.node?.data?.name
const hoverTarget = event.target as HTMLElement
const targetRect = hoverTarget.getBoundingClientRect()
nodePreviewStyle.top = `${targetRect.top - 40}px`
nodePreviewStyle.left = `${targetRect.right}px`
},
onMouseleave: () => {
hoveredComfyNodeName = null
}
})
}"
>
<template #folder="{ node }">
<span class="folder-label">{{ node.label }}</span>
<Badge
:value="node.totalNodes"
severity="secondary"
:style="{ marginLeft: '0.5rem' }"
></Badge>
</template>
<template #node="{ node }">
<span class="node-label">{{ node.label }}</span>
</template>
</TreePlus>
<div
v-if="hoveredComfyNode"
class="node-lib-node-preview"
:style="nodePreviewStyle"
>
<NodePreview
:key="hoveredComfyNode.name"
:nodeDef="hoveredComfyNode"
></NodePreview>
</div>
</template>
<script setup lang="ts">
import Badge from 'primevue/badge'
import { ComfyNodeDefImpl, useNodeDefStore } from '@/stores/nodeDefStore'
import { computed, ref } from 'vue'
import { TreeNode } from 'primevue/treenode'
import TreePlus from '@/components/primevueOverride/TreePlus.vue'
import NodePreview from '@/components/NodePreview.vue'
const nodeDefStore = useNodeDefStore()
const expandedKeys = ref({})
const hoveredComfyNodeName = ref<string | null>(null)
const hoveredComfyNode = computed<ComfyNodeDefImpl | null>(() => {
if (!hoveredComfyNodeName.value) {
return null
}
return nodeDefStore.nodeDefsByName[hoveredComfyNodeName.value] || null
})
const nodePreviewStyle = ref<Record<string, string>>({
position: 'absolute',
top: '0px',
left: '0px'
})
const root = computed(() => nodeDefStore.nodeTree)
const renderedRoot = computed(() => {
return fillNodeInfo(root.value)
})
const fillNodeInfo = (node: TreeNode): TreeNode => {
const isLeaf = node.children === undefined || node.children.length === 0
const isExpanded = expandedKeys.value[node.key]
const icon = isLeaf
? 'pi pi-circle-fill'
: isExpanded
? 'pi pi-folder-open'
: 'pi pi-folder'
const children = node.children?.map(fillNodeInfo)
return {
...node,
icon,
children,
type: isLeaf ? 'node' : 'folder',
totalNodes: isLeaf
? 1
: children.reduce((acc, child) => acc + child.totalNodes, 0)
}
}
</script>