mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-30 19:21:54 +00:00
Working drag and drop
This commit is contained in:
@@ -1,73 +1,100 @@
|
|||||||
<template>
|
|
||||||
<SidebarTabTemplate
|
|
||||||
:title="'Subgraph Node'"
|
|
||||||
class="workflows-sidebar-tab bg-[var(--p-tree-background)]"
|
|
||||||
>
|
|
||||||
<template #body>
|
|
||||||
<TreeExplorer
|
|
||||||
v-model:expandedKeys="expandedKeys"
|
|
||||||
:root="renderedRoot"
|
|
||||||
>
|
|
||||||
<template #node="{ node }">
|
|
||||||
<SubgraphNodeWidget :node="node"/>
|
|
||||||
</template>
|
|
||||||
</TreeExplorer>
|
|
||||||
</template>
|
|
||||||
</SidebarTabTemplate>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, watch, computed } from 'vue';
|
import { ref, watch, computed } from 'vue';
|
||||||
import { watchDebounced } from '@vueuse/core'
|
import { watchDebounced } from '@vueuse/core'
|
||||||
import OrderList from 'primevue/orderlist';
|
|
||||||
|
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import draggable from 'vuedraggable'
|
||||||
|
|
||||||
import SidebarTabTemplate from '@/components/sidebar/tabs/SidebarTabTemplate.vue'
|
import SidebarTabTemplate from '@/components/sidebar/tabs/SidebarTabTemplate.vue'
|
||||||
import TreeExplorer from '@/components/common/TreeExplorer.vue'
|
|
||||||
import SubgraphNodeWidget from '@/components/selectionbar/SubgraphNodeWidget.vue'
|
import SubgraphNodeWidget from '@/components/selectionbar/SubgraphNodeWidget.vue'
|
||||||
import { useCanvasStore } from '@/stores/graphStore'
|
import { useCanvasStore } from '@/stores/graphStore'
|
||||||
import { buildTree } from '@/utils/treeUtil'
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
const canvasStore = useCanvasStore()
|
const canvasStore = useCanvasStore()
|
||||||
|
|
||||||
const expandedKeys = ref<Record<string, boolean>>({})
|
const expandedKeys = ref<Record<string, boolean>>({})
|
||||||
|
|
||||||
const widgetTree = computed(() => {
|
const triggerUpdate = ref(0)
|
||||||
|
|
||||||
|
const activeNode = computed(() => {
|
||||||
|
return canvasStore.selectedItems[0]
|
||||||
|
})
|
||||||
|
|
||||||
|
function keyfn(item) {
|
||||||
|
return `${item[0].title}(${item[0].id}): ${item[1].name}`
|
||||||
|
}
|
||||||
|
const activeWidgets = computed({
|
||||||
|
get() {
|
||||||
|
triggerUpdate.value
|
||||||
|
const node = activeNode.value
|
||||||
|
const pw = node.properties.proxyWidgets ?? []
|
||||||
|
return pw.map(([id, name]) => {
|
||||||
|
const wNode = node.subgraph._nodes_by_id[id]
|
||||||
|
const w = wNode.widgets.find((w) => w.name === name)
|
||||||
|
return [wNode, w]
|
||||||
|
})
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
//map back to id/name
|
||||||
|
const pw = value.map(([node, widget]) => [node.id, widget.name])
|
||||||
|
activeNode.value.properties.proxyWidgets = pw
|
||||||
|
//force trigger an update
|
||||||
|
triggerUpdate.value++
|
||||||
|
canvasStore.canvas.setDirty(true)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const candidateWidgets = computed(() =>{
|
||||||
const node = canvasStore.selectedItems[0] ?? {}
|
const node = canvasStore.selectedItems[0] ?? {}
|
||||||
const interiorNodes = node?.subgraph?.nodes ?? []
|
const interiorNodes = node?.subgraph?.nodes ?? []
|
||||||
node.widgets ??= []
|
node.widgets ??= []
|
||||||
const intn = interiorNodes.map((n) =>
|
const intn = interiorNodes.flatMap((n) =>
|
||||||
n.widgets?.map((w) => [n, w, node]) ?? []).flat(1)
|
n.widgets?.map((w) => {
|
||||||
|
return [n,w] ?? []
|
||||||
|
}))
|
||||||
//widget has connected link. Should not be displayed
|
//widget has connected link. Should not be displayed
|
||||||
.filter((i) => !i[1].computedDisabled)
|
.filter((i) => !i[1].computedDisabled)
|
||||||
//TODO: filter enabled/disabled items while keeping order
|
//TODO: filter enabled/disabled items while keeping order
|
||||||
return buildTree(intn, (item: [unknown, unknown]) =>
|
return intn
|
||||||
[`${item[0].title}(${item[0].id}): ${item[1].name}`]
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const renderedRoot = computed<TreeExplorerNode<ComfyNodeDefImpl>>(() => {
|
|
||||||
const fillNodeInfo = (node: TreeNode): TreeExplorerNode<ComfyNodeDefImpl> => {
|
|
||||||
const children = node.children?.map(fillNodeInfo)
|
|
||||||
|
|
||||||
return {
|
|
||||||
key: node.key,
|
|
||||||
leaf: node.leaf,
|
|
||||||
data: node.data,
|
|
||||||
label: node.label,
|
|
||||||
getIcon() {
|
|
||||||
return 'pi pi-minus'
|
|
||||||
},
|
|
||||||
children,
|
|
||||||
onToggle: () => console.log(widgetTree,node),
|
|
||||||
draggable: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const ret = fillNodeInfo(widgetTree.value)
|
|
||||||
return ret
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
<template>
|
||||||
|
<SidebarTabTemplate
|
||||||
|
:title="'Subgraph Node'"
|
||||||
|
class="workflows-sidebar-tab bg-[var(--p-tree-background)]"
|
||||||
|
>
|
||||||
|
<template #body>
|
||||||
|
<div class="widgets-section">
|
||||||
|
<draggable
|
||||||
|
v-model="activeWidgets"
|
||||||
|
group="people"
|
||||||
|
:animation="100"
|
||||||
|
@start="drag=true"
|
||||||
|
@end="drag=false"
|
||||||
|
item-key="id">
|
||||||
|
<template #item="{element}">
|
||||||
|
<SubgraphNodeWidget :item="element" :node="activeNode" :draggable="true"/>
|
||||||
|
</template>
|
||||||
|
</draggable>
|
||||||
|
</div>
|
||||||
|
<div class="widgets-section">
|
||||||
|
<div v-for="element in candidateWidgets">
|
||||||
|
<SubgraphNodeWidget :item="element" :node="activeNode"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</SidebarTabTemplate>
|
||||||
|
</template>
|
||||||
|
<style scoped>
|
||||||
|
.widgets-section {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
padding: 4px 0 16px 0;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 16px;
|
||||||
|
align-self: stretch;
|
||||||
|
border-bottom: 1px solid var(--color-node-divider, #2E3037);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,21 +1,3 @@
|
|||||||
<template>
|
|
||||||
<div ref="container" class="node-lib-node-container">
|
|
||||||
<TreeExplorerTreeNode :node="node">
|
|
||||||
<template #actions>
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
text
|
|
||||||
severity="secondary"
|
|
||||||
@click.stop="onClick"
|
|
||||||
>
|
|
||||||
<i-lucide:eye-off v-if="isShown"/>
|
|
||||||
<i-lucide:eye v-else/>
|
|
||||||
</Button>
|
|
||||||
</template>
|
|
||||||
</TreeExplorerTreeNode>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import Button from 'primevue/button'
|
import Button from 'primevue/button'
|
||||||
@@ -23,9 +5,7 @@ import { useDomWidgetStore } from '@/stores/domWidgetStore'
|
|||||||
import { useCanvasStore } from '@/stores/graphStore'
|
import { useCanvasStore } from '@/stores/graphStore'
|
||||||
|
|
||||||
function hasWidget() {
|
function hasWidget() {
|
||||||
const node = props.node.data[2]
|
return props.node.widgets.some((w) => w.name === props.item[1].name)
|
||||||
const name = props.node.data[1].name
|
|
||||||
return node.widgets.some((w) => w.name === name)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let isShown = ref(false)
|
let isShown = ref(false)
|
||||||
@@ -34,7 +14,9 @@ import TreeExplorerTreeNode from '@/components/common/TreeExplorerTreeNode.vue'
|
|||||||
import { RenderedTreeExplorerNode } from '@/types/treeExplorerTypes'
|
import { RenderedTreeExplorerNode } from '@/types/treeExplorerTypes'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
node: RenderedTreeExplorerNode<ComfyNodeDefImpl>
|
item: [unknown, unknown],
|
||||||
|
node: unknown
|
||||||
|
draggable?: boolean
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
@@ -43,9 +25,9 @@ onMounted(() => {
|
|||||||
|
|
||||||
function onClick(e) {
|
function onClick(e) {
|
||||||
//props.node?.onToggle()
|
//props.node?.onToggle()
|
||||||
const nodeId = props.node.data[0].id
|
const nodeId = props.item[0].id
|
||||||
const widgetName = props.node.data[1].name
|
const widgetName = props.item[1].name
|
||||||
const node = props.node.data[2]
|
const node = props.node
|
||||||
|
|
||||||
const { widgetStates } = useDomWidgetStore()
|
const { widgetStates } = useDomWidgetStore()
|
||||||
if (!isShown.value) {
|
if (!isShown.value) {
|
||||||
@@ -65,7 +47,7 @@ function onClick(e) {
|
|||||||
}
|
}
|
||||||
const { properties } = node
|
const { properties } = node
|
||||||
properties.proxyWidgets = properties.proxyWidgets.filter((p) => {
|
properties.proxyWidgets = properties.proxyWidgets.filter((p) => {
|
||||||
return p[1] !== widgetName
|
return p[1] !== widgetName
|
||||||
//NOTE: intentional loose as nodeId is often string/int
|
//NOTE: intentional loose as nodeId is often string/int
|
||||||
|| p[0] != nodeId})
|
|| p[0] != nodeId})
|
||||||
|
|
||||||
@@ -74,6 +56,67 @@ function onClick(e) {
|
|||||||
useCanvasStore().canvas.setDirty(true)
|
useCanvasStore().canvas.setDirty(true)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
<template>
|
||||||
|
<div class="widget-item">
|
||||||
|
<div v-if="draggable">
|
||||||
|
<i-lucide:grip-vertical/>
|
||||||
|
</div>
|
||||||
|
<div class="widget-title">
|
||||||
|
<div class="widget-node">{{item[0].title}}</div>
|
||||||
|
<div class="widget-name">{{item[1].name}}</div>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
text
|
||||||
|
severity="secondary"
|
||||||
|
@click.stop="onClick"
|
||||||
|
>
|
||||||
|
<i-lucide:eye v-if="isShown"/>
|
||||||
|
<i-lucide:eye-off v-else/>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
.widget-item {
|
||||||
|
display: flex;
|
||||||
|
padding: 4px 16px 4px 0;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
align-self: stretch;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: var(--bg-color, #202020);
|
||||||
|
}
|
||||||
|
.widget-title {
|
||||||
|
display: flex;
|
||||||
|
width: 269px;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
.widget-node {
|
||||||
|
display: flex;
|
||||||
|
height: 15px;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-self: stretch;
|
||||||
|
color: var(--color-text-secondary, #9C9EAB);
|
||||||
|
|
||||||
|
/* heading-text-nav */
|
||||||
|
font-family: Inter;
|
||||||
|
font-size: 10px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: normal;
|
||||||
|
}
|
||||||
|
.widget-name {
|
||||||
|
color: var(--color-text-primary, #FFF);
|
||||||
|
|
||||||
|
/* body-text-small */
|
||||||
|
font-family: Inter;
|
||||||
|
font-size: 12px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: normal;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -148,7 +148,8 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
|
|||||||
)
|
)
|
||||||
|
|
||||||
this.type = subgraph.id
|
this.type = subgraph.id
|
||||||
this.configure(instanceData)
|
////FIXME: This breaks subgraph conversion
|
||||||
|
//this.configure(instanceData)
|
||||||
|
|
||||||
this.addTitleButton({
|
this.addTitleButton({
|
||||||
name: 'enter_subgraph',
|
name: 'enter_subgraph',
|
||||||
@@ -305,6 +306,23 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
//ensure proxyWidgets is enumerated during serialization
|
||||||
|
this.properties.proxyWidgets ??= []
|
||||||
|
const proxyWidgets = this.properties.proxyWidgets
|
||||||
|
Object.defineProperty(this.properties, 'proxyWidgets', {
|
||||||
|
get: () => {
|
||||||
|
return this.widgets.filter((w) => !!w._overlay)
|
||||||
|
.map((w) => [w._overlay.nodeId, w._overlay.widgetName])
|
||||||
|
},
|
||||||
|
set: (property) => {
|
||||||
|
//NOTE: This does not apply to pushed entries, only initial load
|
||||||
|
this.widgets = this.widgets.filter((w) => !w._overlay)
|
||||||
|
for (const [nodeId, widgetName] of property)
|
||||||
|
this.addProxyWidget(`${nodeId}`, widgetName)
|
||||||
|
//TODO: set dirty canvas
|
||||||
|
}
|
||||||
|
})
|
||||||
|
this.properties.proxyWidgets = proxyWidgets
|
||||||
}
|
}
|
||||||
|
|
||||||
#setWidget(
|
#setWidget(
|
||||||
@@ -576,21 +594,8 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
|
|||||||
// Call parent serialize method
|
// Call parent serialize method
|
||||||
return super.serialize()
|
return super.serialize()
|
||||||
}
|
}
|
||||||
onPropertyChanged(k, property) {
|
|
||||||
if (k !== "proxyWidgets") return
|
|
||||||
this.widgets = this.widgets.filter((w) => !w.isProxyWidget)
|
|
||||||
setTimeout(() => {
|
|
||||||
for (const [nodeId, widgetName] of property)
|
|
||||||
this.addProxyFromOverlay({
|
|
||||||
__proto__:{nodeId, widgetName}})
|
|
||||||
}, 0)
|
|
||||||
|
|
||||||
}
|
|
||||||
addProxyWidget(nodeId: string, widgetName: string) {
|
addProxyWidget(nodeId: string, widgetName: string) {
|
||||||
const overlay = {nodeId, widgetName}
|
const overlay = {nodeId, widgetName}
|
||||||
this.properties.proxyWidgets ??= []
|
|
||||||
//NOTE: This doesn't trigger onPropertyChanged
|
|
||||||
this.properties.proxyWidgets.push([overlay.nodeId, overlay.widgetName])
|
|
||||||
return this.addProxyFromOverlay({__proto__:overlay})
|
return this.addProxyFromOverlay({__proto__:overlay})
|
||||||
}
|
}
|
||||||
addProxyFromOverlay(overlay: Object) {
|
addProxyFromOverlay(overlay: Object) {
|
||||||
@@ -611,11 +616,14 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
|
|||||||
if (!n) return
|
if (!n) return
|
||||||
return n.widgets.find((w) => w.name === widgetName)
|
return n.widgets.find((w) => w.name === widgetName)
|
||||||
}
|
}
|
||||||
|
let lw = undefined
|
||||||
const handler = Object.fromEntries(['get', 'set', 'getPrototypeOf', 'ownKeys', 'has'].map((s) => {
|
const handler = Object.fromEntries(['get', 'set', 'getPrototypeOf', 'ownKeys', 'has'].map((s) => {
|
||||||
const func = function(t,p,...rest) {
|
const func = function(t,p,...rest) {
|
||||||
if (s == 'get' && p == '_overlay')
|
if (s == 'get' && p == '_overlay')
|
||||||
return overlay
|
return overlay
|
||||||
const lw = linkedWidget(overlay.graph, overlay.nodeId, overlay.widgetName)
|
if (!lw) {
|
||||||
|
lw = linkedWidget(overlay.graph, overlay.nodeId, overlay.widgetName)
|
||||||
|
}
|
||||||
if (s == 'get' && p == 'node') {
|
if (s == 'get' && p == 'node') {
|
||||||
return subgraphNode
|
return subgraphNode
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user