mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-05 15:40:10 +00:00
[Major Refactor] Use TreeExplorer on nodeLibrarySidebarTab (#699)
* Basic move * Add back node bookmark * Move node preview * Fix drag node to canvas * Restore click node to add to canvas * Split bookmark tree and library tree * Migrate rename and delete context menu * Fix expanded keys * Split components * Support extra menu items * Context menu only for folder * Migrate add folder * Handle drop * Store color customization * remove extra padding * Do not show context menu if no item * Hide divider if no bookmark * Sort bookmarks alphabetically default * nit * proper edit * Update test selectors * Auto expand on item drop * nit * Fix tests * Search also searches bookmarks tree * Add serach playwright test
This commit is contained in:
@@ -57,8 +57,12 @@ class NodeLibrarySidebarTab {
|
||||
)
|
||||
}
|
||||
|
||||
get nodeLibrarySearchBoxInput() {
|
||||
return this.page.locator('.node-lib-search-box input[type="text"]')
|
||||
}
|
||||
|
||||
get nodeLibraryTree() {
|
||||
return this.page.locator('.node-lib-tree')
|
||||
return this.page.locator('.node-lib-tree-explorer')
|
||||
}
|
||||
|
||||
get nodePreview() {
|
||||
@@ -83,7 +87,7 @@ class NodeLibrarySidebarTab {
|
||||
}
|
||||
|
||||
folderSelector(folderName: string) {
|
||||
return `.p-tree-node-content:has(> .node-lib-tree-node-label:has(.folder-label:has-text("${folderName}")))`
|
||||
return `.p-tree-node-content:has(> .tree-explorer-node-label:has(.tree-folder .node-label:has-text("${folderName}")))`
|
||||
}
|
||||
|
||||
getFolder(folderName: string) {
|
||||
@@ -91,7 +95,7 @@ class NodeLibrarySidebarTab {
|
||||
}
|
||||
|
||||
nodeSelector(nodeName: string) {
|
||||
return `.p-tree-node-content:has(> .node-lib-tree-node-label:has(.node-label:has-text("${nodeName}")))`
|
||||
return `.p-tree-node-content:has(> .tree-explorer-node-label:has(.tree-leaf .node-label:has-text("${nodeName}")))`
|
||||
}
|
||||
|
||||
getNode(nodeName: string) {
|
||||
|
||||
@@ -109,7 +109,7 @@ test.describe('Menu', () => {
|
||||
expect(await tab.getNode('KSampler (Advanced)').count()).toBe(2)
|
||||
|
||||
// Hover on the bookmark node to display the preview
|
||||
await comfyPage.page.hover('.node-tree-leaf.bookmark')
|
||||
await comfyPage.page.hover('.node-lib-bookmark-tree-explorer .tree-leaf')
|
||||
expect(await comfyPage.page.isVisible('.node-lib-node-preview')).toBe(
|
||||
true
|
||||
)
|
||||
@@ -145,10 +145,12 @@ test.describe('Menu', () => {
|
||||
|
||||
await tab.getFolder('foo').click({ button: 'right' })
|
||||
await comfyPage.page.getByLabel('New Folder').click()
|
||||
await comfyPage.page.keyboard.type('bar')
|
||||
await comfyPage.page.keyboard.press('Enter')
|
||||
|
||||
expect(await tab.getFolder('bar').count()).toBe(1)
|
||||
expect(await comfyPage.getSetting('Comfy.NodeLibrary.Bookmarks')).toEqual(
|
||||
['foo/', 'foo/New Folder/']
|
||||
['foo/', 'foo/bar/']
|
||||
)
|
||||
})
|
||||
|
||||
@@ -325,6 +327,20 @@ test.describe('Menu', () => {
|
||||
await comfyPage.getSetting('Comfy.NodeLibrary.BookmarksCustomization')
|
||||
).toEqual({})
|
||||
})
|
||||
|
||||
test('Can filter nodes in both trees', async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks', [
|
||||
'foo/',
|
||||
'foo/KSampler (Advanced)',
|
||||
'KSampler'
|
||||
])
|
||||
|
||||
const tab = comfyPage.menu.nodeLibraryTab
|
||||
await tab.nodeLibrarySearchBoxInput.fill('KSampler')
|
||||
// Node search box is debounced and may take some time to update.
|
||||
await comfyPage.page.waitForTimeout(1000)
|
||||
expect(await tab.getNode('KSampler (Advanced)').count()).toBe(2)
|
||||
})
|
||||
})
|
||||
|
||||
test('Can change canvas zoom speed setting', async ({ comfyPage }) => {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
:class="props.class"
|
||||
v-model:expandedKeys="expandedKeys"
|
||||
:value="renderedRoots"
|
||||
selectionMode="single"
|
||||
:pt="{
|
||||
nodeLabel: 'tree-explorer-node-label',
|
||||
nodeContent: ({ props }) => ({
|
||||
@@ -40,25 +41,31 @@ import type {
|
||||
TreeExplorerNode
|
||||
} from '@/types/treeExplorerTypes'
|
||||
import type { MenuItem } from 'primevue/menuitem'
|
||||
import { useTreeExpansion } from '@/hooks/treeHooks'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const expandedKeys = defineModel<Record<string, boolean>>('expandedKeys')
|
||||
provide('expandedKeys', expandedKeys)
|
||||
const props = defineProps<{
|
||||
roots: TreeExplorerNode[]
|
||||
class?: string
|
||||
extraMenuItems?: MenuItem[]
|
||||
extraMenuItems?:
|
||||
| MenuItem[]
|
||||
| ((targetNode: RenderedTreeExplorerNode) => MenuItem[])
|
||||
}>()
|
||||
const emit = defineEmits<{
|
||||
(e: 'nodeClick', node: RenderedTreeExplorerNode): void
|
||||
(e: 'nodeClick', node: RenderedTreeExplorerNode, event: MouseEvent): void
|
||||
(e: 'nodeDelete', node: RenderedTreeExplorerNode): void
|
||||
(e: 'contextMenu', node: RenderedTreeExplorerNode, event: MouseEvent): void
|
||||
}>()
|
||||
const { expandedKeys, toggleNodeOnEvent } = useTreeExpansion()
|
||||
const renderedRoots = computed<RenderedTreeExplorerNode[]>(() => {
|
||||
return props.roots.map(fillNodeInfo)
|
||||
})
|
||||
const getTreeNodeIcon = (node: TreeExplorerNode) => {
|
||||
if (node.getIcon) {
|
||||
return node.getIcon(node)
|
||||
const icon = node.getIcon(node)
|
||||
if (icon) {
|
||||
return icon
|
||||
}
|
||||
} else if (node.icon) {
|
||||
return node.icon
|
||||
}
|
||||
@@ -82,46 +89,57 @@ const fillNodeInfo = (node: TreeExplorerNode): RenderedTreeExplorerNode => {
|
||||
}
|
||||
}
|
||||
const onNodeContentClick = (e: MouseEvent, node: RenderedTreeExplorerNode) => {
|
||||
if (!node.key) return
|
||||
if (node.type === 'folder') {
|
||||
toggleNodeOnEvent(e, node)
|
||||
}
|
||||
emit('nodeClick', node)
|
||||
emit('nodeClick', node, e)
|
||||
}
|
||||
const menu = ref(null)
|
||||
const menuTargetNode = ref<RenderedTreeExplorerNode | null>(null)
|
||||
provide('menuTargetNode', menuTargetNode)
|
||||
const renameEditingNode = ref<RenderedTreeExplorerNode | null>(null)
|
||||
provide('renameEditingNode', renameEditingNode)
|
||||
|
||||
const { t } = useI18n()
|
||||
const renameCommand = (node: RenderedTreeExplorerNode) => {
|
||||
renameEditingNode.value = node
|
||||
}
|
||||
const deleteCommand = (node: RenderedTreeExplorerNode) => {
|
||||
node.handleDelete?.(node)
|
||||
emit('nodeDelete', node)
|
||||
}
|
||||
const menuItems = computed<MenuItem[]>(() => [
|
||||
{
|
||||
label: 'Rename',
|
||||
label: t('rename'),
|
||||
icon: 'pi pi-file-edit',
|
||||
command: () => {
|
||||
renameEditingNode.value = menuTargetNode.value
|
||||
},
|
||||
command: () => renameCommand(menuTargetNode.value),
|
||||
visible: menuTargetNode.value?.handleRename !== undefined
|
||||
},
|
||||
{
|
||||
label: 'Delete',
|
||||
label: t('delete'),
|
||||
icon: 'pi pi-trash',
|
||||
command: () => {
|
||||
menuTargetNode.value?.handleDelete?.(menuTargetNode.value)
|
||||
emit('nodeDelete', menuTargetNode.value)
|
||||
},
|
||||
command: () => deleteCommand(menuTargetNode.value),
|
||||
visible: menuTargetNode.value?.handleDelete !== undefined
|
||||
},
|
||||
...(props.extraMenuItems || [])
|
||||
...(props.extraMenuItems
|
||||
? typeof props.extraMenuItems === 'function'
|
||||
? props.extraMenuItems(menuTargetNode.value)
|
||||
: props.extraMenuItems
|
||||
: [])
|
||||
])
|
||||
const handleContextMenu = (node: RenderedTreeExplorerNode, e: MouseEvent) => {
|
||||
menuTargetNode.value = node
|
||||
emit('contextMenu', node, e)
|
||||
menu.value?.show(e)
|
||||
if (menuItems.value.filter((item) => item.visible).length > 0) {
|
||||
menu.value?.show(e)
|
||||
}
|
||||
}
|
||||
defineExpose({
|
||||
renameCommand,
|
||||
deleteCommand
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.tree-explorer-node-label {
|
||||
:deep(.tree-explorer-node-label) {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: var(--p-tree-node-gap);
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<EditableText
|
||||
:modelValue="node.label"
|
||||
:isEditing="isEditing"
|
||||
@edit="(newName: string) => props.node.handleRename(node, newName)"
|
||||
@edit="handleRename"
|
||||
/>
|
||||
<slot name="after-label" :node="props.node"></slot>
|
||||
</span>
|
||||
@@ -26,9 +26,9 @@
|
||||
severity="secondary"
|
||||
class="leaf-count-badge"
|
||||
/>
|
||||
<slot name="actions" :node="node">
|
||||
<!-- Default slot content for actions -->
|
||||
</slot>
|
||||
</div>
|
||||
<div class="node-actions">
|
||||
<slot name="actions" :node="props.node"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -62,13 +62,15 @@ const emit = defineEmits<{
|
||||
}>()
|
||||
|
||||
const labelEditable = computed<boolean>(() => !!props.node.handleRename)
|
||||
const renameEditingNode = inject(
|
||||
'renameEditingNode'
|
||||
) as Ref<TreeExplorerNode | null>
|
||||
const renameEditingNode =
|
||||
inject<Ref<TreeExplorerNode | null>>('renameEditingNode')
|
||||
const isEditing = computed(
|
||||
() => labelEditable.value && renameEditingNode.value?.key === props.node.key
|
||||
)
|
||||
|
||||
const handleRename = (newName: string) => {
|
||||
props.node.handleRename(props.node, newName)
|
||||
renameEditingNode.value = null
|
||||
}
|
||||
const container = ref<HTMLElement | null>(null)
|
||||
const canDrop = ref(false)
|
||||
const treeNodeElement = ref<HTMLElement | null>(null)
|
||||
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
LGraphCanvas,
|
||||
ContextMenu
|
||||
} from '@comfyorg/litegraph'
|
||||
import type { RenderedTreeExplorerNode } from '@/types/treeExplorerTypes'
|
||||
|
||||
const emit = defineEmits(['ready'])
|
||||
const canvasRef = ref<HTMLCanvasElement | null>(null)
|
||||
@@ -129,15 +130,18 @@ onMounted(async () => {
|
||||
const loc = event.location.current.input
|
||||
const dndData = event.source.data
|
||||
|
||||
if (dndData.type === 'add-node') {
|
||||
const nodeDef = dndData.data as ComfyNodeDefImpl
|
||||
// Add an offset on x to make sure after adding the node, the cursor
|
||||
// is on the node (top left corner)
|
||||
const pos = comfyApp.clientPosToCanvasPos([
|
||||
loc.clientX - 20,
|
||||
loc.clientY
|
||||
])
|
||||
comfyApp.addNodeOnGraph(nodeDef, { pos })
|
||||
if (dndData.type === 'tree-explorer-node') {
|
||||
const node = dndData.data as RenderedTreeExplorerNode
|
||||
if (node.data instanceof ComfyNodeDefImpl) {
|
||||
const nodeDef = node.data
|
||||
// Add an offset on x to make sure after adding the node, the cursor
|
||||
// is on the node (top left corner)
|
||||
const pos = comfyApp.clientPosToCanvasPos([
|
||||
loc.clientX - 20,
|
||||
loc.clientY
|
||||
])
|
||||
comfyApp.addNodeOnGraph(nodeDef, { pos })
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
icon="pi pi-folder-plus"
|
||||
text
|
||||
severity="secondary"
|
||||
@click="addNewBookmarkFolder()"
|
||||
@click="nodeBookmarkTreeExplorerRef?.addNewBookmarkFolder()"
|
||||
v-tooltip="$t('newFolder')"
|
||||
/>
|
||||
<Button
|
||||
@@ -33,237 +33,110 @@
|
||||
<Popover ref="searchFilter" class="node-lib-filter-popup">
|
||||
<NodeSearchFilter @addFilter="onAddFilter" />
|
||||
</Popover>
|
||||
|
||||
<Tree
|
||||
class="node-lib-tree"
|
||||
<NodeBookmarkTreeExplorer
|
||||
ref="nodeBookmarkTreeExplorerRef"
|
||||
:filtered-node-defs="filteredNodeDefs"
|
||||
/>
|
||||
<Divider v-if="nodeBookmarkStore.bookmarks.length > 0" />
|
||||
<TreeExplorer
|
||||
class="node-lib-tree-explorer"
|
||||
:roots="renderedRoot.children"
|
||||
v-model:expandedKeys="expandedKeys"
|
||||
selectionMode="single"
|
||||
:value="renderedRoot.children"
|
||||
:pt="{
|
||||
nodeLabel: 'node-lib-tree-node-label',
|
||||
nodeContent: ({ props }) => ({
|
||||
onClick: (e: MouseEvent) => onNodeContentClick(e, props.node),
|
||||
onMouseenter: (event: MouseEvent) =>
|
||||
handleNodeHover(event, props.node?.data?.name),
|
||||
onMouseleave: () => {
|
||||
hoveredComfyNodeName = null
|
||||
},
|
||||
onContextmenu: (e: MouseEvent) => handleContextMenu(props.node, e)
|
||||
}),
|
||||
nodeToggleButton: () => ({
|
||||
onClick: (e: MouseEvent) => {
|
||||
// Prevent toggle action as the node controls it
|
||||
e.stopImmediatePropagation()
|
||||
}
|
||||
})
|
||||
}"
|
||||
@nodeClick="handleNodeClick"
|
||||
>
|
||||
<template #folder="{ node }">
|
||||
<NodeTreeFolder
|
||||
:node="node"
|
||||
:isBookmarkFolder="!!node.data && node.data.isDummyFolder"
|
||||
@itemDropped="handleItemDropped"
|
||||
>
|
||||
<template #folder-label="{ node }">
|
||||
<EditableText
|
||||
:modelValue="node.label"
|
||||
:isEditing="renameEditingNode?.key === node.key"
|
||||
@edit="(newName: string) => handleRename(node, newName)"
|
||||
/>
|
||||
</template>
|
||||
</NodeTreeFolder>
|
||||
</template>
|
||||
<template #node="{ node }">
|
||||
<NodeTreeLeaf
|
||||
:node="node.data"
|
||||
:isBookmarked="nodeBookmarkStore.isBookmarked(node.data)"
|
||||
@toggleBookmark="nodeBookmarkStore.toggleBookmark(node.data)"
|
||||
/>
|
||||
<NodeTreeLeaf :node="node" />
|
||||
</template>
|
||||
</Tree>
|
||||
<div
|
||||
v-if="hoveredComfyNode"
|
||||
class="node-lib-node-preview"
|
||||
:style="nodePreviewStyle"
|
||||
>
|
||||
<NodePreview
|
||||
ref="previewRef"
|
||||
:key="hoveredComfyNode.name"
|
||||
:nodeDef="hoveredComfyNode"
|
||||
></NodePreview>
|
||||
</div>
|
||||
</TreeExplorer>
|
||||
</template>
|
||||
</SidebarTabTemplate>
|
||||
<ContextMenu ref="menu" :model="menuItems" />
|
||||
<FolderCustomizationDialog
|
||||
v-model="showCustomizationDialog"
|
||||
@confirm="updateCustomization"
|
||||
:initialIcon="initialIcon"
|
||||
:initialColor="initialColor"
|
||||
/>
|
||||
<div id="node-library-node-preview-container" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import { useNodeBookmarkStore } from '@/stores/nodeBookmarkStore'
|
||||
import {
|
||||
buildNodeDefTree,
|
||||
ComfyNodeDefImpl,
|
||||
useNodeDefStore
|
||||
} from '@/stores/nodeDefStore'
|
||||
import { computed, ref, nextTick, Ref } from 'vue'
|
||||
import { computed, nextTick, ref, Ref } from 'vue'
|
||||
import type { TreeNode } from 'primevue/treenode'
|
||||
import Popover from 'primevue/popover'
|
||||
import NodeTreeLeaf from './nodeLibrary/NodeTreeLeaf.vue'
|
||||
import NodeTreeFolder from './nodeLibrary/NodeTreeFolder.vue'
|
||||
import Tree from 'primevue/tree'
|
||||
import ContextMenu from 'primevue/contextmenu'
|
||||
import EditableText from '@/components/common/EditableText.vue'
|
||||
import NodePreview from '@/components/node/NodePreview.vue'
|
||||
import Divider from 'primevue/divider'
|
||||
import SearchBox from '@/components/common/SearchBox.vue'
|
||||
import FolderCustomizationDialog from '@/components/common/CustomizationDialog.vue'
|
||||
import SidebarTabTemplate from '@/components/sidebar/tabs/SidebarTabTemplate.vue'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import NodeBookmarkTreeExplorer from './nodeLibrary/NodeBookmarkTreeExplorer.vue'
|
||||
import TreeExplorer from '@/components/common/TreeExplorer.vue'
|
||||
import NodeTreeLeaf from '@/components/sidebar/tabs/nodeLibrary/NodeTreeLeaf.vue'
|
||||
import { app } from '@/scripts/app'
|
||||
import { findNodeByKey, sortedTree } from '@/utils/treeUtil'
|
||||
import { sortedTree } from '@/utils/treeUtil'
|
||||
import { useTreeExpansion } from '@/hooks/treeHooks'
|
||||
import type { MenuItem } from 'primevue/menuitem'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
import NodeSearchFilter from '@/components/searchbox/NodeSearchFilter.vue'
|
||||
import { FilterAndValue } from '@/services/nodeSearchService'
|
||||
import { SearchFilter } from '@/components/common/SearchFilterChip.vue'
|
||||
import type {
|
||||
RenderedTreeExplorerNode,
|
||||
TreeExplorerNode
|
||||
} from '@/types/treeExplorerTypes'
|
||||
import { useNodeBookmarkStore } from '@/stores/nodeBookmarkStore'
|
||||
|
||||
const { t } = useI18n()
|
||||
const toast = useToast()
|
||||
const nodeDefStore = useNodeDefStore()
|
||||
const nodeBookmarkStore = useNodeBookmarkStore()
|
||||
const { expandedKeys, expandNode, toggleNodeOnEvent } = useTreeExpansion()
|
||||
|
||||
const nodeBookmarkTreeExplorerRef = ref<InstanceType<
|
||||
typeof NodeBookmarkTreeExplorer
|
||||
> | null>(null)
|
||||
const searchFilter = ref(null)
|
||||
const alphabeticalSort = ref(false)
|
||||
const hoveredComfyNodeName = ref<string | null>(null)
|
||||
const hoveredComfyNode = computed<ComfyNodeDefImpl | null>(() => {
|
||||
if (!hoveredComfyNodeName.value) {
|
||||
return null
|
||||
}
|
||||
return nodeDefStore.nodeDefsByName[hoveredComfyNodeName.value] || null
|
||||
})
|
||||
const previewRef = ref<InstanceType<typeof NodePreview> | null>(null)
|
||||
|
||||
const searchQuery = ref<string>('')
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
const sidebarLocation = computed<'left' | 'right'>(() =>
|
||||
settingStore.get('Comfy.Sidebar.Location')
|
||||
)
|
||||
|
||||
const nodePreviewStyle = ref<Record<string, string>>({
|
||||
position: 'absolute',
|
||||
top: '0px',
|
||||
left: '0px'
|
||||
})
|
||||
|
||||
const nodeBookmarkStore = useNodeBookmarkStore()
|
||||
|
||||
const allNodesRoot = computed<TreeNode>(() => {
|
||||
return {
|
||||
key: 'all-nodes',
|
||||
label: 'All Nodes',
|
||||
children: [
|
||||
...(nodeBookmarkStore.bookmarkedRoot.children ?? []),
|
||||
...nodeDefStore.nodeTree.children
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
const root = computed(() => {
|
||||
const root = filteredRoot.value || allNodesRoot.value
|
||||
const root = filteredRoot.value || nodeDefStore.nodeTree
|
||||
return alphabeticalSort.value ? sortedTree(root) : root
|
||||
})
|
||||
|
||||
const renderedRoot = computed(() => {
|
||||
const renderedRoot = computed<TreeExplorerNode<ComfyNodeDefImpl>>(() => {
|
||||
const fillNodeInfo = (node: TreeNode): TreeExplorerNode<ComfyNodeDefImpl> => {
|
||||
const children = node.children?.map(fillNodeInfo)
|
||||
|
||||
return {
|
||||
key: node.key,
|
||||
label: node.label,
|
||||
leaf: node.leaf,
|
||||
data: node.data,
|
||||
getIcon: (node: TreeExplorerNode<ComfyNodeDefImpl>) => {
|
||||
if (node.leaf) {
|
||||
return 'pi pi-circle-fill'
|
||||
}
|
||||
},
|
||||
children,
|
||||
draggable: node.leaf
|
||||
}
|
||||
}
|
||||
return fillNodeInfo(root.value)
|
||||
})
|
||||
|
||||
const getTreeNodeIcon = (node: TreeNode) => {
|
||||
if (node.leaf) {
|
||||
return 'pi pi-circle-fill'
|
||||
const filteredNodeDefs = ref<ComfyNodeDefImpl[]>([])
|
||||
const filteredRoot = computed<TreeNode | null>(() => {
|
||||
if (!filteredNodeDefs.value.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
// If the node is a bookmark folder, show a bookmark icon
|
||||
if (node.data && node.data.isDummyFolder) {
|
||||
const customization =
|
||||
nodeBookmarkStore.bookmarksCustomization[node.data.nodePath]
|
||||
if (customization?.icon) {
|
||||
return 'pi ' + customization.icon
|
||||
}
|
||||
return 'pi pi-bookmark-fill'
|
||||
}
|
||||
|
||||
const isExpanded = expandedKeys.value[node.key]
|
||||
return isExpanded ? 'pi pi-folder-open' : 'pi pi-folder'
|
||||
}
|
||||
|
||||
const fillNodeInfo = (node: TreeNode): TreeNode => {
|
||||
const children = node.children?.map(fillNodeInfo)
|
||||
|
||||
return {
|
||||
...node,
|
||||
icon: getTreeNodeIcon(node),
|
||||
children,
|
||||
type: node.leaf ? 'node' : 'folder',
|
||||
totalNodes: node.leaf
|
||||
? 1
|
||||
: children.reduce((acc, child) => acc + child.totalNodes, 0)
|
||||
}
|
||||
}
|
||||
|
||||
const handleNodeHover = async (
|
||||
event: MouseEvent,
|
||||
nodeName: string | undefined
|
||||
) => {
|
||||
hoveredComfyNodeName.value = nodeName || null
|
||||
|
||||
if (!nodeName) return
|
||||
|
||||
const hoverTarget = event.target as HTMLElement
|
||||
const targetRect = hoverTarget.getBoundingClientRect()
|
||||
|
||||
await nextTick()
|
||||
|
||||
const previewHeight = previewRef.value?.$el.offsetHeight || 0
|
||||
const availableSpaceBelow = window.innerHeight - targetRect.bottom
|
||||
|
||||
nodePreviewStyle.value.top =
|
||||
previewHeight > availableSpaceBelow
|
||||
? `${Math.max(0, targetRect.top - (previewHeight - availableSpaceBelow) - 20)}px`
|
||||
: `${targetRect.top - 40}px`
|
||||
if (sidebarLocation.value === 'left') {
|
||||
nodePreviewStyle.value.left = `${targetRect.right}px`
|
||||
} else {
|
||||
nodePreviewStyle.value.left = `${targetRect.left - 400}px`
|
||||
}
|
||||
}
|
||||
|
||||
const handleItemDropped = (node: TreeNode) => {
|
||||
expandedKeys.value[node.key] = true
|
||||
}
|
||||
|
||||
const insertNode = (nodeDef: ComfyNodeDefImpl) => {
|
||||
app.addNodeOnGraph(nodeDef, { pos: app.getCanvasCenter() })
|
||||
}
|
||||
|
||||
const filteredRoot = ref<TreeNode | null>(null)
|
||||
return buildNodeDefTree(filteredNodeDefs.value)
|
||||
})
|
||||
const filters: Ref<Array<SearchFilter & { filter: FilterAndValue<string> }>> =
|
||||
ref([])
|
||||
const handleSearch = (query: string) => {
|
||||
if (query.length < 3 && !filters.value.length) {
|
||||
filteredRoot.value = null
|
||||
filteredNodeDefs.value = []
|
||||
expandedKeys.value = {}
|
||||
return
|
||||
}
|
||||
|
||||
const f = filters.value.map((f) => f.filter as FilterAndValue<string>)
|
||||
const matchedNodes = nodeDefStore.nodeSearchService.searchNode(
|
||||
filteredNodeDefs.value = nodeDefStore.nodeSearchService.searchNode(
|
||||
query,
|
||||
f,
|
||||
{
|
||||
@@ -274,106 +147,19 @@ const handleSearch = (query: string) => {
|
||||
}
|
||||
)
|
||||
|
||||
filteredRoot.value = buildNodeDefTree(matchedNodes)
|
||||
expandNode(filteredRoot.value)
|
||||
}
|
||||
|
||||
const onNodeContentClick = (e: MouseEvent, node: TreeNode) => {
|
||||
if (!node.key) return
|
||||
if (node.type === 'folder') {
|
||||
toggleNodeOnEvent(e, node)
|
||||
} else {
|
||||
insertNode(node.data)
|
||||
}
|
||||
}
|
||||
|
||||
const menu = ref(null)
|
||||
const menuTargetNode = ref<TreeNode | null>(null)
|
||||
const renameEditingNode = ref<TreeNode | null>(null)
|
||||
const menuItems = computed<MenuItem[]>(() => [
|
||||
{
|
||||
label: t('newFolder'),
|
||||
icon: 'pi pi-folder-plus',
|
||||
command: () => {
|
||||
if (menuTargetNode.value?.data) {
|
||||
addNewBookmarkFolder(menuTargetNode.value?.data)
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
label: t('delete'),
|
||||
icon: 'pi pi-trash',
|
||||
command: () => {
|
||||
if (menuTargetNode.value?.data) {
|
||||
nodeBookmarkStore.deleteBookmarkFolder(menuTargetNode.value.data)
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
label: t('rename'),
|
||||
icon: 'pi pi-file-edit',
|
||||
command: () => {
|
||||
renameEditingNode.value = menuTargetNode.value
|
||||
}
|
||||
},
|
||||
{
|
||||
label: t('customize'),
|
||||
icon: 'pi pi-palette',
|
||||
command: () => {
|
||||
initialIcon.value =
|
||||
nodeBookmarkStore.bookmarksCustomization[
|
||||
menuTargetNode.value.data.nodePath
|
||||
]?.icon || nodeBookmarkStore.defaultBookmarkIcon
|
||||
initialColor.value =
|
||||
nodeBookmarkStore.bookmarksCustomization[
|
||||
menuTargetNode.value.data.nodePath
|
||||
]?.color || nodeBookmarkStore.defaultBookmarkColor
|
||||
showCustomizationDialog.value = true
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
const handleContextMenu = (node: TreeNode, e: MouseEvent) => {
|
||||
const nodeDef = node.data as ComfyNodeDefImpl
|
||||
if (nodeDef?.isDummyFolder) {
|
||||
menuTargetNode.value = node
|
||||
menu.value?.show(e)
|
||||
}
|
||||
}
|
||||
|
||||
const handleRename = (node: TreeNode, newName: string) => {
|
||||
if (node.data && node.data.isDummyFolder) {
|
||||
try {
|
||||
nodeBookmarkStore.renameBookmarkFolder(node.data, newName)
|
||||
} catch (e) {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('error'),
|
||||
detail: e.message,
|
||||
life: 3000
|
||||
})
|
||||
}
|
||||
}
|
||||
renameEditingNode.value = null
|
||||
}
|
||||
|
||||
const addNewBookmarkFolder = (parent?: ComfyNodeDefImpl) => {
|
||||
const newFolderKey =
|
||||
'root/' + nodeBookmarkStore.addNewBookmarkFolder(parent).slice(0, -1)
|
||||
nextTick(() => {
|
||||
renameEditingNode.value = findNodeByKey(renderedRoot.value, newFolderKey)
|
||||
expandNode(filteredRoot.value)
|
||||
})
|
||||
}
|
||||
|
||||
const showCustomizationDialog = ref(false)
|
||||
const initialIcon = ref(nodeBookmarkStore.defaultBookmarkIcon)
|
||||
const initialColor = ref(nodeBookmarkStore.defaultBookmarkColor)
|
||||
const updateCustomization = (icon: string, color: string) => {
|
||||
if (menuTargetNode.value?.data) {
|
||||
nodeBookmarkStore.updateBookmarkCustomization(
|
||||
menuTargetNode.value.data.nodePath,
|
||||
{ icon, color }
|
||||
)
|
||||
const handleNodeClick = (
|
||||
node: RenderedTreeExplorerNode<ComfyNodeDefImpl>,
|
||||
e: MouseEvent
|
||||
) => {
|
||||
if (node.leaf) {
|
||||
app.addNodeOnGraph(node.data, { pos: app.getCanvasCenter() })
|
||||
} else {
|
||||
toggleNodeOnEvent(e, node)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -399,13 +185,6 @@ const onRemoveFilter = (filterAndValue) => {
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.node-lib-tree-node-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: var(--p-tree-node-gap);
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.node-lib-filter-popup {
|
||||
margin-left: -13px;
|
||||
}
|
||||
@@ -420,23 +199,11 @@ const onRemoveFilter = (filterAndValue) => {
|
||||
background: var(--p-tree-background);
|
||||
}
|
||||
|
||||
/*
|
||||
* The following styles are necessary to avoid layout shift when dragging nodes over folders.
|
||||
* By setting the position to relative on the parent and using an absolutely positioned pseudo-element,
|
||||
* we can create a visual indicator for the drop target without affecting the layout of other elements.
|
||||
*/
|
||||
:deep(.p-tree-node-content:has(.node-tree-folder)) {
|
||||
position: relative;
|
||||
:deep(.node-lib-bookmark-tree-explorer) {
|
||||
padding-bottom: 0px;
|
||||
}
|
||||
|
||||
:deep(.p-tree-node-content:has(.node-tree-folder.can-drop))::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
border: 1px solid var(--p-content-color);
|
||||
pointer-events: none;
|
||||
:deep(.node-lib-tree-explorer) {
|
||||
padding-top: 0px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,246 @@
|
||||
<template>
|
||||
<TreeExplorer
|
||||
class="node-lib-bookmark-tree-explorer"
|
||||
ref="treeExplorerRef"
|
||||
:roots="renderedBookmarkedRoot.children"
|
||||
:expandedKeys="expandedKeys"
|
||||
:extraMenuItems="extraMenuItems"
|
||||
@nodeClick="handleNodeClick"
|
||||
>
|
||||
<template #folder="{ node }">
|
||||
<NodeTreeFolder :node="node" />
|
||||
</template>
|
||||
<template #node="{ node }">
|
||||
<NodeTreeLeaf :node="node" />
|
||||
</template>
|
||||
</TreeExplorer>
|
||||
|
||||
<FolderCustomizationDialog
|
||||
v-model="showCustomizationDialog"
|
||||
@confirm="updateCustomization"
|
||||
:initialIcon="initialIcon"
|
||||
:initialColor="initialColor"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import TreeExplorer from '@/components/common/TreeExplorer.vue'
|
||||
import NodeTreeLeaf from '@/components/sidebar/tabs/nodeLibrary/NodeTreeLeaf.vue'
|
||||
import NodeTreeFolder from '@/components/sidebar/tabs/nodeLibrary/NodeTreeFolder.vue'
|
||||
import FolderCustomizationDialog from '@/components/common/CustomizationDialog.vue'
|
||||
import { useNodeBookmarkStore } from '@/stores/nodeBookmarkStore'
|
||||
import { ComfyNodeDefImpl } from '@/stores/nodeDefStore'
|
||||
import type {
|
||||
RenderedTreeExplorerNode,
|
||||
TreeExplorerDragAndDropData,
|
||||
TreeExplorerNode
|
||||
} from '@/types/treeExplorerTypes'
|
||||
import type { TreeNode } from 'primevue/treenode'
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
import { computed, nextTick, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useTreeExpansion } from '@/hooks/treeHooks'
|
||||
import { app } from '@/scripts/app'
|
||||
import { findNodeByKey } from '@/utils/treeUtil'
|
||||
|
||||
const props = defineProps<{
|
||||
filteredNodeDefs: ComfyNodeDefImpl[]
|
||||
}>()
|
||||
|
||||
const { expandedKeys, expandNode, toggleNodeOnEvent } = useTreeExpansion()
|
||||
|
||||
const handleNodeClick = (
|
||||
node: RenderedTreeExplorerNode<ComfyNodeDefImpl>,
|
||||
e: MouseEvent
|
||||
) => {
|
||||
if (node.leaf) {
|
||||
app.addNodeOnGraph(node.data, { pos: app.getCanvasCenter() })
|
||||
} else {
|
||||
toggleNodeOnEvent(e, node)
|
||||
}
|
||||
}
|
||||
|
||||
const nodeBookmarkStore = useNodeBookmarkStore()
|
||||
const bookmarkedRoot = computed<TreeNode>(() => {
|
||||
const filterTree = (node: TreeNode): TreeNode | null => {
|
||||
if (node.leaf) {
|
||||
// Check if the node's display_name is in the filteredNodeDefs list
|
||||
return props.filteredNodeDefs.some(
|
||||
(def) => def.display_name === node.data.display_name
|
||||
)
|
||||
? node
|
||||
: null
|
||||
}
|
||||
|
||||
const filteredChildren = node.children
|
||||
?.map(filterTree)
|
||||
.filter((child): child is TreeNode => child !== null)
|
||||
|
||||
if (filteredChildren && filteredChildren.length > 0) {
|
||||
return {
|
||||
...node,
|
||||
children: filteredChildren
|
||||
}
|
||||
}
|
||||
|
||||
return null // Remove empty folders
|
||||
}
|
||||
|
||||
return props.filteredNodeDefs.length
|
||||
? filterTree(nodeBookmarkStore.bookmarkedRoot) || {
|
||||
key: 'root',
|
||||
label: 'Root',
|
||||
children: []
|
||||
}
|
||||
: nodeBookmarkStore.bookmarkedRoot
|
||||
})
|
||||
watch(
|
||||
() => props.filteredNodeDefs,
|
||||
(newValue) => {
|
||||
if (newValue.length) {
|
||||
nextTick(() => expandNode(bookmarkedRoot.value))
|
||||
}
|
||||
}
|
||||
)
|
||||
const renderedBookmarkedRoot = computed<TreeExplorerNode<ComfyNodeDefImpl>>(
|
||||
() => {
|
||||
const fillNodeInfo = (
|
||||
node: TreeNode
|
||||
): TreeExplorerNode<ComfyNodeDefImpl> => {
|
||||
const children = node.children?.map(fillNodeInfo)
|
||||
|
||||
// Sort children: non-leaf nodes first, then leaf nodes, both alphabetically
|
||||
const sortedChildren = children?.sort((a, b) => {
|
||||
if (a.leaf === b.leaf) {
|
||||
return a.label.localeCompare(b.label)
|
||||
}
|
||||
return a.leaf ? 1 : -1
|
||||
})
|
||||
|
||||
return {
|
||||
key: node.key,
|
||||
label: node.label,
|
||||
leaf: node.leaf,
|
||||
data: node.data,
|
||||
getIcon: (node: TreeExplorerNode<ComfyNodeDefImpl>) => {
|
||||
if (node.leaf) {
|
||||
return 'pi pi-circle-fill'
|
||||
}
|
||||
const customization =
|
||||
nodeBookmarkStore.bookmarksCustomization[node.data.nodePath]
|
||||
return customization?.icon
|
||||
? 'pi ' + customization.icon
|
||||
: 'pi pi-bookmark-fill'
|
||||
},
|
||||
children: sortedChildren,
|
||||
draggable: node.leaf,
|
||||
droppable: !node.leaf,
|
||||
handleDrop: (
|
||||
node: TreeExplorerNode<ComfyNodeDefImpl>,
|
||||
data: TreeExplorerDragAndDropData<ComfyNodeDefImpl>
|
||||
) => {
|
||||
const nodeDefToAdd = data.data.data
|
||||
// Remove bookmark if the source is the top level bookmarked node.
|
||||
if (nodeBookmarkStore.isBookmarked(nodeDefToAdd)) {
|
||||
nodeBookmarkStore.toggleBookmark(nodeDefToAdd)
|
||||
}
|
||||
const folderNodeDef = node.data as ComfyNodeDefImpl
|
||||
const nodePath =
|
||||
folderNodeDef.category + '/' + nodeDefToAdd.display_name
|
||||
nodeBookmarkStore.addBookmark(nodePath)
|
||||
},
|
||||
...(node.leaf
|
||||
? {}
|
||||
: {
|
||||
handleRename,
|
||||
handleDelete: (node: TreeExplorerNode<ComfyNodeDefImpl>) => {
|
||||
nodeBookmarkStore.deleteBookmarkFolder(node.data)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
return fillNodeInfo(bookmarkedRoot.value)
|
||||
}
|
||||
)
|
||||
|
||||
const treeExplorerRef = ref<InstanceType<typeof TreeExplorer> | null>(null)
|
||||
const addNewBookmarkFolder = (
|
||||
parent?: RenderedTreeExplorerNode<ComfyNodeDefImpl>
|
||||
) => {
|
||||
const newFolderKey =
|
||||
'root/' + nodeBookmarkStore.addNewBookmarkFolder(parent?.data).slice(0, -1)
|
||||
nextTick(() => {
|
||||
treeExplorerRef.value?.renameCommand(
|
||||
findNodeByKey(
|
||||
renderedBookmarkedRoot.value,
|
||||
newFolderKey
|
||||
) as RenderedTreeExplorerNode
|
||||
)
|
||||
if (parent) {
|
||||
expandedKeys.value[parent.key] = true
|
||||
}
|
||||
})
|
||||
}
|
||||
defineExpose({
|
||||
addNewBookmarkFolder
|
||||
})
|
||||
|
||||
const toast = useToast()
|
||||
const { t } = useI18n()
|
||||
const handleRename = (node: TreeNode, newName: string) => {
|
||||
if (node.data && node.data.isDummyFolder) {
|
||||
try {
|
||||
nodeBookmarkStore.renameBookmarkFolder(node.data, newName)
|
||||
} catch (e) {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('error'),
|
||||
detail: e.message,
|
||||
life: 3000
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const showCustomizationDialog = ref(false)
|
||||
const initialIcon = ref(nodeBookmarkStore.defaultBookmarkIcon)
|
||||
const initialColor = ref(nodeBookmarkStore.defaultBookmarkColor)
|
||||
const customizationTargetNodePath = ref('')
|
||||
const updateCustomization = (icon: string, color: string) => {
|
||||
if (customizationTargetNodePath.value) {
|
||||
nodeBookmarkStore.updateBookmarkCustomization(
|
||||
customizationTargetNodePath.value,
|
||||
{ icon, color }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const extraMenuItems = computed(
|
||||
() => (menuTargetNode: RenderedTreeExplorerNode<ComfyNodeDefImpl>) => [
|
||||
{
|
||||
label: t('newFolder'),
|
||||
icon: 'pi pi-folder-plus',
|
||||
command: () => {
|
||||
addNewBookmarkFolder(menuTargetNode)
|
||||
},
|
||||
visible: !menuTargetNode?.leaf
|
||||
},
|
||||
{
|
||||
label: t('customize'),
|
||||
icon: 'pi pi-palette',
|
||||
command: () => {
|
||||
const customization =
|
||||
nodeBookmarkStore.bookmarksCustomization[menuTargetNode.data.nodePath]
|
||||
initialIcon.value =
|
||||
customization?.icon || nodeBookmarkStore.defaultBookmarkIcon
|
||||
initialColor.value =
|
||||
customization?.color || nodeBookmarkStore.defaultBookmarkColor
|
||||
|
||||
showCustomizationDialog.value = true
|
||||
customizationTargetNodePath.value = menuTargetNode.data.nodePath
|
||||
},
|
||||
visible: !menuTargetNode?.leaf
|
||||
}
|
||||
]
|
||||
)
|
||||
</script>
|
||||
@@ -1,103 +1,42 @@
|
||||
<template>
|
||||
<div
|
||||
:class="[
|
||||
'node-tree-folder',
|
||||
{ bookmark: props.isBookmarkFolder, 'can-drop': canDrop }
|
||||
]"
|
||||
ref="container"
|
||||
>
|
||||
<span class="folder-label">
|
||||
<slot name="folder-label" :node="props.node">
|
||||
{{ props.node.label }}
|
||||
</slot>
|
||||
</span>
|
||||
<Badge
|
||||
:value="props.node.totalNodes"
|
||||
severity="secondary"
|
||||
:style="{ marginLeft: '0.5rem' }"
|
||||
/>
|
||||
<div ref="container" class="node-lib-node-container">
|
||||
<TreeExplorerTreeNode
|
||||
:node="node"
|
||||
@item-dropped="handleItemDrop"
|
||||
></TreeExplorerTreeNode>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import TreeExplorerTreeNode from '@/components/common/TreeExplorerTreeNode.vue'
|
||||
import { ComfyNodeDefImpl } from '@/stores/nodeDefStore'
|
||||
import { useNodeBookmarkStore } from '@/stores/nodeBookmarkStore'
|
||||
import type { CanvasDragAndDropData } from '@/types/litegraphTypes'
|
||||
import { dropTargetForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter'
|
||||
import Badge from 'primevue/badge'
|
||||
import type { TreeNode } from 'primevue/treenode'
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { computed, inject, onMounted, onUnmounted, Ref, ref, watch } from 'vue'
|
||||
import type { BookmarkCustomization } from '@/types/apiTypes'
|
||||
import { RenderedTreeExplorerNode } from '@/types/treeExplorerTypes'
|
||||
|
||||
const props = defineProps<{
|
||||
node: TreeNode
|
||||
isBookmarkFolder: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'itemDropped', node: TreeNode): void
|
||||
node: RenderedTreeExplorerNode<ComfyNodeDefImpl>
|
||||
}>()
|
||||
|
||||
const nodeBookmarkStore = useNodeBookmarkStore()
|
||||
|
||||
const customization = computed<BookmarkCustomization | undefined>(() => {
|
||||
return nodeBookmarkStore.bookmarksCustomization[props.node.data.nodePath]
|
||||
})
|
||||
|
||||
const addNodeToBookmarkFolder = (node: ComfyNodeDefImpl) => {
|
||||
if (!props.node.data) {
|
||||
console.error('Bookmark folder does not have data!')
|
||||
return
|
||||
}
|
||||
if (nodeBookmarkStore.isBookmarked(node)) {
|
||||
nodeBookmarkStore.toggleBookmark(node)
|
||||
}
|
||||
|
||||
const folderNodeDef = props.node.data as ComfyNodeDefImpl
|
||||
const nodePath = folderNodeDef.category + '/' + node.display_name
|
||||
nodeBookmarkStore.addBookmark(nodePath)
|
||||
}
|
||||
|
||||
const container = ref<HTMLElement | null>(null)
|
||||
const canDrop = ref(false)
|
||||
|
||||
const treeNodeElement = ref<HTMLElement | null>(null)
|
||||
const iconElement = ref<HTMLElement | null>(null)
|
||||
|
||||
let dropTargetCleanup = () => {}
|
||||
let stopWatchCustomization: (() => void) | null = null
|
||||
|
||||
const container = ref<HTMLElement | null>(null)
|
||||
onMounted(() => {
|
||||
if (!props.isBookmarkFolder) return
|
||||
|
||||
treeNodeElement.value = container.value?.closest(
|
||||
'.p-tree-node-content'
|
||||
) as HTMLElement
|
||||
dropTargetCleanup = dropTargetForElements({
|
||||
element: treeNodeElement.value,
|
||||
onDrop: (event) => {
|
||||
const dndData = event.source.data as CanvasDragAndDropData
|
||||
if (dndData.type === 'add-node') {
|
||||
addNodeToBookmarkFolder(dndData.data)
|
||||
canDrop.value = false
|
||||
emit('itemDropped', props.node)
|
||||
}
|
||||
},
|
||||
onDragEnter: (event) => {
|
||||
const dndData = event.source.data as CanvasDragAndDropData
|
||||
if (dndData.type === 'add-node') {
|
||||
canDrop.value = true
|
||||
}
|
||||
},
|
||||
onDragLeave: (event) => {
|
||||
canDrop.value = false
|
||||
}
|
||||
})
|
||||
|
||||
iconElement.value = treeNodeElement.value.querySelector(
|
||||
':scope > .p-tree-node-icon'
|
||||
) as HTMLElement
|
||||
|
||||
updateIconColor()
|
||||
|
||||
// Start watching after the component is mounted
|
||||
@@ -111,16 +50,13 @@ const updateIconColor = () => {
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
dropTargetCleanup()
|
||||
if (stopWatchCustomization) {
|
||||
stopWatchCustomization()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.node-tree-folder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
const expandedKeys = inject<Ref<Record<string, boolean>>>('expandedKeys')
|
||||
const handleItemDrop = (node: RenderedTreeExplorerNode) => {
|
||||
expandedKeys.value[node.key] = true
|
||||
}
|
||||
</style>
|
||||
</script>
|
||||
|
||||
@@ -1,83 +1,129 @@
|
||||
<template>
|
||||
<div :class="['node-tree-leaf', { bookmark: isBookmarked }]" ref="container">
|
||||
<div class="node-content">
|
||||
<Tag
|
||||
v-if="node.experimental"
|
||||
:value="$t('experimental')"
|
||||
severity="primary"
|
||||
/>
|
||||
<Tag v-if="node.deprecated" :value="$t('deprecated')" severity="danger" />
|
||||
<span class="node-label">{{ node.display_name }}</span>
|
||||
</div>
|
||||
<Button
|
||||
class="bookmark-button"
|
||||
size="small"
|
||||
:icon="isBookmarked ? 'pi pi-bookmark-fill' : 'pi pi-bookmark'"
|
||||
text
|
||||
severity="secondary"
|
||||
@click.stop="toggleBookmark"
|
||||
/>
|
||||
<div ref="container" class="node-lib-node-container">
|
||||
<TreeExplorerTreeNode :node="node">
|
||||
<template #before-label>
|
||||
<Tag
|
||||
v-if="nodeDef.experimental"
|
||||
:value="$t('experimental')"
|
||||
severity="primary"
|
||||
/>
|
||||
<Tag
|
||||
v-if="nodeDef.deprecated"
|
||||
:value="$t('deprecated')"
|
||||
severity="danger"
|
||||
/>
|
||||
</template>
|
||||
<template #actions>
|
||||
<Button
|
||||
class="bookmark-button"
|
||||
size="small"
|
||||
:icon="isBookmarked ? 'pi pi-bookmark-fill' : 'pi pi-bookmark'"
|
||||
text
|
||||
severity="secondary"
|
||||
@click.stop="toggleBookmark"
|
||||
/>
|
||||
</template>
|
||||
</TreeExplorerTreeNode>
|
||||
|
||||
<teleport v-if="isHovered" to="#node-library-node-preview-container">
|
||||
<div class="node-lib-node-preview" :style="nodePreviewStyle">
|
||||
<NodePreview ref="previewRef" :nodeDef="nodeDef"></NodePreview>
|
||||
</div>
|
||||
</teleport>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import Tag from 'primevue/tag'
|
||||
import TreeExplorerTreeNode from '@/components/common/TreeExplorerTreeNode.vue'
|
||||
import NodePreview from '@/components/node/NodePreview.vue'
|
||||
import { ComfyNodeDefImpl } from '@/stores/nodeDefStore'
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
import { draggable } from '@atlaskit/pragmatic-drag-and-drop/element/adapter'
|
||||
import { CanvasDragAndDropData } from '@/types/litegraphTypes'
|
||||
import {
|
||||
computed,
|
||||
CSSProperties,
|
||||
nextTick,
|
||||
onMounted,
|
||||
onUnmounted,
|
||||
ref
|
||||
} from 'vue'
|
||||
import { RenderedTreeExplorerNode } from '@/types/treeExplorerTypes'
|
||||
import { useNodeBookmarkStore } from '@/stores/nodeBookmarkStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
|
||||
const props = defineProps<{
|
||||
node: ComfyNodeDefImpl
|
||||
isBookmarked: boolean
|
||||
node: RenderedTreeExplorerNode<ComfyNodeDefImpl>
|
||||
}>()
|
||||
|
||||
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 emit = defineEmits<{
|
||||
(e: 'toggle-bookmark', value: ComfyNodeDefImpl): void
|
||||
}>()
|
||||
|
||||
const toggleBookmark = () => {
|
||||
emit('toggle-bookmark', props.node)
|
||||
nodeBookmarkStore.toggleBookmark(nodeDef.value)
|
||||
}
|
||||
|
||||
const previewRef = ref<InstanceType<typeof NodePreview> | null>(null)
|
||||
const nodePreviewStyle = ref<CSSProperties>({
|
||||
position: 'absolute',
|
||||
top: '0px',
|
||||
left: '0px'
|
||||
})
|
||||
|
||||
const handleNodeHover = async () => {
|
||||
const hoverTarget = nodeContentElement.value
|
||||
const targetRect = hoverTarget.getBoundingClientRect()
|
||||
|
||||
const previewHeight = previewRef.value?.$el.offsetHeight || 0
|
||||
const availableSpaceBelow = window.innerHeight - targetRect.bottom
|
||||
|
||||
nodePreviewStyle.value.top =
|
||||
previewHeight > availableSpaceBelow
|
||||
? `${Math.max(0, targetRect.top - (previewHeight - availableSpaceBelow) - 20)}px`
|
||||
: `${targetRect.top - 40}px`
|
||||
if (sidebarLocation.value === 'left') {
|
||||
nodePreviewStyle.value.left = `${targetRect.right}px`
|
||||
} else {
|
||||
nodePreviewStyle.value.left = `${targetRect.left - 400}px`
|
||||
}
|
||||
}
|
||||
|
||||
const container = ref<HTMLElement | null>(null)
|
||||
let draggableCleanup: () => void
|
||||
const nodeContentElement = ref<HTMLElement | null>(null)
|
||||
const isHovered = ref(false)
|
||||
const handleMouseEnter = async () => {
|
||||
isHovered.value = true
|
||||
await nextTick()
|
||||
handleNodeHover()
|
||||
}
|
||||
const handleMouseLeave = () => {
|
||||
isHovered.value = false
|
||||
}
|
||||
onMounted(() => {
|
||||
const treeNodeElement = container.value?.closest(
|
||||
'.p-tree-node'
|
||||
) as HTMLElement
|
||||
draggableCleanup = draggable({
|
||||
element: treeNodeElement,
|
||||
getInitialData() {
|
||||
return {
|
||||
type: 'add-node',
|
||||
data: props.node
|
||||
} as CanvasDragAndDropData<ComfyNodeDefImpl>
|
||||
}
|
||||
})
|
||||
nodeContentElement.value = container.value?.closest('.p-tree-node-content')
|
||||
nodeContentElement.value?.addEventListener('mouseenter', handleMouseEnter)
|
||||
nodeContentElement.value?.addEventListener('mouseleave', handleMouseLeave)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
draggableCleanup()
|
||||
nodeContentElement.value?.removeEventListener('mouseenter', handleMouseEnter)
|
||||
nodeContentElement.value?.removeEventListener('mouseleave', handleMouseLeave)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.node-tree-leaf {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.node-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.node-label {
|
||||
margin-left: 0.5rem;
|
||||
.node-lib-node-container {
|
||||
@apply h-full w-full;
|
||||
}
|
||||
|
||||
.bookmark-button {
|
||||
|
||||
Reference in New Issue
Block a user