mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-02 20:22:08 +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>
106 lines
2.8 KiB
TypeScript
106 lines
2.8 KiB
TypeScript
import { mount } from '@vue/test-utils'
|
|
import { nextTick } from 'vue'
|
|
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
import MarqueeLine from './MarqueeLine.vue'
|
|
import TextTickerMultiLine from './TextTickerMultiLine.vue'
|
|
|
|
type Callback = () => void
|
|
|
|
const resizeCallbacks: Callback[] = []
|
|
const mutationCallbacks: Callback[] = []
|
|
|
|
vi.mock('@vueuse/core', async () => {
|
|
const actual = await vi.importActual('@vueuse/core')
|
|
return {
|
|
...actual,
|
|
useResizeObserver: (_target: unknown, cb: Callback) => {
|
|
resizeCallbacks.push(cb)
|
|
return { stop: vi.fn() }
|
|
},
|
|
useMutationObserver: (_target: unknown, cb: Callback) => {
|
|
mutationCallbacks.push(cb)
|
|
return { stop: vi.fn() }
|
|
}
|
|
}
|
|
})
|
|
|
|
function mockElementSize(
|
|
el: HTMLElement,
|
|
clientWidth: number,
|
|
scrollWidth: number
|
|
) {
|
|
Object.defineProperty(el, 'clientWidth', {
|
|
value: clientWidth,
|
|
configurable: true
|
|
})
|
|
Object.defineProperty(el, 'scrollWidth', {
|
|
value: scrollWidth,
|
|
configurable: true
|
|
})
|
|
}
|
|
|
|
describe(TextTickerMultiLine, () => {
|
|
let wrapper: ReturnType<typeof mount>
|
|
|
|
afterEach(() => {
|
|
wrapper?.unmount()
|
|
resizeCallbacks.length = 0
|
|
mutationCallbacks.length = 0
|
|
})
|
|
|
|
function mountComponent(text: string) {
|
|
wrapper = mount(TextTickerMultiLine, {
|
|
slots: { default: text }
|
|
})
|
|
return wrapper
|
|
}
|
|
|
|
function getMeasureEl(): HTMLElement {
|
|
return wrapper.find('[aria-hidden="true"]').element as HTMLElement
|
|
}
|
|
|
|
async function triggerSplitLines() {
|
|
resizeCallbacks.forEach((cb) => cb())
|
|
await nextTick()
|
|
}
|
|
|
|
it('renders slot content', () => {
|
|
mountComponent('Load Checkpoint')
|
|
expect(wrapper.text()).toContain('Load Checkpoint')
|
|
})
|
|
|
|
it('renders a single MarqueeLine when text fits', async () => {
|
|
mountComponent('Short')
|
|
mockElementSize(getMeasureEl(), 200, 100)
|
|
await triggerSplitLines()
|
|
|
|
expect(wrapper.findAllComponents(MarqueeLine)).toHaveLength(1)
|
|
})
|
|
|
|
it('renders two MarqueeLines when text overflows', async () => {
|
|
mountComponent('Load Checkpoint Loader Simple')
|
|
mockElementSize(getMeasureEl(), 100, 300)
|
|
await triggerSplitLines()
|
|
|
|
expect(wrapper.findAllComponents(MarqueeLine)).toHaveLength(2)
|
|
})
|
|
|
|
it('splits text at word boundary when overflowing', async () => {
|
|
mountComponent('Load Checkpoint Loader')
|
|
mockElementSize(getMeasureEl(), 100, 200)
|
|
await triggerSplitLines()
|
|
|
|
const lines = wrapper.findAllComponents(MarqueeLine)
|
|
expect(lines[0].text()).toBe('Load')
|
|
expect(lines[1].text()).toBe('Checkpoint Loader')
|
|
})
|
|
|
|
it('has hidden measurement element with aria-hidden', () => {
|
|
mountComponent('Test')
|
|
const measureEl = wrapper.find('[aria-hidden="true"]')
|
|
expect(measureEl.exists()).toBe(true)
|
|
expect(measureEl.classes()).toContain('invisible')
|
|
})
|
|
})
|