mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-13 17:10:06 +00:00
Allow reordering of linked subgraph widgets (#5981)
Widgets which are promoted by linking to a subgraphInput node are now also displayed in the subgraph configuration window. They can now be reordered by drag and drop along with proxyWidgets  Known Issues: - "Hide All" will incorrectly remove physically linked widgets ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-5981-Allow-reordering-of-linked-subgraph-widgets-2866d73d365081d9b27cf4a9c3078007) by [Unito](https://www.unito.io)
This commit is contained in:
@@ -64,15 +64,21 @@ const activeNode = computed(() => {
|
|||||||
|
|
||||||
const activeWidgets = computed<WidgetItem[]>({
|
const activeWidgets = computed<WidgetItem[]>({
|
||||||
get() {
|
get() {
|
||||||
|
if (!activeNode.value) return []
|
||||||
const node = activeNode.value
|
const node = activeNode.value
|
||||||
if (!node) return []
|
function mapWidgets([id, name]: [string, string]): WidgetItem[] {
|
||||||
return proxyWidgets.value.flatMap(([id, name]: [string, string]) => {
|
if (id === '-1') {
|
||||||
|
const widget = node.widgets.find((w) => w.name === name)
|
||||||
|
if (!widget) return []
|
||||||
|
return [[{ id: -1, title: '(Linked)', type: '' }, widget]]
|
||||||
|
}
|
||||||
const wNode = node.subgraph._nodes_by_id[id]
|
const wNode = node.subgraph._nodes_by_id[id]
|
||||||
if (!wNode?.widgets) return []
|
if (!wNode?.widgets) return []
|
||||||
const w = wNode.widgets.find((w) => w.name === name)
|
const widget = wNode.widgets.find((w) => w.name === name)
|
||||||
if (!w) return []
|
if (!widget) return []
|
||||||
return [[wNode, w]]
|
return [[wNode, widget]]
|
||||||
})
|
}
|
||||||
|
return proxyWidgets.value.flatMap(mapWidgets)
|
||||||
},
|
},
|
||||||
set(value: WidgetItem[]) {
|
set(value: WidgetItem[]) {
|
||||||
const node = activeNode.value
|
const node = activeNode.value
|
||||||
@@ -80,9 +86,7 @@ const activeWidgets = computed<WidgetItem[]>({
|
|||||||
console.error('Attempted to toggle widgets with no node selected')
|
console.error('Attempted to toggle widgets with no node selected')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
//map back to id/name
|
proxyWidgets.value = value.map(widgetItemToProperty)
|
||||||
const widgets: ProxyWidgetsProperty = value.map(widgetItemToProperty)
|
|
||||||
proxyWidgets.value = widgets
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -165,10 +169,10 @@ function showAll() {
|
|||||||
function hideAll() {
|
function hideAll() {
|
||||||
const node = activeNode.value
|
const node = activeNode.value
|
||||||
if (!node) return //Not reachable
|
if (!node) return //Not reachable
|
||||||
//Not great from a nesting perspective, but path is cold
|
|
||||||
//and it cleans up potential error states
|
|
||||||
proxyWidgets.value = proxyWidgets.value.filter(
|
proxyWidgets.value = proxyWidgets.value.filter(
|
||||||
(widgetItem) => !filteredActive.value.some(matchesWidgetItem(widgetItem))
|
(propertyItem) =>
|
||||||
|
!filteredActive.value.some(matchesWidgetItem(propertyItem)) ||
|
||||||
|
propertyItem[0] === '-1'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
function showRecommended() {
|
function showRecommended() {
|
||||||
@@ -258,20 +262,16 @@ onBeforeUnmount(() => {
|
|||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<div ref="draggableItems">
|
<div ref="draggableItems">
|
||||||
<div
|
<SubgraphNodeWidget
|
||||||
v-for="[node, widget] in filteredActive"
|
v-for="[node, widget] in filteredActive"
|
||||||
:key="toKey([node, widget])"
|
:key="toKey([node, widget])"
|
||||||
class="draggable-item w-full"
|
:node-title="node.title"
|
||||||
style=""
|
:widget-name="widget.name"
|
||||||
>
|
:is-shown="true"
|
||||||
<SubgraphNodeWidget
|
:is-draggable="!debouncedQuery"
|
||||||
:node-title="node.title"
|
:is-physical="node.id === -1"
|
||||||
:widget-name="widget.name"
|
@toggle-visibility="demote([node, widget])"
|
||||||
:is-shown="true"
|
/>
|
||||||
:is-draggable="!debouncedQuery"
|
|
||||||
@toggle-visibility="demote([node, widget])"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="filteredCandidates.length" class="pt-1 pb-4">
|
<div v-if="filteredCandidates.length" class="pt-1 pb-4">
|
||||||
@@ -286,17 +286,13 @@ onBeforeUnmount(() => {
|
|||||||
{{ $t('subgraphStore.showAll') }}</a
|
{{ $t('subgraphStore.showAll') }}</a
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<SubgraphNodeWidget
|
||||||
v-for="[node, widget] in filteredCandidates"
|
v-for="[node, widget] in filteredCandidates"
|
||||||
:key="toKey([node, widget])"
|
:key="toKey([node, widget])"
|
||||||
class="w-full"
|
:node-title="node.title"
|
||||||
>
|
:widget-name="widget.name"
|
||||||
<SubgraphNodeWidget
|
@toggle-visibility="promote([node, widget])"
|
||||||
:node-title="node.title"
|
/>
|
||||||
:widget-name="widget.name"
|
|
||||||
@toggle-visibility="promote([node, widget])"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="recommendedWidgets.length"
|
v-if="recommendedWidgets.length"
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ const props = defineProps<{
|
|||||||
widgetName: string
|
widgetName: string
|
||||||
isShown?: boolean
|
isShown?: boolean
|
||||||
isDraggable?: boolean
|
isDraggable?: boolean
|
||||||
|
isPhysical?: boolean
|
||||||
}>()
|
}>()
|
||||||
defineEmits<{
|
defineEmits<{
|
||||||
(e: 'toggleVisibility'): void
|
(e: 'toggleVisibility'): void
|
||||||
@@ -17,11 +18,17 @@ function classes() {
|
|||||||
return cn(
|
return cn(
|
||||||
'flex py-1 pr-4 pl-0 break-all rounded items-center gap-1',
|
'flex py-1 pr-4 pl-0 break-all rounded items-center gap-1',
|
||||||
'bg-node-component-surface',
|
'bg-node-component-surface',
|
||||||
props.isDraggable
|
props.isDraggable &&
|
||||||
? 'drag-handle cursor-grab [.is-draggable]:cursor-grabbing'
|
'draggable-item drag-handle cursor-grab [&.is-draggable]:cursor-grabbing'
|
||||||
: ''
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
function getIcon() {
|
||||||
|
return props.isPhysical
|
||||||
|
? 'icon-[lucide--link]'
|
||||||
|
: props.isDraggable
|
||||||
|
? 'icon-[lucide--eye]'
|
||||||
|
: 'icon-[lucide--eye-off]'
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div :class="classes()">
|
<div :class="classes()">
|
||||||
@@ -40,7 +47,8 @@ function classes() {
|
|||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
text
|
text
|
||||||
:icon="isDraggable ? 'icon-[lucide--eye]' : 'icon-[lucide--eye-off]'"
|
:icon="getIcon()"
|
||||||
|
:disabled="isPhysical"
|
||||||
severity="secondary"
|
severity="secondary"
|
||||||
@click.stop="$emit('toggleVisibility')"
|
@click.stop="$emit('toggleVisibility')"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { demoteWidget } from '@/core/graph/subgraph/proxyWidgetUtils'
|
import { demoteWidget } from '@/core/graph/subgraph/proxyWidgetUtils'
|
||||||
import { parseProxyWidgets } from '@/core/schemas/proxyWidget'
|
import { parseProxyWidgets } from '@/core/schemas/proxyWidget'
|
||||||
|
import type { NodeProperty } from '@/lib/litegraph/src/LGraphNode'
|
||||||
import type {
|
import type {
|
||||||
LGraph,
|
LGraph,
|
||||||
LGraphCanvas,
|
LGraphCanvas,
|
||||||
@@ -75,15 +76,17 @@ const onConfigure = function (
|
|||||||
const canvasStore = useCanvasStore()
|
const canvasStore = useCanvasStore()
|
||||||
//Must give value to proxyWidgets prior to defining or it won't serialize
|
//Must give value to proxyWidgets prior to defining or it won't serialize
|
||||||
this.properties.proxyWidgets ??= []
|
this.properties.proxyWidgets ??= []
|
||||||
let proxyWidgets = this.properties.proxyWidgets
|
|
||||||
|
|
||||||
originalOnConfigure?.call(this, serialisedNode)
|
originalOnConfigure?.call(this, serialisedNode)
|
||||||
|
|
||||||
Object.defineProperty(this.properties, 'proxyWidgets', {
|
Object.defineProperty(this.properties, 'proxyWidgets', {
|
||||||
get: () => {
|
get: () =>
|
||||||
return proxyWidgets
|
this.widgets.map((w) =>
|
||||||
},
|
isProxyWidget(w)
|
||||||
set: (property: string) => {
|
? [w._overlay.nodeId, w._overlay.widgetName]
|
||||||
|
: ['-1', w.name]
|
||||||
|
),
|
||||||
|
set: (property: NodeProperty) => {
|
||||||
const parsed = parseProxyWidgets(property)
|
const parsed = parseProxyWidgets(property)
|
||||||
const { deactivateWidget, setWidget } = useDomWidgetStore()
|
const { deactivateWidget, setWidget } = useDomWidgetStore()
|
||||||
const isActiveGraph = useCanvasStore().canvas?.graph === this.graph
|
const isActiveGraph = useCanvasStore().canvas?.graph === this.graph
|
||||||
@@ -92,21 +95,34 @@ const onConfigure = function (
|
|||||||
if (w instanceof DOMWidgetImpl) deactivateWidget(w.id)
|
if (w instanceof DOMWidgetImpl) deactivateWidget(w.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.widgets = this.widgets.filter((w) => !isProxyWidget(w))
|
|
||||||
for (const [nodeId, widgetName] of parsed) {
|
const newWidgets = parsed.flatMap(([nodeId, widgetName]) => {
|
||||||
const w = addProxyWidget(this, `${nodeId}`, widgetName)
|
if (nodeId === '-1') {
|
||||||
|
const widget = this.widgets.find((w) => w.name === widgetName)
|
||||||
|
return widget ? [widget] : []
|
||||||
|
}
|
||||||
|
const w = newProxyWidget(this, nodeId, widgetName)
|
||||||
if (isActiveGraph && w instanceof DOMWidgetImpl) setWidget(w)
|
if (isActiveGraph && w instanceof DOMWidgetImpl) setWidget(w)
|
||||||
}
|
return [w]
|
||||||
proxyWidgets = property
|
})
|
||||||
|
this.widgets = this.widgets.filter(
|
||||||
|
(w) => !isProxyWidget(w) && !parsed.some(([, name]) => w.name === name)
|
||||||
|
)
|
||||||
|
this.widgets.push(...newWidgets)
|
||||||
|
|
||||||
canvasStore.canvas?.setDirty(true, true)
|
canvasStore.canvas?.setDirty(true, true)
|
||||||
this._setConcreteSlots()
|
this._setConcreteSlots()
|
||||||
this.arrange()
|
this.arrange()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
this.properties.proxyWidgets = proxyWidgets
|
if (serialisedNode.properties?.proxyWidgets)
|
||||||
|
this.properties.proxyWidgets = serialisedNode.properties.proxyWidgets
|
||||||
|
serialisedNode.widgets_values?.forEach((v, index) => {
|
||||||
|
if (v !== null) this.widgets[index].value = v
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function addProxyWidget(
|
function newProxyWidget(
|
||||||
subgraphNode: SubgraphNode,
|
subgraphNode: SubgraphNode,
|
||||||
nodeId: string,
|
nodeId: string,
|
||||||
widgetName: string
|
widgetName: string
|
||||||
@@ -130,7 +146,7 @@ function addProxyWidget(
|
|||||||
width: undefined,
|
width: undefined,
|
||||||
y: 0
|
y: 0
|
||||||
}
|
}
|
||||||
return addProxyFromOverlay(subgraphNode, overlay)
|
return newProxyFromOverlay(subgraphNode, overlay)
|
||||||
}
|
}
|
||||||
function resolveLinkedWidget(
|
function resolveLinkedWidget(
|
||||||
overlay: Overlay
|
overlay: Overlay
|
||||||
@@ -141,7 +157,7 @@ function resolveLinkedWidget(
|
|||||||
return [n, n.widgets?.find((w: IBaseWidget) => w.name === widgetName)]
|
return [n, n.widgets?.find((w: IBaseWidget) => w.name === widgetName)]
|
||||||
}
|
}
|
||||||
|
|
||||||
function addProxyFromOverlay(subgraphNode: SubgraphNode, overlay: Overlay) {
|
function newProxyFromOverlay(subgraphNode: SubgraphNode, overlay: Overlay) {
|
||||||
const { updatePreviews } = useLitegraphService()
|
const { updatePreviews } = useLitegraphService()
|
||||||
let [linkedNode, linkedWidget] = resolveLinkedWidget(overlay)
|
let [linkedNode, linkedWidget] = resolveLinkedWidget(overlay)
|
||||||
let backingWidget = linkedWidget ?? disconnectedWidget
|
let backingWidget = linkedWidget ?? disconnectedWidget
|
||||||
@@ -213,6 +229,5 @@ function addProxyFromOverlay(subgraphNode: SubgraphNode, overlay: Overlay) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const w = new Proxy(disconnectedWidget, handler)
|
const w = new Proxy(disconnectedWidget, handler)
|
||||||
subgraphNode.widgets.push(w)
|
|
||||||
return w
|
return w
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,13 +9,15 @@ import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets.ts'
|
|||||||
import { useLitegraphService } from '@/services/litegraphService'
|
import { useLitegraphService } from '@/services/litegraphService'
|
||||||
import { useSubgraphNavigationStore } from '@/stores/subgraphNavigationStore'
|
import { useSubgraphNavigationStore } from '@/stores/subgraphNavigationStore'
|
||||||
|
|
||||||
export type WidgetItem = [LGraphNode, IBaseWidget]
|
type PartialNode = Pick<LGraphNode, 'title' | 'id' | 'type'>
|
||||||
|
|
||||||
|
export type WidgetItem = [PartialNode, IBaseWidget]
|
||||||
|
|
||||||
function getProxyWidgets(node: SubgraphNode) {
|
function getProxyWidgets(node: SubgraphNode) {
|
||||||
return parseProxyWidgets(node.properties.proxyWidgets)
|
return parseProxyWidgets(node.properties.proxyWidgets)
|
||||||
}
|
}
|
||||||
export function promoteWidget(
|
export function promoteWidget(
|
||||||
node: LGraphNode,
|
node: PartialNode,
|
||||||
widget: IBaseWidget,
|
widget: IBaseWidget,
|
||||||
parents: SubgraphNode[]
|
parents: SubgraphNode[]
|
||||||
) {
|
) {
|
||||||
@@ -30,7 +32,7 @@ export function promoteWidget(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function demoteWidget(
|
export function demoteWidget(
|
||||||
node: LGraphNode,
|
node: PartialNode,
|
||||||
widget: IBaseWidget,
|
widget: IBaseWidget,
|
||||||
parents: SubgraphNode[]
|
parents: SubgraphNode[]
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
import { describe, expect, test, vi } from 'vitest'
|
import { describe, expect, test, vi } from 'vitest'
|
||||||
|
|
||||||
import { registerProxyWidgets } from '@/core/graph/subgraph/proxyWidget'
|
import { registerProxyWidgets } from '@/core/graph/subgraph/proxyWidget'
|
||||||
import {
|
import { parseProxyWidgets } from '@/core/schemas/proxyWidget'
|
||||||
type LGraphCanvas,
|
import { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||||
LGraphNode,
|
import type { LGraphCanvas, SubgraphNode } from '@/lib/litegraph/src/litegraph'
|
||||||
type SubgraphNode
|
|
||||||
} from '@/lib/litegraph/src/litegraph'
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
createTestSubgraph,
|
createTestSubgraph,
|
||||||
@@ -66,14 +64,19 @@ describe('Subgraph proxyWidgets', () => {
|
|||||||
subgraphNode.widgets[1].name
|
subgraphNode.widgets[1].name
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
test('Will not modify existing widgets', () => {
|
test('Will serialize existing widgets', () => {
|
||||||
const [subgraphNode, innerNodes] = setupSubgraph(1)
|
const [subgraphNode, innerNodes] = setupSubgraph(1)
|
||||||
innerNodes[0].addWidget('text', 'stringWidget', 'value', () => {})
|
innerNodes[0].addWidget('text', 'istringWidget', 'value', () => {})
|
||||||
subgraphNode.addWidget('text', 'stringWidget', 'value', () => {})
|
subgraphNode.addWidget('text', 'stringWidget', 'value', () => {})
|
||||||
subgraphNode.properties.proxyWidgets = [['1', 'stringWidget']]
|
|
||||||
|
const proxyWidgets = parseProxyWidgets(subgraphNode.properties.proxyWidgets)
|
||||||
|
proxyWidgets.push(['1', 'istringWidget'])
|
||||||
|
subgraphNode.properties.proxyWidgets = proxyWidgets
|
||||||
|
|
||||||
expect(subgraphNode.widgets.length).toBe(2)
|
expect(subgraphNode.widgets.length).toBe(2)
|
||||||
subgraphNode.properties.proxyWidgets = []
|
expect(subgraphNode.widgets[0].name).toBe('stringWidget')
|
||||||
expect(subgraphNode.widgets.length).toBe(1)
|
subgraphNode.properties.proxyWidgets = [proxyWidgets[1], proxyWidgets[0]]
|
||||||
|
expect(subgraphNode.widgets[0].name).toBe('1: istringWidget')
|
||||||
})
|
})
|
||||||
test('Will mirror changes to value', () => {
|
test('Will mirror changes to value', () => {
|
||||||
const [subgraphNode, innerNodes] = setupSubgraph(1)
|
const [subgraphNode, innerNodes] = setupSubgraph(1)
|
||||||
|
|||||||
Reference in New Issue
Block a user