Merge branch 'main' into glary/oxlint-func-style

This commit is contained in:
Alexander Brown
2026-05-20 11:47:23 -07:00
committed by GitHub
8 changed files with 91 additions and 7 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 55 KiB

View File

@@ -0,0 +1,41 @@
import {
comfyPageFixture as test,
comfyExpect as expect
} from '@e2e/fixtures/ComfyPage'
test('Price badge displays on subgraphs @vue-nodes', async ({ comfyPage }) => {
const apiNodeName = 'Node With Price Badge'
await comfyPage.settings.setSetting('Comfy.NodeSearchBoxImpl', 'v1 (legacy)')
const priceBadge = comfyPage.page.locator('.lg-node-header i + span')
const apiNode = comfyPage.vueNodes.getNodeByTitle(apiNodeName)
await comfyPage.menu.topbar.newWorkflowButton.click()
await comfyPage.nextFrame()
await comfyPage.page.mouse.dblclick(500, 500, { delay: 5 })
await comfyPage.searchBox.fillAndSelectFirstNode(apiNodeName)
await expect(comfyPage.searchBox.input).toBeHidden()
await expect(apiNode, 'Add partner node').toBeVisible()
await expect(apiNode.locator(priceBadge), 'Has price badge').toBeVisible()
await comfyPage.contextMenu
.openForVueNode(apiNode)
.then((m) => m.clickMenuItemExact('Convert to Subgraph'))
const subgraphNode = comfyPage.vueNodes.getNodeByTitle('New Subgraph')
await expect(subgraphNode, 'Convert to Subgraph').toBeVisible()
const nodePrice = subgraphNode.locator(priceBadge)
await expect(nodePrice, 'subgraphNode has price badge').toBeVisible()
const initialPrice = Number(await nodePrice.innerText())
await comfyPage.subgraph.editor.togglePromotion(subgraphNode, {
nodeName: apiNodeName,
widgetName: 'price',
toState: true
})
await comfyPage.vueNodes.selectComboOption('New Subgraph', 'price', '2x')
await expect(nodePrice, 'Price is reactive').toHaveText(
String(initialPrice * 2)
)
})

View File

@@ -12,7 +12,7 @@
</span>
<span
v-if="rest"
class="-ml-2.5 max-w-max min-w-0 grow basis-0 truncate rounded-r-full bg-component-node-widget-background"
class="-ml-2.5 flex h-5 max-w-max min-w-0 grow basis-0 items-center truncate rounded-r-full bg-component-node-widget-background text-xs"
>
<span class="pr-2" v-text="rest" />
</span>

View File

@@ -48,6 +48,8 @@ export interface WidgetSlotMetadata {
type: string
}
type Badges = (LGraphBadge | (() => LGraphBadge))[]
/**
* Minimal render-specific widget data extracted from LiteGraph widgets.
* Value and metadata (label, hidden, disabled, etc.) are accessed via widgetValueStore.
@@ -107,7 +109,7 @@ export interface VueNodeData {
title: string
type: string
apiNode?: boolean
badges?: (LGraphBadge | (() => LGraphBadge))[]
badges?: Badges
bgcolor?: string
color?: string
flags?: {
@@ -786,6 +788,12 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager {
showAdvanced: Boolean(propertyEvent.newValue)
})
break
case 'badges':
vueNodeData.set(nodeId, {
...currentData,
badges: propertyEvent.newValue as Badges
})
break
}
}
},

View File

@@ -628,9 +628,9 @@ describe('useNodePricing', () => {
getNodeDisplayPrice(node)
await new Promise((r) => setTimeout(r, 50))
// VueNodes path bumps per-node ref instead of the global tick.
// VueNodes path bumps per-node ref and the global tick.
expect(getNodeRevisionRef(node.id).value).toBeGreaterThan(revBefore)
expect(pricingRevision.value).toBe(tickBefore)
expect(pricingRevision.value).toBeGreaterThan(tickBefore)
} finally {
LiteGraph.vueNodesMode = false
}

View File

@@ -524,10 +524,8 @@ function scheduleEvaluation(
if (LiteGraph.vueNodesMode) {
// VueNodes mode: bump per-node revision (only this node re-renders)
getNodeRevisionRef(node.id).value++
} else {
// Nodes 1.0 mode: bump global tick to trigger setDirtyCanvas
pricingTick.value++
}
pricingTick.value++
})
inflight.set(node, { sig, promise })

View File

@@ -18,6 +18,15 @@ export function usePriceBadge() {
} else {
node.badges.push(...newBadges)
}
const graph = node.graph
if (!graph) return
graph.trigger('node:property:changed', {
type: 'node:property:changed',
nodeId: node.id,
property: 'badges',
oldValue: node.badges,
newValue: node.badges
})
}
function collectCreditsBadges(
graph: LGraph,

View File

@@ -2,6 +2,8 @@ from __future__ import annotations
import time
from comfy_api.v0_0_2 import IO
class LongComboDropdown:
@classmethod
@@ -317,6 +319,30 @@ class NodeWithLegacyWidget:
def node_with_legacy_widget(self):
return ()
class NodeWithPriceBadge(IO.ComfyNode):
@classmethod
def define_schema(cls):
return IO.Schema(
node_id="DevToolsNodeWithPriceBadge",
display_name="Node With Price Badge",
description="An API node with a price badge",
inputs=[IO.Combo.Input("price", options=["1x", "2x", "3x"])],
is_api_node=True,
price_badge=IO.PriceBadge(
depends_on=IO.PriceBadgeDepends(widgets=["price"]),
expr="""
(
$p := widgets.price;
{"type":"usd","usd": $contains($p, "2x") ? 2 : $contains($p, "3x") ? 3 : 1}
)
""",
),
)
@classmethod
async def execute(cls, price):
return IO.NodeOutput()
NODE_CLASS_MAPPINGS = {
"DevToolsLongComboDropdown": LongComboDropdown,
@@ -334,6 +360,7 @@ NODE_CLASS_MAPPINGS = {
"DevToolsNodeWithValidation": NodeWithValidation,
"DevToolsNodeWithV2ComboInput": NodeWithV2ComboInput,
"DevToolsNodeWithLegacyWidget": NodeWithLegacyWidget,
"DevToolsNodeWithPriceBadge": NodeWithPriceBadge,
}
NODE_DISPLAY_NAME_MAPPINGS = {
@@ -352,6 +379,7 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"DevToolsNodeWithValidation": "Node With Validation",
"DevToolsNodeWithV2ComboInput": "Node With V2 Combo Input",
"DevToolsNodeWithLegacyWidget": "Node With Legacy Widget",
"DevToolsNodeWithPriceBadge": "Node With Price Badge",
}
__all__ = [