From a70d69cbd26be18ef0626e75b9b32870a50f1902 Mon Sep 17 00:00:00 2001 From: Christian Byrne Date: Sat, 12 Jul 2025 14:37:48 -0700 Subject: [PATCH] [fix] Sync subgraph node title changes with breadcrumb navigation (#4394) --- browser_tests/assets/nested-subgraph.json | 716 ++++++++++++++++++ .../tests/subgraphBreadcrumb.spec.ts | 82 ++ src/components/graph/TitleEditor.vue | 10 +- 3 files changed, 807 insertions(+), 1 deletion(-) create mode 100644 browser_tests/assets/nested-subgraph.json create mode 100644 browser_tests/tests/subgraphBreadcrumb.spec.ts diff --git a/browser_tests/assets/nested-subgraph.json b/browser_tests/assets/nested-subgraph.json new file mode 100644 index 0000000000..d65e0c29c1 --- /dev/null +++ b/browser_tests/assets/nested-subgraph.json @@ -0,0 +1,716 @@ +{ + "id": "976d6e9a-927d-42db-abd4-96bfc0ecf8d9", + "revision": 0, + "last_node_id": 10, + "last_link_id": 11, + "nodes": [ + { + "id": 10, + "type": "8beb610f-ddd1-4489-ae0d-2f732a4042ae", + "pos": [ + 532, + 412.5 + ], + "size": [ + 140, + 46 + ], + "flags": {}, + "order": 0, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "LATENT", + "type": "LATENT", + "links": [ + 10 + ] + }, + { + "name": "VAE", + "type": "VAE", + "links": [ + 11 + ] + } + ], + "title": "subgraph 2", + "properties": {}, + "widgets_values": [] + }, + { + "id": 8, + "type": "VAEDecode", + "pos": [ + 758.2109985351562, + 398.3681335449219 + ], + "size": [ + 210, + 46 + ], + "flags": {}, + "order": 1, + "mode": 0, + "inputs": [ + { + "name": "samples", + "type": "LATENT", + "link": 10 + }, + { + "name": "vae", + "type": "VAE", + "link": 11 + } + ], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "slot_index": 0, + "links": [ + 9 + ] + } + ], + "properties": { + "Node name for S&R": "VAEDecode" + }, + "widgets_values": [] + }, + { + "id": 9, + "type": "SaveImage", + "pos": [ + 1028.9615478515625, + 381.83746337890625 + ], + "size": [ + 210, + 270 + ], + "flags": {}, + "order": 2, + "mode": 0, + "inputs": [ + { + "name": "images", + "type": "IMAGE", + "link": 9 + } + ], + "outputs": [], + "properties": {}, + "widgets_values": [ + "ComfyUI" + ] + } + ], + "links": [ + [ + 9, + 8, + 0, + 9, + 0, + "IMAGE" + ], + [ + 10, + 10, + 0, + 8, + 0, + "LATENT" + ], + [ + 11, + 10, + 1, + 8, + 1, + "VAE" + ] + ], + "groups": [], + "definitions": { + "subgraphs": [ + { + "id": "8beb610f-ddd1-4489-ae0d-2f732a4042ae", + "version": 1, + "state": { + "lastGroupId": 0, + "lastNodeId": 10, + "lastLinkId": 14, + "lastRerouteId": 0 + }, + "revision": 0, + "config": {}, + "name": "subgraph 2", + "inputNode": { + "id": -10, + "bounding": [ + -154, + 415.5, + 120, + 40 + ] + }, + "outputNode": { + "id": -20, + "bounding": [ + 1238, + 395.5, + 120, + 80 + ] + }, + "inputs": [], + "outputs": [ + { + "id": "4d6c7e4e-971e-4f78-9218-9a604db53a4b", + "name": "LATENT", + "type": "LATENT", + "linkIds": [ + 7 + ], + "localized_name": "LATENT", + "pos": { + "0": 1258, + "1": 415.5 + } + }, + { + "id": "f8201d4f-7fc6-4a1b-b8c9-9f0716d9c09a", + "name": "VAE", + "type": "VAE", + "linkIds": [ + 14 + ], + "localized_name": "VAE", + "pos": { + "0": 1258, + "1": 435.5 + } + } + ], + "widgets": [], + "nodes": [ + { + "id": 6, + "type": "CLIPTextEncode", + "pos": [ + 415, + 186 + ], + "size": [ + 422.84503173828125, + 164.31304931640625 + ], + "flags": {}, + "order": 0, + "mode": 0, + "inputs": [ + { + "localized_name": "clip", + "name": "clip", + "type": "CLIP", + "link": 13 + } + ], + "outputs": [ + { + "localized_name": "CONDITIONING", + "name": "CONDITIONING", + "type": "CONDITIONING", + "slot_index": 0, + "links": [ + 4 + ] + } + ], + "properties": { + "Node name for S&R": "CLIPTextEncode" + }, + "widgets_values": [ + "beautiful scenery nature glass bottle landscape, , purple galaxy bottle," + ] + }, + { + "id": 3, + "type": "KSampler", + "pos": [ + 863, + 186 + ], + "size": [ + 315, + 262 + ], + "flags": {}, + "order": 2, + "mode": 0, + "inputs": [ + { + "localized_name": "model", + "name": "model", + "type": "MODEL", + "link": 12 + }, + { + "localized_name": "positive", + "name": "positive", + "type": "CONDITIONING", + "link": 4 + }, + { + "localized_name": "negative", + "name": "negative", + "type": "CONDITIONING", + "link": 10 + }, + { + "localized_name": "latent_image", + "name": "latent_image", + "type": "LATENT", + "link": 11 + } + ], + "outputs": [ + { + "localized_name": "LATENT", + "name": "LATENT", + "type": "LATENT", + "slot_index": 0, + "links": [ + 7 + ] + } + ], + "properties": { + "Node name for S&R": "KSampler" + }, + "widgets_values": [ + 32115495257102, + "randomize", + 20, + 8, + "euler", + "normal", + 1 + ] + }, + { + "id": 10, + "type": "dbe5763f-440b-47b4-82ac-454f1f98b0e3", + "pos": [ + 194.13900756835938, + 657.3333740234375 + ], + "size": [ + 140, + 106 + ], + "flags": {}, + "order": 1, + "mode": 0, + "inputs": [], + "outputs": [ + { + "localized_name": "CONDITIONING", + "name": "CONDITIONING", + "type": "CONDITIONING", + "links": [ + 10 + ] + }, + { + "localized_name": "LATENT", + "name": "LATENT", + "type": "LATENT", + "links": [ + 11 + ] + }, + { + "localized_name": "MODEL", + "name": "MODEL", + "type": "MODEL", + "links": [ + 12 + ] + }, + { + "localized_name": "CLIP", + "name": "CLIP", + "type": "CLIP", + "links": [ + 13 + ] + }, + { + "localized_name": "VAE", + "name": "VAE", + "type": "VAE", + "links": [ + 14 + ] + } + ], + "title": "subgraph 3", + "properties": {}, + "widgets_values": [] + } + ], + "groups": [], + "links": [ + { + "id": 4, + "origin_id": 6, + "origin_slot": 0, + "target_id": 3, + "target_slot": 1, + "type": "CONDITIONING" + }, + { + "id": 7, + "origin_id": 3, + "origin_slot": 0, + "target_id": -20, + "target_slot": 0, + "type": "LATENT" + }, + { + "id": 10, + "origin_id": 10, + "origin_slot": 0, + "target_id": 3, + "target_slot": 2, + "type": "CONDITIONING" + }, + { + "id": 11, + "origin_id": 10, + "origin_slot": 1, + "target_id": 3, + "target_slot": 3, + "type": "LATENT" + }, + { + "id": 12, + "origin_id": 10, + "origin_slot": 2, + "target_id": 3, + "target_slot": 0, + "type": "MODEL" + }, + { + "id": 13, + "origin_id": 10, + "origin_slot": 3, + "target_id": 6, + "target_slot": 0, + "type": "CLIP" + }, + { + "id": 14, + "origin_id": 10, + "origin_slot": 4, + "target_id": -20, + "target_slot": 1, + "type": "VAE" + } + ], + "extra": {} + }, + { + "id": "dbe5763f-440b-47b4-82ac-454f1f98b0e3", + "version": 1, + "state": { + "lastGroupId": 0, + "lastNodeId": 9, + "lastLinkId": 9, + "lastRerouteId": 0 + }, + "revision": 0, + "config": {}, + "name": "subgraph 3", + "inputNode": { + "id": -10, + "bounding": [ + -154, + 517, + 120, + 40 + ] + }, + "outputNode": { + "id": -20, + "bounding": [ + 898.2780151367188, + 467, + 128.6640625, + 140 + ] + }, + "inputs": [], + "outputs": [ + { + "id": "b4882169-329b-43f6-a373-81abfbdea55b", + "name": "CONDITIONING", + "type": "CONDITIONING", + "linkIds": [ + 6 + ], + "localized_name": "CONDITIONING", + "pos": { + "0": 918.2780151367188, + "1": 487 + } + }, + { + "id": "01f51f96-a741-428e-8772-9557ee50b609", + "name": "LATENT", + "type": "LATENT", + "linkIds": [ + 2 + ], + "localized_name": "LATENT", + "pos": { + "0": 918.2780151367188, + "1": 507 + } + }, + { + "id": "47fa906e-d80b-45c3-a596-211a0e59d4a1", + "name": "MODEL", + "type": "MODEL", + "linkIds": [ + 1 + ], + "localized_name": "MODEL", + "pos": { + "0": 918.2780151367188, + "1": 527 + } + }, + { + "id": "f03dccd7-10e8-4513-9994-15854a92d192", + "name": "CLIP", + "type": "CLIP", + "linkIds": [ + 3 + ], + "localized_name": "CLIP", + "pos": { + "0": 918.2780151367188, + "1": 547 + } + }, + { + "id": "a666877f-e34f-49bc-8a78-b26156656b83", + "name": "VAE", + "type": "VAE", + "linkIds": [ + 8 + ], + "localized_name": "VAE", + "pos": { + "0": 918.2780151367188, + "1": 567 + } + } + ], + "widgets": [], + "nodes": [ + { + "id": 7, + "type": "CLIPTextEncode", + "pos": [ + 413, + 389 + ], + "size": [ + 425.27801513671875, + 180.6060791015625 + ], + "flags": {}, + "order": 2, + "mode": 0, + "inputs": [ + { + "localized_name": "clip", + "name": "clip", + "type": "CLIP", + "link": 5 + } + ], + "outputs": [ + { + "localized_name": "CONDITIONING", + "name": "CONDITIONING", + "type": "CONDITIONING", + "slot_index": 0, + "links": [ + 6 + ] + } + ], + "properties": { + "Node name for S&R": "CLIPTextEncode" + }, + "widgets_values": [ + "text, watermark" + ] + }, + { + "id": 5, + "type": "EmptyLatentImage", + "pos": [ + 473, + 609 + ], + "size": [ + 315, + 106 + ], + "flags": {}, + "order": 0, + "mode": 0, + "inputs": [], + "outputs": [ + { + "localized_name": "LATENT", + "name": "LATENT", + "type": "LATENT", + "slot_index": 0, + "links": [ + 2 + ] + } + ], + "properties": { + "Node name for S&R": "EmptyLatentImage" + }, + "widgets_values": [ + 512, + 512, + 1 + ] + }, + { + "id": 4, + "type": "CheckpointLoaderSimple", + "pos": [ + 26, + 474 + ], + "size": [ + 315, + 98 + ], + "flags": {}, + "order": 1, + "mode": 0, + "inputs": [], + "outputs": [ + { + "localized_name": "MODEL", + "name": "MODEL", + "type": "MODEL", + "slot_index": 0, + "links": [ + 1 + ] + }, + { + "localized_name": "CLIP", + "name": "CLIP", + "type": "CLIP", + "slot_index": 1, + "links": [ + 3, + 5 + ] + }, + { + "localized_name": "VAE", + "name": "VAE", + "type": "VAE", + "slot_index": 2, + "links": [ + 8 + ] + } + ], + "properties": { + "Node name for S&R": "CheckpointLoaderSimple" + }, + "widgets_values": [ + "v1-5-pruned-emaonly-fp16.safetensors" + ] + } + ], + "groups": [], + "links": [ + { + "id": 5, + "origin_id": 4, + "origin_slot": 1, + "target_id": 7, + "target_slot": 0, + "type": "CLIP" + }, + { + "id": 6, + "origin_id": 7, + "origin_slot": 0, + "target_id": -20, + "target_slot": 0, + "type": "CONDITIONING" + }, + { + "id": 2, + "origin_id": 5, + "origin_slot": 0, + "target_id": -20, + "target_slot": 1, + "type": "LATENT" + }, + { + "id": 1, + "origin_id": 4, + "origin_slot": 0, + "target_id": -20, + "target_slot": 2, + "type": "MODEL" + }, + { + "id": 3, + "origin_id": 4, + "origin_slot": 1, + "target_id": -20, + "target_slot": 3, + "type": "CLIP" + }, + { + "id": 8, + "origin_id": 4, + "origin_slot": 2, + "target_id": -20, + "target_slot": 4, + "type": "VAE" + } + ], + "extra": {} + } + ] + }, + "config": {}, + "extra": { + "frontendVersion": "1.24.0-1" + }, + "version": 0.4 +} \ No newline at end of file diff --git a/browser_tests/tests/subgraphBreadcrumb.spec.ts b/browser_tests/tests/subgraphBreadcrumb.spec.ts new file mode 100644 index 0000000000..d0bcb3360a --- /dev/null +++ b/browser_tests/tests/subgraphBreadcrumb.spec.ts @@ -0,0 +1,82 @@ +import { expect } from '@playwright/test' + +import { comfyPageFixture as test } from '../fixtures/ComfyPage' + +test.describe('Subgraph Breadcrumb Title Sync', () => { + test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') + }) + + test('Breadcrumb updates when subgraph node title is changed', async ({ + comfyPage + }) => { + // Load a workflow with subgraphs + await comfyPage.loadWorkflow('nested-subgraph') + await comfyPage.nextFrame() + + // Get the subgraph node by ID (node 10 is the subgraph) + const subgraphNode = await comfyPage.getNodeRefById('10') + + // Get node position and double-click on it to enter the subgraph + const nodePos = await subgraphNode.getPosition() + const nodeSize = await subgraphNode.getSize() + await comfyPage.canvas.dblclick({ + position: { + x: nodePos.x + nodeSize.width / 2, + y: nodePos.y + nodeSize.height / 2 + 10 + } + }) + await comfyPage.nextFrame() + + // Wait for breadcrumb to appear + await comfyPage.page.waitForSelector('.subgraph-breadcrumb', { + state: 'visible', + timeout: 20000 + }) + + // Get initial breadcrumb text + const breadcrumb = comfyPage.page.locator('.subgraph-breadcrumb') + const initialBreadcrumbText = await breadcrumb.textContent() + + // Go back to main graph + await comfyPage.page.keyboard.press('Escape') + + // Double-click on the title area of the subgraph node to edit + await comfyPage.canvas.dblclick({ + position: { + x: nodePos.x + nodeSize.width / 2, + y: nodePos.y - 10 // Title area is above the node body + }, + delay: 5 + }) + + // Wait for title editor to appear + await expect(comfyPage.page.locator('.node-title-editor')).toBeVisible() + + // Clear existing text and type new title + await comfyPage.page.keyboard.press('Control+a') + const newTitle = 'Updated Subgraph Title' + await comfyPage.page.keyboard.type(newTitle) + await comfyPage.page.keyboard.press('Enter') + + // Wait a frame for the update to complete + await comfyPage.nextFrame() + + // Enter the subgraph again + await comfyPage.canvas.dblclick({ + position: { + x: nodePos.x + nodeSize.width / 2, + y: nodePos.y + nodeSize.height / 2 + }, + delay: 5 + }) + + // Wait for breadcrumb + await comfyPage.page.waitForSelector('.subgraph-breadcrumb') + + // Check that breadcrumb now shows the new title + const updatedBreadcrumbText = await breadcrumb.textContent() + expect(updatedBreadcrumbText).toContain(newTitle) + expect(updatedBreadcrumbText).not.toBe(initialBreadcrumbText) + }) +}) diff --git a/src/components/graph/TitleEditor.vue b/src/components/graph/TitleEditor.vue index eabc03bb35..f3e49e77fc 100644 --- a/src/components/graph/TitleEditor.vue +++ b/src/components/graph/TitleEditor.vue @@ -41,7 +41,15 @@ const previousCanvasDraggable = ref(true) const onEdit = (newValue: string) => { if (titleEditorStore.titleEditorTarget && newValue.trim() !== '') { - titleEditorStore.titleEditorTarget.title = newValue.trim() + const trimmedTitle = newValue.trim() + titleEditorStore.titleEditorTarget.title = trimmedTitle + + // If this is a subgraph node, sync the runtime subgraph name for breadcrumb reactivity + const target = titleEditorStore.titleEditorTarget + if (target instanceof LGraphNode && target.isSubgraphNode?.()) { + target.subgraph.name = trimmedTitle + } + app.graph.setDirtyCanvas(true, true) } showInput.value = false