Skip to content

Commit c0875d0

Browse files
authored
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
1 parent 980ed00 commit c0875d0

File tree

11 files changed

+376
-6
lines changed

11 files changed

+376
-6
lines changed

browser_tests/ComfyPage.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,42 @@ class ComfyNodeSearchBox {
3737
}
3838
}
3939

40+
class NodeLibrarySideBarTab {
41+
public readonly tabId: string = 'node-library'
42+
constructor(public readonly page: Page) {}
43+
44+
get tabButton() {
45+
return this.page.locator(`.${this.tabId}-tab-button`)
46+
}
47+
48+
get selectedTabButton() {
49+
return this.page.locator(
50+
`.${this.tabId}-tab-button.side-bar-button-selected`
51+
)
52+
}
53+
54+
get nodeLibraryTree() {
55+
return this.page.locator('.node-lib-tree')
56+
}
57+
58+
get nodePreview() {
59+
return this.page.locator('.node-lib-node-preview')
60+
}
61+
62+
async open() {
63+
if (await this.selectedTabButton.isVisible()) {
64+
return
65+
}
66+
67+
await this.tabButton.click()
68+
await this.nodeLibraryTree.waitFor({ state: 'visible' })
69+
}
70+
71+
async toggleFirstFolder() {
72+
await this.page.locator('.p-tree-node-toggle-button').nth(0).click()
73+
}
74+
}
75+
4076
class ComfyMenu {
4177
public readonly sideToolBar: Locator
4278
public readonly themeToggleButton: Locator
@@ -46,6 +82,10 @@ class ComfyMenu {
4682
this.themeToggleButton = page.locator('.comfy-vue-theme-toggle')
4783
}
4884

85+
get nodeLibraryTab() {
86+
return new NodeLibrarySideBarTab(this.page)
87+
}
88+
4989
async toggleTheme() {
5090
await this.themeToggleButton.click()
5191
await this.page.evaluate(() => {
@@ -96,6 +136,12 @@ export class ComfyPage {
96136
this.menu = new ComfyMenu(page)
97137
}
98138

139+
async getGraphNodesCount(): Promise<number> {
140+
return await this.page.evaluate(() => {
141+
return window['app']?.graph?._nodes?.length || 0
142+
})
143+
}
144+
99145
async setup() {
100146
await this.goto()
101147
// Unify font for consistent screenshots.

browser_tests/menu.spec.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,4 +67,29 @@ test.describe('Menu', () => {
6767
)
6868
expect(newChildrenCount).toBe(initialChildrenCount + 1)
6969
})
70+
71+
test('Sidebar node preview and drag to canvas', async ({ comfyPage }) => {
72+
// Open the sidebar
73+
const tab = comfyPage.menu.nodeLibraryTab
74+
await tab.open()
75+
await tab.toggleFirstFolder()
76+
77+
// Hover over a node to display the preview
78+
const nodeSelector = '.p-tree-node-leaf'
79+
await comfyPage.page.hover(nodeSelector)
80+
81+
// Verify the preview is displayed
82+
const previewVisible = await comfyPage.page.isVisible(
83+
'.node-lib-node-preview'
84+
)
85+
expect(previewVisible).toBe(true)
86+
87+
const count = await comfyPage.getGraphNodesCount()
88+
// Drag the node onto the canvas
89+
const canvasSelector = '#graph-canvas'
90+
await comfyPage.page.dragAndDrop(nodeSelector, canvasSelector)
91+
92+
// Verify the node is added to the canvas
93+
expect(await comfyPage.getGraphNodesCount()).toBe(count + 1)
94+
})
7095
})

package-lock.json

Lines changed: 25 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
"zip-dir": "^2.0.0"
4747
},
4848
"dependencies": {
49+
"@atlaskit/pragmatic-drag-and-drop": "^1.2.1",
4950
"@comfyorg/litegraph": "^0.7.29",
5051
"@primevue/themes": "^4.0.0-rc.2",
5152
"@vitejs/plugin-vue": "^5.0.5",

src/App.vue

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
</template>
1414

1515
<script setup lang="ts">
16-
import { computed, markRaw, onMounted, ref, watch } from 'vue'
16+
import { computed, markRaw, onMounted, onUnmounted, ref, watch } from 'vue'
1717
import NodeSearchboxPopover from '@/components/NodeSearchBoxPopover.vue'
1818
import SideToolBar from '@/components/sidebar/SideToolBar.vue'
1919
import LiteGraphCanvasSplitterOverlay from '@/components/LiteGraphCanvasSplitterOverlay.vue'
@@ -23,6 +23,9 @@ import { app } from './scripts/app'
2323
import { useSettingStore } from './stores/settingStore'
2424
import { useI18n } from 'vue-i18n'
2525
import { useWorkspaceStore } from './stores/workspaceStateStore'
26+
import NodeLibrarySideBarTab from './components/sidebar/tabs/NodeLibrarySideBarTab.vue'
27+
import { dropTargetForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter'
28+
import { useNodeDefStore } from './stores/nodeDefStore'
2629
2730
const isLoading = ref(true)
2831
const nodeSearchEnabled = computed<boolean>(
@@ -49,6 +52,7 @@ const betaMenuEnabled = computed(
4952
)
5053
5154
const { t } = useI18n()
55+
let dropTargetCleanup = () => {}
5256
const init = () => {
5357
useSettingStore().addSettings(app.ui.settings)
5458
app.vueAppReady = true
@@ -61,6 +65,29 @@ const init = () => {
6165
component: markRaw(QueueSideBarTab),
6266
type: 'vue'
6367
})
68+
app.extensionManager.registerSidebarTab({
69+
id: 'node-library',
70+
icon: 'pi pi-book',
71+
title: t('sideToolBar.nodeLibrary'),
72+
tooltip: t('sideToolBar.nodeLibrary'),
73+
component: markRaw(NodeLibrarySideBarTab),
74+
type: 'vue'
75+
})
76+
77+
dropTargetCleanup = dropTargetForElements({
78+
element: document.querySelector('.graph-canvas-container'),
79+
onDrop: (event) => {
80+
const loc = event.location.current.input
81+
// Add an offset on x to make sure after adding the node, the cursor
82+
// is on the node (top left corner)
83+
const pos = app.clientPosToCanvasPos([loc.clientX - 20, loc.clientY])
84+
const comfyNodeName = event.source.element.getAttribute(
85+
'data-comfy-node-name'
86+
)
87+
const nodeDef = useNodeDefStore().nodeDefsByName[comfyNodeName]
88+
app.addNodeOnGraph(nodeDef, { pos })
89+
}
90+
})
6491
}
6592
6693
onMounted(() => {
@@ -72,6 +99,10 @@ onMounted(() => {
7299
isLoading.value = false
73100
}
74101
})
102+
103+
onUnmounted(() => {
104+
dropTargetCleanup()
105+
})
75106
</script>
76107

77108
<style scoped>
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
<!-- Tree with all leaf nodes draggable -->
2+
<script>
3+
import Tree from 'primevue/tree'
4+
import { draggable } from '@atlaskit/pragmatic-drag-and-drop/element/adapter'
5+
import { h, onMounted, onBeforeUnmount, computed } from 'vue'
6+
7+
export default {
8+
name: 'TreePlus',
9+
extends: Tree,
10+
props: {
11+
dragSelector: {
12+
type: String,
13+
default: '.p-tree-node'
14+
},
15+
// Explicitly declare all v-model props
16+
expandedKeys: {
17+
type: Object,
18+
default: () => ({})
19+
},
20+
selectionKeys: {
21+
type: Object,
22+
default: () => ({})
23+
}
24+
},
25+
emits: ['update:expandedKeys', 'update:selectionKeys'],
26+
setup(props, context) {
27+
// Create computed properties for each v-model prop
28+
const computedExpandedKeys = computed({
29+
get: () => props.expandedKeys,
30+
set: (value) => context.emit('update:expandedKeys', value)
31+
})
32+
33+
const computedSelectionKeys = computed({
34+
get: () => props.selectionKeys,
35+
set: (value) => context.emit('update:selectionKeys', value)
36+
})
37+
38+
let observer = null
39+
40+
const makeDraggable = (element) => {
41+
if (!element._draggableCleanup) {
42+
element._draggableCleanup = draggable({
43+
element
44+
})
45+
}
46+
}
47+
48+
const observeTreeChanges = (treeElement) => {
49+
observer = new MutationObserver((mutations) => {
50+
mutations.forEach((mutation) => {
51+
if (mutation.type === 'childList') {
52+
mutation.addedNodes.forEach((node) => {
53+
if (node.nodeType === Node.ELEMENT_NODE) {
54+
node.querySelectorAll(props.dragSelector).forEach(makeDraggable)
55+
}
56+
})
57+
}
58+
})
59+
})
60+
61+
observer.observe(treeElement, { childList: true, subtree: true })
62+
63+
// Make existing nodes draggable
64+
treeElement.querySelectorAll(props.dragSelector).forEach(makeDraggable)
65+
}
66+
67+
onMounted(() => {
68+
const treeElement = document.querySelector('.p-tree')
69+
if (treeElement) {
70+
observeTreeChanges(treeElement)
71+
}
72+
})
73+
74+
onBeforeUnmount(() => {
75+
if (observer) {
76+
observer.disconnect()
77+
}
78+
// Clean up draggable instances if necessary
79+
const treeElement = document.querySelector('.p-tree')
80+
if (treeElement) {
81+
treeElement.querySelectorAll(props.dragSelector).forEach((node) => {
82+
if (node._draggableCleanup) {
83+
node._draggableCleanup()
84+
}
85+
})
86+
}
87+
})
88+
89+
return () =>
90+
h(
91+
Tree,
92+
{
93+
...context.attrs,
94+
...props,
95+
expandedKeys: computedExpandedKeys.value,
96+
selectionKeys: computedSelectionKeys.value,
97+
'onUpdate:expandedKeys': (value) =>
98+
(computedExpandedKeys.value = value),
99+
'onUpdate:selectionKeys': (value) =>
100+
(computedSelectionKeys.value = value)
101+
},
102+
context.slots
103+
)
104+
}
105+
}
106+
</script>

src/components/sidebar/SideToolBar.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
:icon="tab.icon"
88
:tooltip="tab.tooltip"
99
:selected="tab === selectedTab"
10+
:class="tab.id + '-tab-button'"
1011
@click="onTabClick(tab)"
1112
/>
1213
<div class="side-tool-bar-end">

0 commit comments

Comments
 (0)