Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
72 commits
Select commit Hold shift + click to select a range
f42a4dd
[feat] Add core Vue widget infrastructure
christian-byrne Jun 24, 2025
2dcfe84
[feat] Add Vue widget registry system
christian-byrne Jun 24, 2025
06d0a63
[feat] Add Vue input widgets
christian-byrne Jun 24, 2025
ac60d1c
[feat] Add Vue selection widgets
christian-byrne Jun 24, 2025
9c4d782
[feat] Add Vue visual widgets
christian-byrne Jun 24, 2025
6e04cb7
[feat] Add Vue action widgets
christian-byrne Jun 24, 2025
19084e2
[feat] TransformPane - Viewport synchronization layer for Vue nodes (…
christian-byrne Aug 6, 2025
9db96f2
Update locales [skip ci]
invalid-email-address Aug 7, 2025
fc6943c
Fix TransformPane pos/size (#4826)
christian-byrne Aug 8, 2025
ac17752
Update locales [skip ci]
invalid-email-address Aug 8, 2025
301355e
refactor(litegraph): decouple render-time state from models for rerou…
benceruleanlu Aug 8, 2025
4171219
Revert "refactor(litegraph): decouple render-time state from models f…
benceruleanlu Aug 8, 2025
2b9a9e2
test(ci): skip transformPerformance suite on CI (#4843)
benceruleanlu Aug 9, 2025
8df41ab
Add vue node feature flag (#4927)
benceruleanlu Aug 12, 2025
c773230
feat: Implement CRDT-based layout system for Vue nodes (#4959)
christian-byrne Aug 17, 2025
889d136
[chore] Extract link rendering out of LGraphCanvas (#4994)
benceruleanlu Aug 18, 2025
0dd4ff2
refactor: Reorganize layout system into new renderer architecture (#5…
christian-byrne Aug 18, 2025
bfcbcf4
[refactor] Reorganize Vue nodes to domain-driven design architecture …
christian-byrne Aug 18, 2025
d1ed5ec
fix: Initialize Vue node manager when first node is added to empty gr…
christian-byrne Aug 19, 2025
b2a828d
[bugfix] Fix Vue node import path after refactoring
christian-byrne Aug 19, 2025
934c650
Remove layout logging noise from console (#5101)
christian-byrne Aug 19, 2025
5a74c01
remove logging from vue node layouting modules (#5111)
christian-byrne Aug 19, 2025
5171dec
feat: Add slot registration and spatial indexing for hit detection
benceruleanlu Aug 20, 2025
ba1fa1b
Revert "feat: Add slot registration and spatial indexing for hit dete…
benceruleanlu Aug 20, 2025
4a7c955
[bugfix] Fix link center dot hit detection when marker is disabled (#…
benceruleanlu Aug 20, 2025
1447b15
[bugfix] Hide center dot when dragging links (#5133)
benceruleanlu Aug 21, 2025
57db10f
feat: v3 style of node body (#5169)
LittleSound Aug 23, 2025
3982f29
Update lockfile after rebase (#5254)
benceruleanlu Aug 30, 2025
0830959
Fix lodash import (#5269)
benceruleanlu Aug 30, 2025
2a5e0d2
Decouple link and slot hit-testing out of Litegraph (#5134)
benceruleanlu Sep 1, 2025
62096d4
chore: Empty commit to trigger CI checks
benceruleanlu Sep 1, 2025
da042ae
[refactor] Remove unused legacy mutation types from layout system (#5…
christian-byrne Sep 1, 2025
3ce3b67
feat: localization fields (#5318)
LittleSound Sep 3, 2025
c6fc8e6
fix: remove clipping by removing unnecessary css contain (#5327)
simula-r Sep 4, 2025
969c8e6
[bugfix] Remove placeholder IMAGE widget to restore previous function…
benceruleanlu Sep 4, 2025
f83801e
- Convert class-based LayoutMutations to useLayoutMutations() composa…
christian-byrne Sep 4, 2025
1dbbf20
feat: widget styles for V3 UI (#5320)
LittleSound Sep 4, 2025
32cffa6
refactor: v3 ui slots connection dots (#5316)
LittleSound Sep 4, 2025
8a10387
add explicit typing on component IDs (#5352)
christian-byrne Sep 4, 2025
c30f5a4
Remove IMAGE widget cont. (#5355)
benceruleanlu Sep 4, 2025
6a3c075
Removes node's dependency on LGraph for access to layout mutations co…
christian-byrne Sep 4, 2025
2a64f53
Merge remote-tracking branch 'origin/main' into vue-nodes-migration
benceruleanlu Sep 4, 2025
f99c9de
[fix] Disable link markers on dragged connections (#5358)
benceruleanlu Sep 4, 2025
1480dd7
[bugfix] Fix NodeHeader test workflow path (#5359)
benceruleanlu Sep 4, 2025
2425f65
[Vue Nodes] Fix Node Header Tests (#5360)
benceruleanlu Sep 4, 2025
0f5315f
Update test expectations [skip ci]
invalid-email-address Sep 4, 2025
7b7f9bb
remove crdt ADR (moved to separate PR)
christian-byrne Sep 5, 2025
7130794
update adr README
christian-byrne Sep 5, 2025
73a1fee
removed unused IMAGE widget enum value
christian-byrne Sep 5, 2025
b0f2a1d
remove all unused (knip pass)
christian-byrne Sep 5, 2025
2f512b8
remove debug overlay panel
christian-byrne Sep 5, 2025
4fc8984
simplify unit tests
christian-byrne Sep 5, 2025
7149af6
change name "transformPaneEnabled" => "isVueNodesEnabled"
christian-byrne Sep 5, 2025
0b94158
remove debug viewport visualizer
christian-byrne Sep 5, 2025
3e5effe
remove debug viewport visualizer prop
christian-byrne Sep 5, 2025
9ab075f
remove outdated README
christian-byrne Sep 5, 2025
1e30756
skip all vue node operations if feature is turned off
christian-byrne Sep 5, 2025
e8dae57
remove debug logging and setting
christian-byrne Sep 5, 2025
f6051f6
remove event forwarding hack. todo: add link moving in vue
christian-byrne Sep 5, 2025
df36693
cleanup comments
christian-byrne Sep 5, 2025
358d98e
cleanup comments
christian-byrne Sep 5, 2025
b7fd1f4
add missing translations
christian-byrne Sep 5, 2025
0aed837
use camelCase for all non-component files
christian-byrne Sep 5, 2025
7d8bdcb
remove debug viewport test
christian-byrne Sep 5, 2025
6eeba70
- Fix memory leaks in node deletion (#5345)
christian-byrne Sep 5, 2025
817f4d0
remove redundant comment
christian-byrne Sep 5, 2025
07b7ed9
use camelcase for layoutStore filename
christian-byrne Sep 5, 2025
85fa2f4
removed unused type guards
christian-byrne Sep 5, 2025
8e098fc
simplify widget registration
christian-byrne Sep 5, 2025
896e44f
move back test that was mistakenly moved
christian-byrne Sep 5, 2025
f6ae1b6
remove unused typeguards
christian-byrne Sep 5, 2025
c52c798
removed unused node def type guards
christian-byrne Sep 5, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,6 @@ const value = api.getServerFeature('config_name', defaultValue) // Get config
- NEVER use `--no-verify` flag when committing
- NEVER delete or disable tests to make them pass
- NEVER circumvent quality checks
- NEVER use `dark:` prefix - always use `dark-theme:` for dark mode styles, for example: `dark-theme:text-white dark-theme:bg-black`
- NEVER use `:class="[]"` to merge class names - always use `import { cn } from '@/utils/tailwindUtil'`, for example: `<div :class="cn('bg-red-500', { 'bg-blue-500': condition })" />`

131 changes: 131 additions & 0 deletions browser_tests/fixtures/utils/vueNodeFixtures.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import type { Locator, Page } from '@playwright/test'

import type { NodeReference } from './litegraphUtils'

/**
* VueNodeFixture provides Vue-specific testing utilities for interacting with
* Vue node components. It bridges the gap between litegraph node references
* and Vue UI components.
*/
export class VueNodeFixture {
constructor(
private readonly nodeRef: NodeReference,
private readonly page: Page
) {}

/**
* Get the node's header element using data-testid
*/
async getHeader(): Promise<Locator> {
const nodeId = this.nodeRef.id
return this.page.locator(`[data-testid="node-header-${nodeId}"]`)
}

/**
* Get the node's title element
*/
async getTitleElement(): Promise<Locator> {
const header = await this.getHeader()
return header.locator('[data-testid="node-title"]')
}

/**
* Get the current title text
*/
async getTitle(): Promise<string> {
const titleElement = await this.getTitleElement()
return (await titleElement.textContent()) || ''
}

/**
* Set a new title by double-clicking and entering text
*/
async setTitle(newTitle: string): Promise<void> {
const titleElement = await this.getTitleElement()
await titleElement.dblclick()

const input = (await this.getHeader()).locator(
'[data-testid="node-title-input"]'
)
await input.fill(newTitle)
await input.press('Enter')
}

/**
* Cancel title editing
*/
async cancelTitleEdit(): Promise<void> {
const titleElement = await this.getTitleElement()
await titleElement.dblclick()

const input = (await this.getHeader()).locator(
'[data-testid="node-title-input"]'
)
await input.press('Escape')
}

/**
* Check if the title is currently being edited
*/
async isEditingTitle(): Promise<boolean> {
const header = await this.getHeader()
const input = header.locator('[data-testid="node-title-input"]')
return await input.isVisible()
}

/**
* Get the collapse/expand button
*/
async getCollapseButton(): Promise<Locator> {
const header = await this.getHeader()
return header.locator('[data-testid="node-collapse-button"]')
}

/**
* Toggle the node's collapsed state
*/
async toggleCollapse(): Promise<void> {
const button = await this.getCollapseButton()
await button.click()
}

/**
* Get the collapse icon element
*/
async getCollapseIcon(): Promise<Locator> {
const button = await this.getCollapseButton()
return button.locator('i')
}

/**
* Get the collapse icon's CSS classes
*/
async getCollapseIconClass(): Promise<string> {
const icon = await this.getCollapseIcon()
return (await icon.getAttribute('class')) || ''
}

/**
* Check if the collapse button is visible
*/
async isCollapseButtonVisible(): Promise<boolean> {
const button = await this.getCollapseButton()
return await button.isVisible()
}

/**
* Get the node's body/content element
*/
async getBody(): Promise<Locator> {
const nodeId = this.nodeRef.id
return this.page.locator(`[data-testid="node-body-${nodeId}"]`)
}

/**
* Check if the node body is visible (not collapsed)
*/
async isBodyVisible(): Promise<boolean> {
const body = await this.getBody()
return await body.isVisible()
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
134 changes: 134 additions & 0 deletions browser_tests/tests/vueNodes/NodeHeader.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import {
comfyExpect as expect,
comfyPageFixture as test
} from '../../fixtures/ComfyPage'
import { VueNodeFixture } from '../../fixtures/utils/vueNodeFixtures'

test.describe('NodeHeader', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Enabled')
await comfyPage.setSetting('Comfy.Graph.CanvasMenu', false)
await comfyPage.setSetting('Comfy.EnableTooltips', true)
await comfyPage.setSetting('Comfy.VueNodes.Enabled', true)
await comfyPage.setup()
})

test('displays node title', async ({ comfyPage }) => {
// Get the KSampler node from the default workflow
const nodes = await comfyPage.getNodeRefsByType('KSampler')
expect(nodes.length).toBeGreaterThanOrEqual(1)

const node = nodes[0]
const vueNode = new VueNodeFixture(node, comfyPage.page)

const title = await vueNode.getTitle()
expect(title).toBe('KSampler')

// Verify title is visible in the header
const header = await vueNode.getHeader()
await expect(header).toContainText('KSampler')
})

test('allows title renaming', async ({ comfyPage }) => {
const nodes = await comfyPage.getNodeRefsByType('KSampler')
const node = nodes[0]
const vueNode = new VueNodeFixture(node, comfyPage.page)

// Test renaming with Enter
await vueNode.setTitle('My Custom Sampler')
const newTitle = await vueNode.getTitle()
expect(newTitle).toBe('My Custom Sampler')

// Verify the title is displayed
const header = await vueNode.getHeader()
await expect(header).toContainText('My Custom Sampler')

// Test cancel with Escape
const titleElement = await vueNode.getTitleElement()
await titleElement.dblclick()
await comfyPage.nextFrame()

// Type a different value but cancel
const input = (await vueNode.getHeader()).locator(
'[data-testid="node-title-input"]'
)
await input.fill('This Should Be Cancelled')
await input.press('Escape')
await comfyPage.nextFrame()

// Title should remain as the previously saved value
const titleAfterCancel = await vueNode.getTitle()
expect(titleAfterCancel).toBe('My Custom Sampler')
})

test('handles node collapsing', async ({ comfyPage }) => {
// Get the KSampler node from the default workflow
const nodes = await comfyPage.getNodeRefsByType('KSampler')
const node = nodes[0]
const vueNode = new VueNodeFixture(node, comfyPage.page)

// Initially should not be collapsed
expect(await node.isCollapsed()).toBe(false)
const body = await vueNode.getBody()
await expect(body).toBeVisible()

// Collapse the node
await vueNode.toggleCollapse()
expect(await node.isCollapsed()).toBe(true)

// Verify node content is hidden
const collapsedSize = await node.getSize()
await expect(body).not.toBeVisible()

// Expand again
await vueNode.toggleCollapse()
expect(await node.isCollapsed()).toBe(false)
await expect(body).toBeVisible()

// Size should be restored
const expandedSize = await node.getSize()
expect(expandedSize.height).toBeGreaterThanOrEqual(collapsedSize.height)
})

test('shows collapse/expand icon state', async ({ comfyPage }) => {
const nodes = await comfyPage.getNodeRefsByType('KSampler')
const node = nodes[0]
const vueNode = new VueNodeFixture(node, comfyPage.page)

// Check initial expanded state icon
let iconClass = await vueNode.getCollapseIconClass()
expect(iconClass).toContain('pi-chevron-down')

// Collapse and check icon
await vueNode.toggleCollapse()
iconClass = await vueNode.getCollapseIconClass()
expect(iconClass).toContain('pi-chevron-right')

// Expand and check icon
await vueNode.toggleCollapse()
iconClass = await vueNode.getCollapseIconClass()
expect(iconClass).toContain('pi-chevron-down')
})

test('preserves title when collapsing/expanding', async ({ comfyPage }) => {
const nodes = await comfyPage.getNodeRefsByType('KSampler')
const node = nodes[0]
const vueNode = new VueNodeFixture(node, comfyPage.page)

// Set custom title
await vueNode.setTitle('Test Sampler')
expect(await vueNode.getTitle()).toBe('Test Sampler')

// Collapse
await vueNode.toggleCollapse()
expect(await vueNode.getTitle()).toBe('Test Sampler')

// Expand
await vueNode.toggleCollapse()
expect(await vueNode.getTitle()).toBe('Test Sampler')

// Verify title is still displayed
const header = await vueNode.getHeader()
await expect(header).toContainText('Test Sampler')
})
})
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,8 @@
"@xterm/xterm": "^5.5.0",
"algoliasearch": "^5.21.0",
"axios": "^1.8.2",
"chart.js": "^4.5.0",
"clsx": "^2.1.1",
"dompurify": "^3.2.5",
"dotenv": "^16.4.5",
"es-toolkit": "^1.39.9",
Expand All @@ -140,12 +142,14 @@
"primeicons": "^7.0.0",
"primevue": "^4.2.5",
"semver": "^7.7.2",
"tailwind-merge": "^3.3.1",
"three": "^0.170.0",
"tiptap-markdown": "^0.8.10",
"vue": "^3.5.13",
"vue-i18n": "^9.14.3",
"vue-router": "^4.4.3",
"vuefire": "^3.2.1",
"yjs": "^13.6.27",
"zod": "^3.23.8",
"zod-validation-error": "^3.3.0"
}
Expand Down
Loading