mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-28 02:02:08 +00:00
feat: Node Library sidebar and V2 Search dialog UI/UX updates (#9085)
## Summary Implement 11 Figma design discrepancies for the Node Library sidebar and V2 Node Search dialog, aligning the UI with the [Toolbox Figma design](https://www.figma.com/design/xMFxCziXJe6Denz4dpDGTq/Toolbox?node-id=2074-21394&m=dev). ## Changes - **What**: Sidebar: reorder tabs (All/Essentials/Blueprints), rename Custom→Blueprints, uppercase section headers, chevron-left of folder icon, bookmark-on-hover for node rows, filter dropdown with checkbox items, sort labels (Categorized/A-Z) with label-left/check-right layout, hide section headers in A-Z mode. Search dialog: expand filter chips from 3→6, add Recents and source categories to sidebar, remove "Filter by" label. Pull foundation V2 components from merged PR #8548. - **Dependencies**: Depends on #8987 (V2 Node Search) and #8548 (NodeLibrarySidebarTabV2) ## Review Focus - Filter dropdown (`filterOptions`) is UI-scaffolded but not yet wired to filtering logic (pending V2 integration) - "Recents" category currently returns frequency-based results as placeholder until a usage-tracking store is implemented - Pre-existing type errors from V2 PR dependencies not in the base commit (SearchBoxV2, usePerTabState, TextTicker, getProviderIcon, getLinkTypeColor, SidebarContainerKey) are expected and will resolve when rebased onto main after parent PRs land ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-9085-feat-Node-Library-sidebar-and-V2-Search-dialog-Figma-design-improvements-30f6d73d36508175bf72d716f5904476) by [Unito](https://www.unito.io) --------- Co-authored-by: Yourz <crazilou@vip.qq.com> Co-authored-by: github-actions <github-actions@github.com>
This commit is contained in:
@@ -1,8 +1,6 @@
|
||||
import type { EssentialsCategory } from '@/constants/essentialsNodes'
|
||||
import {
|
||||
ESSENTIALS_CATEGORIES,
|
||||
ESSENTIALS_NODES
|
||||
} from '@/constants/essentialsNodes'
|
||||
import { ESSENTIALS_NODES } from '@/constants/essentialsNodes'
|
||||
import { t } from '@/i18n'
|
||||
import type { ComfyNodeDefImpl } from '@/stores/nodeDefStore'
|
||||
import { buildNodeDefTree } from '@/stores/nodeDefStore'
|
||||
import type {
|
||||
@@ -14,11 +12,23 @@ import type {
|
||||
} from '@/types/nodeOrganizationTypes'
|
||||
import { NodeSourceType } from '@/types/nodeSource'
|
||||
import type { TreeNode } from '@/types/treeExplorerTypes'
|
||||
import { sortedTree } from '@/utils/treeUtil'
|
||||
import { upperCase } from 'es-toolkit/string'
|
||||
import { sortedTree, unwrapTreeRoot } from '@/utils/treeUtil'
|
||||
|
||||
const DEFAULT_ICON = 'pi pi-sort'
|
||||
|
||||
function categoryPathExtractor(nodeDef: ComfyNodeDefImpl): string[] {
|
||||
const category = nodeDef.category || ''
|
||||
const categoryParts = category ? category.split('/') : []
|
||||
return [...categoryParts, nodeDef.name]
|
||||
}
|
||||
|
||||
function isBlueprint(node: ComfyNodeDefImpl): boolean {
|
||||
return (
|
||||
node.nodeSource.type === NodeSourceType.Blueprint ||
|
||||
!!node.python_module?.startsWith('blueprint')
|
||||
)
|
||||
}
|
||||
|
||||
export const DEFAULT_GROUPING_ID = 'category' as const
|
||||
export const DEFAULT_SORTING_ID = 'original' as const
|
||||
export const DEFAULT_TAB_ID = 'all' as const
|
||||
@@ -30,11 +40,7 @@ class NodeOrganizationService {
|
||||
label: 'sideToolbar.nodeLibraryTab.groupStrategies.category',
|
||||
icon: 'pi pi-folder',
|
||||
description: 'sideToolbar.nodeLibraryTab.groupStrategies.categoryDesc',
|
||||
getNodePath: (nodeDef: ComfyNodeDefImpl) => {
|
||||
const category = nodeDef.category || ''
|
||||
const categoryParts = category ? category.split('/') : []
|
||||
return [...categoryParts, nodeDef.name]
|
||||
}
|
||||
getNodePath: categoryPathExtractor
|
||||
},
|
||||
{
|
||||
id: 'module',
|
||||
@@ -125,102 +131,197 @@ class NodeOrganizationService {
|
||||
nodes: ComfyNodeDefImpl[],
|
||||
tabId: TabId = DEFAULT_TAB_ID
|
||||
): NodeSection[] {
|
||||
const categoryPathExtractor = (nodeDef: ComfyNodeDefImpl) => {
|
||||
const category = nodeDef.category || ''
|
||||
const categoryParts = category ? category.split('/') : []
|
||||
return [...categoryParts, nodeDef.name]
|
||||
}
|
||||
|
||||
switch (tabId) {
|
||||
case 'essentials': {
|
||||
const essentialNodes = nodes.filter(
|
||||
(nodeDef) => nodeDef.essentials_category !== undefined
|
||||
)
|
||||
const essentialsPathExtractor = (nodeDef: ComfyNodeDefImpl) => {
|
||||
const folder = nodeDef.essentials_category || ''
|
||||
return folder ? [folder, nodeDef.name] : [nodeDef.name]
|
||||
}
|
||||
const tree = buildNodeDefTree(essentialNodes, {
|
||||
pathExtractor: essentialsPathExtractor
|
||||
})
|
||||
if (tree.children) {
|
||||
const len = ESSENTIALS_CATEGORIES.length
|
||||
const originalIndex = new Map(
|
||||
tree.children.map((child, i) => [child, i])
|
||||
)
|
||||
tree.children.sort((a, b) => {
|
||||
const ai = ESSENTIALS_CATEGORIES.indexOf(
|
||||
a.label as EssentialsCategory
|
||||
)
|
||||
const bi = ESSENTIALS_CATEGORIES.indexOf(
|
||||
b.label as EssentialsCategory
|
||||
)
|
||||
const orderA = ai === -1 ? len + originalIndex.get(a)! : ai
|
||||
const orderB = bi === -1 ? len + originalIndex.get(b)! : bi
|
||||
return orderA - orderB
|
||||
})
|
||||
for (const folder of tree.children) {
|
||||
if (!folder.children) continue
|
||||
const order = ESSENTIALS_NODES[folder.label as EssentialsCategory]
|
||||
if (!order) continue
|
||||
const nodeOrder: readonly string[] = order
|
||||
const orderLen = nodeOrder.length
|
||||
folder.children.sort((a, b) => {
|
||||
const nameA = a.data?.name ?? a.label ?? ''
|
||||
const nameB = b.data?.name ?? b.label ?? ''
|
||||
const ai = nodeOrder.indexOf(nameA)
|
||||
const bi = nodeOrder.indexOf(nameB)
|
||||
const orderA = ai === -1 ? orderLen : ai
|
||||
const orderB = bi === -1 ? orderLen : bi
|
||||
return orderA - orderB
|
||||
})
|
||||
}
|
||||
}
|
||||
return [{ tree }]
|
||||
}
|
||||
case 'custom': {
|
||||
const customNodes = nodes.filter(
|
||||
(nodeDef) => nodeDef.nodeSource.type === NodeSourceType.CustomNodes
|
||||
)
|
||||
const groupedByMainCategory = new Map<string, ComfyNodeDefImpl[]>()
|
||||
for (const node of customNodes) {
|
||||
const mainCategory = node.main_category ?? 'custom_extensions'
|
||||
if (!groupedByMainCategory.has(mainCategory)) {
|
||||
groupedByMainCategory.set(mainCategory, [])
|
||||
}
|
||||
groupedByMainCategory.get(mainCategory)!.push(node)
|
||||
}
|
||||
|
||||
return Array.from(groupedByMainCategory.entries()).map(
|
||||
([mainCategory, categoryNodes]) => ({
|
||||
title: upperCase(mainCategory),
|
||||
tree: buildNodeDefTree(categoryNodes, {
|
||||
pathExtractor: categoryPathExtractor
|
||||
})
|
||||
})
|
||||
)
|
||||
}
|
||||
case 'essentials':
|
||||
return this.organizeEssentials(nodes)
|
||||
case 'blueprints':
|
||||
return this.organizeBlueprints(nodes)
|
||||
case 'all':
|
||||
default: {
|
||||
const groupedByMainCategory = new Map<string, ComfyNodeDefImpl[]>()
|
||||
for (const node of nodes) {
|
||||
const mainCategory = node.main_category ?? 'basics'
|
||||
if (!groupedByMainCategory.has(mainCategory)) {
|
||||
groupedByMainCategory.set(mainCategory, [])
|
||||
}
|
||||
groupedByMainCategory.get(mainCategory)!.push(node)
|
||||
}
|
||||
default:
|
||||
return this.organizeAll(nodes)
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(groupedByMainCategory.entries()).map(
|
||||
([mainCategory, categoryNodes]) => ({
|
||||
title: upperCase(mainCategory),
|
||||
tree: buildNodeDefTree(categoryNodes, {
|
||||
pathExtractor: categoryPathExtractor
|
||||
})
|
||||
private organizeEssentials(nodes: ComfyNodeDefImpl[]): NodeSection[] {
|
||||
const essentialNodes = nodes.filter(
|
||||
(nodeDef) => !!nodeDef.essentials_category
|
||||
)
|
||||
const tree = buildNodeDefTree(essentialNodes, {
|
||||
pathExtractor: (nodeDef) => {
|
||||
const folder = nodeDef.essentials_category || ''
|
||||
return folder ? [folder, nodeDef.name] : [nodeDef.name]
|
||||
}
|
||||
})
|
||||
this.sortEssentialsFolders(tree)
|
||||
return [{ tree }]
|
||||
}
|
||||
|
||||
private sortEssentialsFolders(tree: TreeNode): void {
|
||||
if (!tree.children) return
|
||||
for (const folder of tree.children) {
|
||||
if (!folder.children) continue
|
||||
const order = ESSENTIALS_NODES[folder.label as EssentialsCategory]
|
||||
if (!order) continue
|
||||
const orderLen = order.length
|
||||
folder.children.sort((a, b) => {
|
||||
const ai = order.indexOf(a.data?.name ?? a.label ?? '')
|
||||
const bi = order.indexOf(b.data?.name ?? b.label ?? '')
|
||||
return (ai === -1 ? orderLen : ai) - (bi === -1 ? orderLen : bi)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private organizeBlueprints(nodes: ComfyNodeDefImpl[]): NodeSection[] {
|
||||
const { myBlueprints, comfyBlueprints } = this.partitionBlueprints(nodes)
|
||||
return [
|
||||
{
|
||||
title: 'sideToolbar.nodeLibraryTab.sections.myBlueprints',
|
||||
tree: unwrapTreeRoot(
|
||||
buildNodeDefTree(myBlueprints, {
|
||||
pathExtractor: categoryPathExtractor
|
||||
})
|
||||
)
|
||||
},
|
||||
{
|
||||
title: 'sideToolbar.nodeLibraryTab.sections.comfyBlueprints',
|
||||
tree: unwrapTreeRoot(
|
||||
buildNodeDefTree(comfyBlueprints, {
|
||||
pathExtractor: categoryPathExtractor
|
||||
})
|
||||
)
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
private organizeAll(nodes: ComfyNodeDefImpl[]): NodeSection[] {
|
||||
const {
|
||||
myBlueprints,
|
||||
comfyBlueprints,
|
||||
partnerNodes,
|
||||
comfyNodes,
|
||||
extensions
|
||||
} = this.classifyNodes(nodes)
|
||||
|
||||
const blueprintTree = this.buildBlueprintTree(
|
||||
myBlueprints,
|
||||
comfyBlueprints,
|
||||
categoryPathExtractor
|
||||
)
|
||||
|
||||
const sections: NodeSection[] = []
|
||||
|
||||
if (blueprintTree.children?.length) {
|
||||
sections.push({ category: 'blueprints', tree: blueprintTree })
|
||||
}
|
||||
if (partnerNodes.length > 0) {
|
||||
sections.push({
|
||||
category: 'partnerNodes',
|
||||
tree: unwrapTreeRoot(
|
||||
buildNodeDefTree(partnerNodes, {
|
||||
pathExtractor: categoryPathExtractor
|
||||
})
|
||||
)
|
||||
})
|
||||
}
|
||||
if (comfyNodes.length > 0) {
|
||||
sections.push({
|
||||
category: 'comfyNodes',
|
||||
tree: buildNodeDefTree(comfyNodes, {
|
||||
pathExtractor: categoryPathExtractor
|
||||
})
|
||||
})
|
||||
}
|
||||
if (extensions.length > 0) {
|
||||
sections.push({
|
||||
category: 'extensions',
|
||||
tree: buildNodeDefTree(extensions, {
|
||||
pathExtractor: categoryPathExtractor
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
return sections
|
||||
}
|
||||
|
||||
private partitionBlueprints(nodes: ComfyNodeDefImpl[]): {
|
||||
myBlueprints: ComfyNodeDefImpl[]
|
||||
comfyBlueprints: ComfyNodeDefImpl[]
|
||||
} {
|
||||
const myBlueprints: ComfyNodeDefImpl[] = []
|
||||
const comfyBlueprints: ComfyNodeDefImpl[] = []
|
||||
for (const node of nodes) {
|
||||
if (!isBlueprint(node)) continue
|
||||
if (node.isGlobal) comfyBlueprints.push(node)
|
||||
else myBlueprints.push(node)
|
||||
}
|
||||
return { myBlueprints, comfyBlueprints }
|
||||
}
|
||||
|
||||
private classifyNodes(nodes: ComfyNodeDefImpl[]): {
|
||||
myBlueprints: ComfyNodeDefImpl[]
|
||||
comfyBlueprints: ComfyNodeDefImpl[]
|
||||
partnerNodes: ComfyNodeDefImpl[]
|
||||
comfyNodes: ComfyNodeDefImpl[]
|
||||
extensions: ComfyNodeDefImpl[]
|
||||
} {
|
||||
const myBlueprints: ComfyNodeDefImpl[] = []
|
||||
const comfyBlueprints: ComfyNodeDefImpl[] = []
|
||||
const partnerNodes: ComfyNodeDefImpl[] = []
|
||||
const comfyNodes: ComfyNodeDefImpl[] = []
|
||||
const extensions: ComfyNodeDefImpl[] = []
|
||||
|
||||
for (const node of nodes) {
|
||||
if (isBlueprint(node)) {
|
||||
if (node.isGlobal) comfyBlueprints.push(node)
|
||||
else myBlueprints.push(node)
|
||||
} else if (node.api_node || node.category?.startsWith('api node')) {
|
||||
partnerNodes.push(node)
|
||||
} else if (
|
||||
node.nodeSource.type === NodeSourceType.Core ||
|
||||
node.nodeSource.type === NodeSourceType.Essentials
|
||||
) {
|
||||
comfyNodes.push(node)
|
||||
} else {
|
||||
extensions.push(node)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
myBlueprints,
|
||||
comfyBlueprints,
|
||||
partnerNodes,
|
||||
comfyNodes,
|
||||
extensions
|
||||
}
|
||||
}
|
||||
|
||||
private buildBlueprintTree(
|
||||
myBlueprints: ComfyNodeDefImpl[],
|
||||
comfyBlueprints: ComfyNodeDefImpl[],
|
||||
pathExtractor: (nodeDef: ComfyNodeDefImpl) => string[]
|
||||
): TreeNode {
|
||||
const children: TreeNode[] = []
|
||||
if (myBlueprints.length > 0) {
|
||||
const tree = unwrapTreeRoot(
|
||||
buildNodeDefTree(myBlueprints, { pathExtractor })
|
||||
)
|
||||
children.push({
|
||||
key: 'root/my-blueprints',
|
||||
label: t('sideToolbar.nodeLibraryTab.sections.myBlueprints'),
|
||||
children: tree.children
|
||||
})
|
||||
}
|
||||
if (comfyBlueprints.length > 0) {
|
||||
const tree = unwrapTreeRoot(
|
||||
buildNodeDefTree(comfyBlueprints, { pathExtractor })
|
||||
)
|
||||
children.push({
|
||||
key: 'root/comfy-blueprints',
|
||||
label: t('sideToolbar.nodeLibraryTab.sections.comfyBlueprints'),
|
||||
children: tree.children
|
||||
})
|
||||
}
|
||||
return { key: 'root', label: '', children }
|
||||
}
|
||||
|
||||
organizeNodes(
|
||||
|
||||
Reference in New Issue
Block a user