From 2ccfb822b4d742801eaeb3969f2fbda768e63811 Mon Sep 17 00:00:00 2001 From: AustinMroz Date: Mon, 9 Mar 2026 13:18:05 -0700 Subject: [PATCH] Restore hiding of linked inputs in app mode (#9671) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit As a temporary fix for widgets being incorrectly hidden, #9669 allowed all disabled widgets to be displayed. This PR provides a more robust implementation to derive whether the widget, as would be displayed from the root graph, is disabled. Potential regression: - Drag drop handlers are applied on node, not widgets. A subgraph containing a "Load Image" node, does not allow dragging and dropping an image onto the subgraphNode in order to load it. Because app mode widgets would display from the original owning node prior to this PR, these drag/drop handlers would apply. Placing "Load Image" nodes. I believe this change makes behavior more consistent, but it warrants consideration. ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-9671-Restore-hiding-of-linked-inputs-in-app-mode-31e6d73d365081688e37fbb931f3af68) by [Unito](https://www.unito.io) --- src/components/builder/AppBuilder.vue | 25 +---------- .../extensions/linearMode/LinearControls.vue | 45 +++++++++++++------ src/utils/litegraphUtil.ts | 26 +++++++++++ 3 files changed, 58 insertions(+), 38 deletions(-) diff --git a/src/components/builder/AppBuilder.vue b/src/components/builder/AppBuilder.vue index ef3adf946d..bd2610d22b 100644 --- a/src/components/builder/AppBuilder.vue +++ b/src/components/builder/AppBuilder.vue @@ -28,6 +28,7 @@ import { DOMWidgetImpl } from '@/scripts/domWidget' import { promptRenameWidget } from '@/utils/widgetUtil' import { useAppMode } from '@/composables/useAppMode' import { nodeTypeValidForApp, useAppModeStore } from '@/stores/appModeStore' +import { resolveNodeWidget } from '@/utils/litegraphUtil' import { cn } from '@/utils/tailwindUtil' import { HideLayoutFieldKey } from '@/types/widgetTypes' @@ -102,30 +103,6 @@ function getHovered( if (widget || node.constructor.nodeData?.output_node) return [node, widget] } -function resolveNodeWidget( - nodeId: NodeId, - widgetName?: string -): [LGraphNode, IBaseWidget] | [LGraphNode] | [] { - const node = app.graph.getNodeById(nodeId) - if (!widgetName) return node ? [node] : [] - if (node) { - const widget = node.widgets?.find((w) => w.name === widgetName) - return widget ? [node, widget] : [] - } - - for (const node of app.graph.nodes) { - if (!node.isSubgraphNode()) continue - const widget = node.widgets?.find( - (w) => - isPromotedWidgetView(w) && - w.sourceWidgetName === widgetName && - w.sourceNodeId === nodeId - ) - if (widget) return [node, widget] - } - - return [] -} function getBounding(nodeId: NodeId, widgetName?: string) { if (settingStore.get('Comfy.VueNodes.Enabled')) return undefined diff --git a/src/renderer/extensions/linearMode/LinearControls.vue b/src/renderer/extensions/linearMode/LinearControls.vue index 74bb0b0054..1b235ff51b 100644 --- a/src/renderer/extensions/linearMode/LinearControls.vue +++ b/src/renderer/extensions/linearMode/LinearControls.vue @@ -10,6 +10,7 @@ import ScrubableNumberInput from '@/components/common/ScrubableNumberInput.vue' import Popover from '@/components/ui/Popover.vue' import Button from '@/components/ui/button/Button.vue' import { extractVueNodeData } from '@/composables/graph/useGraphNodeManager' +import { isPromotedWidgetView } from '@/core/graph/subgraph/promotedWidgetTypes' import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode' import { LGraphEventMode } from '@/lib/litegraph/src/types/globalEnums' import { useBillingContext } from '@/composables/billing/useBillingContext' @@ -29,7 +30,7 @@ import { useQueueSettingsStore } from '@/stores/queueStore' import { cn } from '@/utils/tailwindUtil' import { useAppMode } from '@/composables/useAppMode' import { useAppModeStore } from '@/stores/appModeStore' -import { resolveNode } from '@/utils/litegraphUtil' +import { resolveNodeWidget } from '@/utils/litegraphUtil' const { t } = useI18n() const commandStore = useCommandStore() const executionErrorStore = useExecutionErrorStore() @@ -63,21 +64,41 @@ useEventListener( ) const mappedSelections = computed(() => { - let unprocessedInputs = [...appModeStore.selectedInputs] - //FIXME strict typing here + let unprocessedInputs = appModeStore.selectedInputs.flatMap( + ([nodeId, widgetName]) => { + const [node, widget] = resolveNodeWidget(nodeId, widgetName) + return widget ? ([[node, widget]] as const) : [] + } + ) const processedInputs: ReturnType[] = [] while (unprocessedInputs.length) { - const nodeId = unprocessedInputs[0][0] - const inputGroup = takeWhile( - unprocessedInputs, - ([id]) => id === nodeId - ).map(([, widgetName]) => widgetName) + const [node] = unprocessedInputs[0] + const inputGroup = takeWhile(unprocessedInputs, ([n]) => n === node).map( + ([, widget]) => widget + ) unprocessedInputs = unprocessedInputs.slice(inputGroup.length) - const node = resolveNode(nodeId) + //FIXME: hide widget if owning node bypassed if (node?.mode !== LGraphEventMode.ALWAYS) continue const nodeData = nodeToNodeData(node) - remove(nodeData.widgets ?? [], (w) => !inputGroup.includes(w.name)) + remove(nodeData.widgets ?? [], (vueWidget) => { + if (vueWidget.slotMetadata?.linked) return true + + if (!node.isSubgraphNode()) + return !inputGroup.some((w) => w.name === vueWidget.name) + + const storeNodeId = vueWidget.storeNodeId?.split(':')?.[1] ?? '' + return !inputGroup.some( + (subWidget) => + isPromotedWidgetView(subWidget) && + subWidget.sourceNodeId == storeNodeId && + subWidget.sourceWidgetName === vueWidget.storeName + ) + }) + for (const widget of nodeData.widgets ?? []) { + widget.slotMetadata = undefined + widget.nodeId = String(node.id) + } processedInputs.push(nodeData) } return processedInputs @@ -107,10 +128,6 @@ function getDropIndicator(node: LGraphNode) { function nodeToNodeData(node: LGraphNode) { const dropIndicator = getDropIndicator(node) const nodeData = extractVueNodeData(node) - for (const widget of nodeData.widgets ?? []) { - widget.slotMetadata = undefined - widget.nodeId = String(node.id) - } return { ...nodeData, diff --git a/src/utils/litegraphUtil.ts b/src/utils/litegraphUtil.ts index 2a05397383..65629a8d3e 100644 --- a/src/utils/litegraphUtil.ts +++ b/src/utils/litegraphUtil.ts @@ -1,5 +1,6 @@ import _ from 'es-toolkit/compat' +import { isPromotedWidgetView } from '@/core/graph/subgraph/promotedWidgetTypes' import type { ColorOption, LGraph } from '@/lib/litegraph/src/litegraph' import type { ExecutedWsMessage } from '@/schemas/apiSchema' import { @@ -319,6 +320,31 @@ export function resolveNode( } return undefined } +export function resolveNodeWidget( + nodeId: NodeId, + widgetName?: string, + graph: LGraph = app.rootGraph +): [LGraphNode, IBaseWidget] | [LGraphNode] | [] { + const node = graph.getNodeById(nodeId) + if (!widgetName) return node ? [node] : [] + if (node) { + const widget = node.widgets?.find((w) => w.name === widgetName) + return widget ? [node, widget] : [] + } + + for (const node of graph.nodes) { + if (!node.isSubgraphNode()) continue + const widget = node.widgets?.find( + (w) => + isPromotedWidgetView(w) && + w.sourceWidgetName === widgetName && + w.sourceNodeId === nodeId + ) + if (widget) return [node, widget] + } + + return [] +} export function isLoad3dNode(node: LGraphNode) { return (