mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-28 02:02:08 +00:00
feat: Enable double-click on subgraph slot labels for renaming (#4833)
This commit is contained in:
@@ -786,6 +786,164 @@ export class ComfyPage {
|
|||||||
await this.nextFrame()
|
await this.nextFrame()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Core helper method for interacting with subgraph I/O slots.
|
||||||
|
* Handles both input/output slots and both right-click/double-click actions.
|
||||||
|
*
|
||||||
|
* @param slotType - 'input' or 'output'
|
||||||
|
* @param action - 'rightClick' or 'doubleClick'
|
||||||
|
* @param slotName - Optional specific slot name to target
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private async interactWithSubgraphSlot(
|
||||||
|
slotType: 'input' | 'output',
|
||||||
|
action: 'rightClick' | 'doubleClick',
|
||||||
|
slotName?: string
|
||||||
|
): Promise<void> {
|
||||||
|
const foundSlot = await this.page.evaluate(
|
||||||
|
async (params) => {
|
||||||
|
const { slotType, action, targetSlotName } = params
|
||||||
|
const app = window['app']
|
||||||
|
const currentGraph = app.canvas.graph
|
||||||
|
|
||||||
|
// Check if we're in a subgraph
|
||||||
|
if (currentGraph.constructor.name !== 'Subgraph') {
|
||||||
|
throw new Error(
|
||||||
|
'Not in a subgraph - this method only works inside subgraphs'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the appropriate node and slots
|
||||||
|
const node =
|
||||||
|
slotType === 'input'
|
||||||
|
? currentGraph.inputNode
|
||||||
|
: currentGraph.outputNode
|
||||||
|
const slots =
|
||||||
|
slotType === 'input' ? currentGraph.inputs : currentGraph.outputs
|
||||||
|
|
||||||
|
if (!node) {
|
||||||
|
throw new Error(`No ${slotType} node found in subgraph`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!slots || slots.length === 0) {
|
||||||
|
throw new Error(`No ${slotType} slots found in subgraph`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter slots based on target name and action type
|
||||||
|
const slotsToTry = targetSlotName
|
||||||
|
? slots.filter((slot) => slot.name === targetSlotName)
|
||||||
|
: action === 'rightClick'
|
||||||
|
? slots
|
||||||
|
: [slots[0]] // Right-click tries all, double-click uses first
|
||||||
|
|
||||||
|
if (slotsToTry.length === 0) {
|
||||||
|
throw new Error(
|
||||||
|
targetSlotName
|
||||||
|
? `${slotType} slot '${targetSlotName}' not found`
|
||||||
|
: `No ${slotType} slots available to try`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle the interaction based on action type
|
||||||
|
if (action === 'rightClick') {
|
||||||
|
// Right-click: try each slot until one works
|
||||||
|
for (const slot of slotsToTry) {
|
||||||
|
if (!slot.pos) continue
|
||||||
|
|
||||||
|
const event = {
|
||||||
|
canvasX: slot.pos[0],
|
||||||
|
canvasY: slot.pos[1],
|
||||||
|
button: 2, // Right mouse button
|
||||||
|
preventDefault: () => {},
|
||||||
|
stopPropagation: () => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.onPointerDown) {
|
||||||
|
node.onPointerDown(
|
||||||
|
event,
|
||||||
|
app.canvas.pointer,
|
||||||
|
app.canvas.linkConnector
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait briefly for menu to appear
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||||
|
|
||||||
|
// Check if context menu appeared
|
||||||
|
const menuExists = document.querySelector('.litemenu-entry')
|
||||||
|
if (menuExists) {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
slotName: slot.name,
|
||||||
|
x: slot.pos[0],
|
||||||
|
y: slot.pos[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (action === 'doubleClick') {
|
||||||
|
// Double-click: use first slot with bounding rect center
|
||||||
|
const slot = slotsToTry[0]
|
||||||
|
if (!slot.boundingRect) {
|
||||||
|
throw new Error(`${slotType} slot bounding rect not found`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const rect = slot.boundingRect
|
||||||
|
const testX = rect[0] + rect[2] / 2 // x + width/2
|
||||||
|
const testY = rect[1] + rect[3] / 2 // y + height/2
|
||||||
|
|
||||||
|
const event = {
|
||||||
|
canvasX: testX,
|
||||||
|
canvasY: testY,
|
||||||
|
button: 0, // Left mouse button
|
||||||
|
preventDefault: () => {},
|
||||||
|
stopPropagation: () => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.onPointerDown) {
|
||||||
|
node.onPointerDown(
|
||||||
|
event,
|
||||||
|
app.canvas.pointer,
|
||||||
|
app.canvas.linkConnector
|
||||||
|
)
|
||||||
|
|
||||||
|
// Trigger double-click
|
||||||
|
if (app.canvas.pointer.onDoubleClick) {
|
||||||
|
app.canvas.pointer.onDoubleClick(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait briefly for dialog to appear
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 200))
|
||||||
|
|
||||||
|
return { success: true, slotName: slot.name, x: testX, y: testY }
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: false }
|
||||||
|
},
|
||||||
|
{ slotType, action, targetSlotName: slotName }
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!foundSlot.success) {
|
||||||
|
const actionText =
|
||||||
|
action === 'rightClick' ? 'open context menu for' : 'double-click'
|
||||||
|
throw new Error(
|
||||||
|
slotName
|
||||||
|
? `Could not ${actionText} ${slotType} slot '${slotName}'`
|
||||||
|
: `Could not find any ${slotType} slot to ${actionText}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for the appropriate UI element to appear
|
||||||
|
if (action === 'rightClick') {
|
||||||
|
await this.page.waitForSelector('.litemenu-entry', {
|
||||||
|
state: 'visible',
|
||||||
|
timeout: 5000
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
await this.nextFrame()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Right-clicks on a subgraph input slot to open the context menu.
|
* Right-clicks on a subgraph input slot to open the context menu.
|
||||||
* Must be called when inside a subgraph.
|
* Must be called when inside a subgraph.
|
||||||
@@ -800,93 +958,7 @@ export class ComfyPage {
|
|||||||
* @returns Promise that resolves when the context menu appears
|
* @returns Promise that resolves when the context menu appears
|
||||||
*/
|
*/
|
||||||
async rightClickSubgraphInputSlot(inputName?: string): Promise<void> {
|
async rightClickSubgraphInputSlot(inputName?: string): Promise<void> {
|
||||||
const foundSlot = await this.page.evaluate(async (targetInputName) => {
|
return this.interactWithSubgraphSlot('input', 'rightClick', inputName)
|
||||||
const app = window['app']
|
|
||||||
const currentGraph = app.canvas.graph
|
|
||||||
|
|
||||||
// Check if we're in a subgraph
|
|
||||||
if (currentGraph.constructor.name !== 'Subgraph') {
|
|
||||||
throw new Error(
|
|
||||||
'Not in a subgraph - this method only works inside subgraphs'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the input node
|
|
||||||
const inputNode = currentGraph.inputNode
|
|
||||||
if (!inputNode) {
|
|
||||||
throw new Error('No input node found in subgraph')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get available inputs
|
|
||||||
const inputs = currentGraph.inputs
|
|
||||||
if (!inputs || inputs.length === 0) {
|
|
||||||
throw new Error('No input slots found in subgraph')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter to specific input if requested
|
|
||||||
const inputsToTry = targetInputName
|
|
||||||
? inputs.filter((inp) => inp.name === targetInputName)
|
|
||||||
: inputs
|
|
||||||
|
|
||||||
if (inputsToTry.length === 0) {
|
|
||||||
throw new Error(
|
|
||||||
targetInputName
|
|
||||||
? `Input slot '${targetInputName}' not found`
|
|
||||||
: 'No input slots available to try'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try right-clicking on each input slot position until one works
|
|
||||||
for (const input of inputsToTry) {
|
|
||||||
if (!input.pos) continue
|
|
||||||
|
|
||||||
const testX = input.pos[0]
|
|
||||||
const testY = input.pos[1]
|
|
||||||
|
|
||||||
// Create a right-click event at the input slot position
|
|
||||||
const rightClickEvent = {
|
|
||||||
canvasX: testX,
|
|
||||||
canvasY: testY,
|
|
||||||
button: 2, // Right mouse button
|
|
||||||
preventDefault: () => {},
|
|
||||||
stopPropagation: () => {}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Trigger the input node's right-click handler
|
|
||||||
if (inputNode.onPointerDown) {
|
|
||||||
inputNode.onPointerDown(
|
|
||||||
rightClickEvent,
|
|
||||||
app.canvas.pointer,
|
|
||||||
app.canvas.linkConnector
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait briefly for menu to appear
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
||||||
|
|
||||||
// Check if litegraph context menu appeared
|
|
||||||
const menuExists = document.querySelector('.litemenu-entry')
|
|
||||||
if (menuExists) {
|
|
||||||
return { success: true, inputName: input.name, x: testX, y: testY }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { success: false }
|
|
||||||
}, inputName)
|
|
||||||
|
|
||||||
if (!foundSlot.success) {
|
|
||||||
throw new Error(
|
|
||||||
inputName
|
|
||||||
? `Could not open context menu for input slot '${inputName}'`
|
|
||||||
: 'Could not find any input slot position to right-click'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for the litegraph context menu to be visible
|
|
||||||
await this.page.waitForSelector('.litemenu-entry', {
|
|
||||||
state: 'visible',
|
|
||||||
timeout: 5000
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -900,93 +972,31 @@ export class ComfyPage {
|
|||||||
* @returns Promise that resolves when the context menu appears
|
* @returns Promise that resolves when the context menu appears
|
||||||
*/
|
*/
|
||||||
async rightClickSubgraphOutputSlot(outputName?: string): Promise<void> {
|
async rightClickSubgraphOutputSlot(outputName?: string): Promise<void> {
|
||||||
const foundSlot = await this.page.evaluate(async (targetOutputName) => {
|
return this.interactWithSubgraphSlot('output', 'rightClick', outputName)
|
||||||
const app = window['app']
|
}
|
||||||
const currentGraph = app.canvas.graph
|
|
||||||
|
|
||||||
// Check if we're in a subgraph
|
/**
|
||||||
if (currentGraph.constructor.name !== 'Subgraph') {
|
* Double-clicks on a subgraph input slot to rename it.
|
||||||
throw new Error(
|
* Must be called when inside a subgraph.
|
||||||
'Not in a subgraph - this method only works inside subgraphs'
|
*
|
||||||
)
|
* @param inputName Optional name of the specific input slot to target (e.g., 'text').
|
||||||
}
|
* If not provided, tries the first available input slot.
|
||||||
|
* @returns Promise that resolves when the rename dialog appears
|
||||||
|
*/
|
||||||
|
async doubleClickSubgraphInputSlot(inputName?: string): Promise<void> {
|
||||||
|
return this.interactWithSubgraphSlot('input', 'doubleClick', inputName)
|
||||||
|
}
|
||||||
|
|
||||||
// Get the output node
|
/**
|
||||||
const outputNode = currentGraph.outputNode
|
* Double-clicks on a subgraph output slot to rename it.
|
||||||
if (!outputNode) {
|
* Must be called when inside a subgraph.
|
||||||
throw new Error('No output node found in subgraph')
|
*
|
||||||
}
|
* @param outputName Optional name of the specific output slot to target.
|
||||||
|
* If not provided, tries the first available output slot.
|
||||||
// Get available outputs
|
* @returns Promise that resolves when the rename dialog appears
|
||||||
const outputs = currentGraph.outputs
|
*/
|
||||||
if (!outputs || outputs.length === 0) {
|
async doubleClickSubgraphOutputSlot(outputName?: string): Promise<void> {
|
||||||
throw new Error('No output slots found in subgraph')
|
return this.interactWithSubgraphSlot('output', 'doubleClick', outputName)
|
||||||
}
|
|
||||||
|
|
||||||
// Filter to specific output if requested
|
|
||||||
const outputsToTry = targetOutputName
|
|
||||||
? outputs.filter((out) => out.name === targetOutputName)
|
|
||||||
: outputs
|
|
||||||
|
|
||||||
if (outputsToTry.length === 0) {
|
|
||||||
throw new Error(
|
|
||||||
targetOutputName
|
|
||||||
? `Output slot '${targetOutputName}' not found`
|
|
||||||
: 'No output slots available to try'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try right-clicking on each output slot position until one works
|
|
||||||
for (const output of outputsToTry) {
|
|
||||||
if (!output.pos) continue
|
|
||||||
|
|
||||||
const testX = output.pos[0]
|
|
||||||
const testY = output.pos[1]
|
|
||||||
|
|
||||||
// Create a right-click event at the output slot position
|
|
||||||
const rightClickEvent = {
|
|
||||||
canvasX: testX,
|
|
||||||
canvasY: testY,
|
|
||||||
button: 2, // Right mouse button
|
|
||||||
preventDefault: () => {},
|
|
||||||
stopPropagation: () => {}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Trigger the output node's right-click handler
|
|
||||||
if (outputNode.onPointerDown) {
|
|
||||||
outputNode.onPointerDown(
|
|
||||||
rightClickEvent,
|
|
||||||
app.canvas.pointer,
|
|
||||||
app.canvas.linkConnector
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait briefly for menu to appear
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
||||||
|
|
||||||
// Check if litegraph context menu appeared
|
|
||||||
const menuExists = document.querySelector('.litemenu-entry')
|
|
||||||
if (menuExists) {
|
|
||||||
return { success: true, outputName: output.name, x: testX, y: testY }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { success: false }
|
|
||||||
}, outputName)
|
|
||||||
|
|
||||||
if (!foundSlot.success) {
|
|
||||||
throw new Error(
|
|
||||||
outputName
|
|
||||||
? `Could not open context menu for output slot '${outputName}'`
|
|
||||||
: 'Could not find any output slot position to right-click'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for the litegraph context menu to be visible
|
|
||||||
await this.page.waitForSelector('.litemenu-entry', {
|
|
||||||
state: 'visible',
|
|
||||||
timeout: 5000
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -155,6 +155,182 @@ test.describe('Subgraph Operations', () => {
|
|||||||
expect(newInputName).toBe(RENAMED_INPUT_NAME)
|
expect(newInputName).toBe(RENAMED_INPUT_NAME)
|
||||||
expect(newInputName).not.toBe(initialInputLabel)
|
expect(newInputName).not.toBe(initialInputLabel)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('Can rename input slots via double-click', async ({ comfyPage }) => {
|
||||||
|
await comfyPage.loadWorkflow('basic-subgraph')
|
||||||
|
|
||||||
|
const subgraphNode = await comfyPage.getNodeRefById('2')
|
||||||
|
await subgraphNode.navigateIntoSubgraph()
|
||||||
|
|
||||||
|
const initialInputLabel = await comfyPage.page.evaluate(() => {
|
||||||
|
const graph = window['app'].canvas.graph
|
||||||
|
return graph.inputs?.[0]?.label || null
|
||||||
|
})
|
||||||
|
|
||||||
|
await comfyPage.doubleClickSubgraphInputSlot(initialInputLabel)
|
||||||
|
|
||||||
|
await comfyPage.page.waitForSelector(SELECTORS.promptDialog, {
|
||||||
|
state: 'visible'
|
||||||
|
})
|
||||||
|
await comfyPage.page.fill(SELECTORS.promptDialog, RENAMED_INPUT_NAME)
|
||||||
|
await comfyPage.page.keyboard.press('Enter')
|
||||||
|
|
||||||
|
// Force re-render
|
||||||
|
await comfyPage.canvas.click({ position: { x: 100, y: 100 } })
|
||||||
|
await comfyPage.nextFrame()
|
||||||
|
|
||||||
|
const newInputName = await comfyPage.page.evaluate(() => {
|
||||||
|
const graph = window['app'].canvas.graph
|
||||||
|
return graph.inputs?.[0]?.label || null
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(newInputName).toBe(RENAMED_INPUT_NAME)
|
||||||
|
expect(newInputName).not.toBe(initialInputLabel)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Can rename output slots via double-click', async ({ comfyPage }) => {
|
||||||
|
await comfyPage.loadWorkflow('basic-subgraph')
|
||||||
|
|
||||||
|
const subgraphNode = await comfyPage.getNodeRefById('2')
|
||||||
|
await subgraphNode.navigateIntoSubgraph()
|
||||||
|
|
||||||
|
const initialOutputLabel = await comfyPage.page.evaluate(() => {
|
||||||
|
const graph = window['app'].canvas.graph
|
||||||
|
return graph.outputs?.[0]?.label || null
|
||||||
|
})
|
||||||
|
|
||||||
|
await comfyPage.doubleClickSubgraphOutputSlot(initialOutputLabel)
|
||||||
|
|
||||||
|
await comfyPage.page.waitForSelector(SELECTORS.promptDialog, {
|
||||||
|
state: 'visible'
|
||||||
|
})
|
||||||
|
const renamedOutputName = 'renamed_output'
|
||||||
|
await comfyPage.page.fill(SELECTORS.promptDialog, renamedOutputName)
|
||||||
|
await comfyPage.page.keyboard.press('Enter')
|
||||||
|
|
||||||
|
// Force re-render
|
||||||
|
await comfyPage.canvas.click({ position: { x: 100, y: 100 } })
|
||||||
|
await comfyPage.nextFrame()
|
||||||
|
|
||||||
|
const newOutputName = await comfyPage.page.evaluate(() => {
|
||||||
|
const graph = window['app'].canvas.graph
|
||||||
|
return graph.outputs?.[0]?.label || null
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(newOutputName).toBe(renamedOutputName)
|
||||||
|
expect(newOutputName).not.toBe(initialOutputLabel)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Right-click context menu still works alongside double-click', async ({
|
||||||
|
comfyPage
|
||||||
|
}) => {
|
||||||
|
await comfyPage.loadWorkflow('basic-subgraph')
|
||||||
|
|
||||||
|
const subgraphNode = await comfyPage.getNodeRefById('2')
|
||||||
|
await subgraphNode.navigateIntoSubgraph()
|
||||||
|
|
||||||
|
const initialInputLabel = await comfyPage.page.evaluate(() => {
|
||||||
|
const graph = window['app'].canvas.graph
|
||||||
|
return graph.inputs?.[0]?.label || null
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test that right-click still works for renaming
|
||||||
|
await comfyPage.rightClickSubgraphInputSlot(initialInputLabel)
|
||||||
|
await comfyPage.clickLitegraphContextMenuItem('Rename Slot')
|
||||||
|
|
||||||
|
await comfyPage.page.waitForSelector(SELECTORS.promptDialog, {
|
||||||
|
state: 'visible'
|
||||||
|
})
|
||||||
|
const rightClickRenamedName = 'right_click_renamed'
|
||||||
|
await comfyPage.page.fill(SELECTORS.promptDialog, rightClickRenamedName)
|
||||||
|
await comfyPage.page.keyboard.press('Enter')
|
||||||
|
|
||||||
|
// Force re-render
|
||||||
|
await comfyPage.canvas.click({ position: { x: 100, y: 100 } })
|
||||||
|
await comfyPage.nextFrame()
|
||||||
|
|
||||||
|
const newInputName = await comfyPage.page.evaluate(() => {
|
||||||
|
const graph = window['app'].canvas.graph
|
||||||
|
return graph.inputs?.[0]?.label || null
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(newInputName).toBe(rightClickRenamedName)
|
||||||
|
expect(newInputName).not.toBe(initialInputLabel)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Can double-click on slot label text to rename', async ({
|
||||||
|
comfyPage
|
||||||
|
}) => {
|
||||||
|
await comfyPage.loadWorkflow('basic-subgraph')
|
||||||
|
|
||||||
|
const subgraphNode = await comfyPage.getNodeRefById('2')
|
||||||
|
await subgraphNode.navigateIntoSubgraph()
|
||||||
|
|
||||||
|
const initialInputLabel = await comfyPage.page.evaluate(() => {
|
||||||
|
const graph = window['app'].canvas.graph
|
||||||
|
return graph.inputs?.[0]?.label || null
|
||||||
|
})
|
||||||
|
|
||||||
|
// Use direct pointer event approach to double-click on label
|
||||||
|
await comfyPage.page.evaluate(() => {
|
||||||
|
const app = window['app']
|
||||||
|
const graph = app.canvas.graph
|
||||||
|
const input = graph.inputs?.[0]
|
||||||
|
|
||||||
|
if (!input?.labelPos) {
|
||||||
|
throw new Error('Could not get label position for testing')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use labelPos for more precise clicking on the text
|
||||||
|
const testX = input.labelPos[0]
|
||||||
|
const testY = input.labelPos[1]
|
||||||
|
|
||||||
|
const leftClickEvent = {
|
||||||
|
canvasX: testX,
|
||||||
|
canvasY: testY,
|
||||||
|
button: 0, // Left mouse button
|
||||||
|
preventDefault: () => {},
|
||||||
|
stopPropagation: () => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const inputNode = graph.inputNode
|
||||||
|
if (inputNode?.onPointerDown) {
|
||||||
|
inputNode.onPointerDown(
|
||||||
|
leftClickEvent,
|
||||||
|
app.canvas.pointer,
|
||||||
|
app.canvas.linkConnector
|
||||||
|
)
|
||||||
|
|
||||||
|
// Trigger double-click if pointer has the handler
|
||||||
|
if (app.canvas.pointer.onDoubleClick) {
|
||||||
|
app.canvas.pointer.onDoubleClick(leftClickEvent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Wait for dialog to appear
|
||||||
|
await comfyPage.page.waitForTimeout(200)
|
||||||
|
await comfyPage.nextFrame()
|
||||||
|
|
||||||
|
await comfyPage.page.waitForSelector(SELECTORS.promptDialog, {
|
||||||
|
state: 'visible'
|
||||||
|
})
|
||||||
|
const labelClickRenamedName = 'label_click_renamed'
|
||||||
|
await comfyPage.page.fill(SELECTORS.promptDialog, labelClickRenamedName)
|
||||||
|
await comfyPage.page.keyboard.press('Enter')
|
||||||
|
|
||||||
|
// Force re-render
|
||||||
|
await comfyPage.canvas.click({ position: { x: 100, y: 100 } })
|
||||||
|
await comfyPage.nextFrame()
|
||||||
|
|
||||||
|
const newInputName = await comfyPage.page.evaluate(() => {
|
||||||
|
const graph = window['app'].canvas.graph
|
||||||
|
return graph.inputs?.[0]?.label || null
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(newInputName).toBe(labelClickRenamedName)
|
||||||
|
expect(newInputName).not.toBe(initialInputLabel)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
test.describe('Subgraph Creation and Deletion', () => {
|
test.describe('Subgraph Creation and Deletion', () => {
|
||||||
|
|||||||
@@ -170,6 +170,21 @@ export abstract class SubgraphIONodeBase<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles double-click on an IO slot to rename it.
|
||||||
|
* @param slot The slot that was double-clicked.
|
||||||
|
* @param event The event that triggered the double-click.
|
||||||
|
*/
|
||||||
|
protected handleSlotDoubleClick(
|
||||||
|
slot: TSlot,
|
||||||
|
event: CanvasPointerEvent
|
||||||
|
): void {
|
||||||
|
// Only allow renaming non-empty slots
|
||||||
|
if (slot !== this.emptySlot) {
|
||||||
|
this.#promptForSlotRename(slot, event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shows the context menu for an IO slot.
|
* Shows the context menu for an IO slot.
|
||||||
* @param slot The slot to show the context menu for.
|
* @param slot The slot to show the context menu for.
|
||||||
@@ -239,16 +254,7 @@ export abstract class SubgraphIONodeBase<
|
|||||||
// Rename the slot
|
// Rename the slot
|
||||||
case 'rename':
|
case 'rename':
|
||||||
if (slot !== this.emptySlot) {
|
if (slot !== this.emptySlot) {
|
||||||
this.subgraph.canvasAction((c) =>
|
this.#promptForSlotRename(slot, event)
|
||||||
c.prompt(
|
|
||||||
'Slot name',
|
|
||||||
slot.name,
|
|
||||||
(newName: string) => {
|
|
||||||
if (newName) this.renameSlot(slot, newName)
|
|
||||||
},
|
|
||||||
event
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -256,6 +262,24 @@ export abstract class SubgraphIONodeBase<
|
|||||||
this.subgraph.setDirtyCanvas(true)
|
this.subgraph.setDirtyCanvas(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prompts the user to rename a slot.
|
||||||
|
* @param slot The slot to rename.
|
||||||
|
* @param event The event that triggered the rename.
|
||||||
|
*/
|
||||||
|
#promptForSlotRename(slot: TSlot, event: CanvasPointerEvent): void {
|
||||||
|
this.subgraph.canvasAction((c) =>
|
||||||
|
c.prompt(
|
||||||
|
'Slot name',
|
||||||
|
slot.name,
|
||||||
|
(newName: string) => {
|
||||||
|
if (newName) this.renameSlot(slot, newName)
|
||||||
|
},
|
||||||
|
event
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/** Arrange the slots in this node. */
|
/** Arrange the slots in this node. */
|
||||||
arrange(): void {
|
arrange(): void {
|
||||||
const { minWidth, roundedRadius } = SubgraphIONodeBase
|
const { minWidth, roundedRadius } = SubgraphIONodeBase
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { LLink } from '@/lib/litegraph/src/LLink'
|
|||||||
import type { RerouteId } from '@/lib/litegraph/src/Reroute'
|
import type { RerouteId } from '@/lib/litegraph/src/Reroute'
|
||||||
import type { LinkConnector } from '@/lib/litegraph/src/canvas/LinkConnector'
|
import type { LinkConnector } from '@/lib/litegraph/src/canvas/LinkConnector'
|
||||||
import { SUBGRAPH_INPUT_ID } from '@/lib/litegraph/src/constants'
|
import { SUBGRAPH_INPUT_ID } from '@/lib/litegraph/src/constants'
|
||||||
import { Rectangle } from '@/lib/litegraph/src/infrastructure/Rectangle'
|
|
||||||
import type {
|
import type {
|
||||||
DefaultConnectionColors,
|
DefaultConnectionColors,
|
||||||
INodeInputSlot,
|
INodeInputSlot,
|
||||||
@@ -51,18 +50,17 @@ export class SubgraphInputNode
|
|||||||
// Left-click handling for dragging connections
|
// Left-click handling for dragging connections
|
||||||
if (e.button === 0) {
|
if (e.button === 0) {
|
||||||
for (const slot of this.allSlots) {
|
for (const slot of this.allSlots) {
|
||||||
const slotBounds = Rectangle.fromCentre(
|
// Check if click is within the full slot area (including label)
|
||||||
slot.pos,
|
if (slot.boundingRect.containsXy(e.canvasX, e.canvasY)) {
|
||||||
slot.boundingRect.height
|
|
||||||
)
|
|
||||||
|
|
||||||
if (slotBounds.containsXy(e.canvasX, e.canvasY)) {
|
|
||||||
pointer.onDragStart = () => {
|
pointer.onDragStart = () => {
|
||||||
linkConnector.dragNewFromSubgraphInput(this.subgraph, this, slot)
|
linkConnector.dragNewFromSubgraphInput(this.subgraph, this, slot)
|
||||||
}
|
}
|
||||||
pointer.onDragEnd = (eUp) => {
|
pointer.onDragEnd = (eUp) => {
|
||||||
linkConnector.dropLinks(this.subgraph, eUp)
|
linkConnector.dropLinks(this.subgraph, eUp)
|
||||||
}
|
}
|
||||||
|
pointer.onDoubleClick = () => {
|
||||||
|
this.handleSlotDoubleClick(slot, e)
|
||||||
|
}
|
||||||
pointer.finally = () => {
|
pointer.finally = () => {
|
||||||
linkConnector.reset(true)
|
linkConnector.reset(true)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import type { LLink } from '@/lib/litegraph/src/LLink'
|
|||||||
import type { RerouteId } from '@/lib/litegraph/src/Reroute'
|
import type { RerouteId } from '@/lib/litegraph/src/Reroute'
|
||||||
import type { LinkConnector } from '@/lib/litegraph/src/canvas/LinkConnector'
|
import type { LinkConnector } from '@/lib/litegraph/src/canvas/LinkConnector'
|
||||||
import { SUBGRAPH_OUTPUT_ID } from '@/lib/litegraph/src/constants'
|
import { SUBGRAPH_OUTPUT_ID } from '@/lib/litegraph/src/constants'
|
||||||
import { Rectangle } from '@/lib/litegraph/src/infrastructure/Rectangle'
|
|
||||||
import type {
|
import type {
|
||||||
DefaultConnectionColors,
|
DefaultConnectionColors,
|
||||||
INodeInputSlot,
|
INodeInputSlot,
|
||||||
@@ -51,18 +50,17 @@ export class SubgraphOutputNode
|
|||||||
// Left-click handling for dragging connections
|
// Left-click handling for dragging connections
|
||||||
if (e.button === 0) {
|
if (e.button === 0) {
|
||||||
for (const slot of this.allSlots) {
|
for (const slot of this.allSlots) {
|
||||||
const slotBounds = Rectangle.fromCentre(
|
// Check if click is within the full slot area (including label)
|
||||||
slot.pos,
|
if (slot.boundingRect.containsXy(e.canvasX, e.canvasY)) {
|
||||||
slot.boundingRect.height
|
|
||||||
)
|
|
||||||
|
|
||||||
if (slotBounds.containsXy(e.canvasX, e.canvasY)) {
|
|
||||||
pointer.onDragStart = () => {
|
pointer.onDragStart = () => {
|
||||||
linkConnector.dragNewFromSubgraphOutput(this.subgraph, this, slot)
|
linkConnector.dragNewFromSubgraphOutput(this.subgraph, this, slot)
|
||||||
}
|
}
|
||||||
pointer.onDragEnd = (eUp) => {
|
pointer.onDragEnd = (eUp) => {
|
||||||
linkConnector.dropLinks(this.subgraph, eUp)
|
linkConnector.dropLinks(this.subgraph, eUp)
|
||||||
}
|
}
|
||||||
|
pointer.onDoubleClick = () => {
|
||||||
|
this.handleSlotDoubleClick(slot, e)
|
||||||
|
}
|
||||||
pointer.finally = () => {
|
pointer.finally = () => {
|
||||||
linkConnector.reset(true)
|
linkConnector.reset(true)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user