mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-01 19:51:54 +00:00
## Summary Implement a redesigned Node Library sidebar using Reka UI components with virtualized tree rendering and improved UX. ## Changes - **What**: - Add three-tab structure (Essential, All, Custom) using Reka UI Tabs - Implement TreeExplorerV2 with virtualized tree using TreeRoot/TreeVirtualizer for performance - Add node hover preview with teleport to show NodePreview component - Implement context menu for toggling favorites on nodes - Add search functionality that auto-expands matching folders - Create panel components: EssentialNodesPanel, AllNodesPanel, CustomNodesPanel - Add 'Open Manager' button in CustomNodesPanel - Use custom icons: comfy--node for nodes, ph--folder-fill for folders - New node preview component: `NodePreviewCard` - Api node folder icon - Node drag preview - **Feature Flag**: Enabled via URL parameter `?nodeRedesign=true` ## Review Focus - TreeExplorerV2.vue uses `[...expandedKeys]` to prevent internal mutation by Reka UI TreeRoot - Context menu injection key is exported from TreeExplorerV2Node.vue and imported by TreeExplorerV2.vue - Hover preview uses teleport to `#node-library-node-preview-container-v2` ## Screenshots (if applicable) | Feature | Screenshot | |---|---| | All nodes tab |<img width="323" height="761" alt="image" src="https://github.com/user-attachments/assets/1976222b-83dc-4a1b-838a-2d49aedea3b8" />| | Custom nodes tab | <img width="308" height="748" alt="image" src="https://github.com/user-attachments/assets/2c23bffb-bdaa-4c6c-8cac-7610fb7f3fb7" />| |Api nodes icon | <img width="299" height="523" alt="image" src="https://github.com/user-attachments/assets/e9ca05b0-1143-44cf-b227-6462173c7cd0" />| | node preview|<img width="499" height="544" alt="image" src="https://github.com/user-attachments/assets/8961a7b4-77ae-4e57-99cf-62d9e4e17088" />| | node drag preview | <img width="434" height="289" alt="image" src="https://github.com/user-attachments/assets/b5838c90-65d4-4bee-b2b3-c41b57870da8" />| Test by adding `?nodeRedesign=true` to the URL ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-8548-WIP-feat-implement-NodeLibrarySidebarTabV2-with-Reka-UI-components-2fb6d73d36508134b7e0f75a2c9b976a) by [Unito](https://www.unito.io) --------- Co-authored-by: Amp <amp@ampcode.com> Co-authored-by: GitHub Action <action@github.com> Co-authored-by: bymyself <cbyrne@comfy.org>
133 lines
3.3 KiB
TypeScript
133 lines
3.3 KiB
TypeScript
import { mount } from '@vue/test-utils'
|
|
import { createTestingPinia } from '@pinia/testing'
|
|
import { TabsContent, TabsList, TabsRoot, TabsTrigger } from 'reka-ui'
|
|
import { ref } from 'vue'
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import { createI18n } from 'vue-i18n'
|
|
|
|
import NodeLibrarySidebarTabV2 from './NodeLibrarySidebarTabV2.vue'
|
|
|
|
vi.mock('@vueuse/core', async () => {
|
|
const actual = await vi.importActual('@vueuse/core')
|
|
return {
|
|
...actual,
|
|
useLocalStorage: vi.fn((_key: string, defaultValue: unknown) =>
|
|
ref(defaultValue)
|
|
)
|
|
}
|
|
})
|
|
|
|
vi.mock('@/composables/node/useNodeDragToCanvas', () => ({
|
|
useNodeDragToCanvas: () => ({
|
|
isDragging: { value: false },
|
|
draggedNode: { value: null },
|
|
cursorPosition: { value: { x: 0, y: 0 } },
|
|
startDrag: vi.fn(),
|
|
cancelDrag: vi.fn(),
|
|
setupGlobalListeners: vi.fn(),
|
|
cleanupGlobalListeners: vi.fn()
|
|
})
|
|
}))
|
|
|
|
vi.mock('@/services/nodeOrganizationService', () => ({
|
|
DEFAULT_TAB_ID: 'essentials',
|
|
DEFAULT_SORTING_ID: 'alphabetical',
|
|
nodeOrganizationService: {
|
|
organizeNodesByTab: vi.fn(() => []),
|
|
getSortingStrategies: vi.fn(() => [])
|
|
}
|
|
}))
|
|
|
|
vi.mock('./nodeLibrary/AllNodesPanel.vue', () => ({
|
|
default: {
|
|
name: 'AllNodesPanel',
|
|
template: '<div data-testid="all-panel"><slot /></div>',
|
|
props: ['sections', 'expandedKeys', 'fillNodeInfo']
|
|
}
|
|
}))
|
|
|
|
vi.mock('./nodeLibrary/CustomNodesPanel.vue', () => ({
|
|
default: {
|
|
name: 'CustomNodesPanel',
|
|
template: '<div data-testid="custom-panel"><slot /></div>',
|
|
props: ['sections', 'expandedKeys']
|
|
}
|
|
}))
|
|
|
|
vi.mock('./nodeLibrary/EssentialNodesPanel.vue', () => ({
|
|
default: {
|
|
name: 'EssentialNodesPanel',
|
|
template: '<div data-testid="essential-panel"><slot /></div>',
|
|
props: ['root', 'expandedKeys']
|
|
}
|
|
}))
|
|
|
|
vi.mock('./nodeLibrary/NodeDragPreview.vue', () => ({
|
|
default: {
|
|
name: 'NodeDragPreview',
|
|
template: '<div />'
|
|
}
|
|
}))
|
|
|
|
vi.mock('@/components/common/SearchBoxV2.vue', () => ({
|
|
default: {
|
|
name: 'SearchBox',
|
|
template: '<input data-testid="search-box" />',
|
|
props: ['modelValue', 'placeholder'],
|
|
setup() {
|
|
return { focus: vi.fn() }
|
|
},
|
|
expose: ['focus']
|
|
}
|
|
}))
|
|
|
|
const i18n = createI18n({
|
|
legacy: false,
|
|
locale: 'en',
|
|
messages: { en: {} }
|
|
})
|
|
|
|
describe('NodeLibrarySidebarTabV2', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
function mountComponent() {
|
|
return mount(NodeLibrarySidebarTabV2, {
|
|
global: {
|
|
plugins: [createTestingPinia({ stubActions: false }), i18n],
|
|
components: {
|
|
TabsRoot,
|
|
TabsList,
|
|
TabsTrigger,
|
|
TabsContent
|
|
},
|
|
stubs: {
|
|
teleport: true
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
it('should render with tabs', () => {
|
|
const wrapper = mountComponent()
|
|
|
|
const triggers = wrapper.findAllComponents(TabsTrigger)
|
|
expect(triggers).toHaveLength(3)
|
|
})
|
|
|
|
it('should render search box', () => {
|
|
const wrapper = mountComponent()
|
|
|
|
expect(wrapper.find('[data-testid="search-box"]').exists()).toBe(true)
|
|
})
|
|
|
|
it('should render only the selected panel', () => {
|
|
const wrapper = mountComponent()
|
|
|
|
expect(wrapper.find('[data-testid="essential-panel"]').exists()).toBe(true)
|
|
expect(wrapper.find('[data-testid="all-panel"]').exists()).toBe(false)
|
|
expect(wrapper.find('[data-testid="custom-panel"]').exists()).toBe(false)
|
|
})
|
|
})
|