[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:
Chenlei Hu
2024-09-01 14:03:15 -04:00
committed by GitHub
parent 5383f97eba
commit d04dbcd2c1
9 changed files with 516 additions and 477 deletions

View File

@@ -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>