mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-13 17:10:06 +00:00
Support selection of app inputs and outputs from vue mode (#9259)
- The input and output indicators are now plugged directly into the `LGraphNode.vue` template. Care was taken to make implementation to have low cost for performance and complexity when not in app mode setup. - Context menu event handlers are added to each widget in vue mode instead of resolving the target widget of an event - Swap the nodeId passed by `useGraphNodeManager` to not include the locator id. This id was never used and was incorrect since it didn't resolve across nested subgraphs. - Continued bug fixes for app mode as a whole. Known issue: There is disparity of nodeId between litegraph (which references the widget in the root graph) and vue (which promotes the original widget). Efforts to reconcile are ongoing. ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-9259-Support-selection-app-inputs-and-outputs-from-vue-mode-3136d73d365081ae8e56e35bf6322409) by [Unito](https://www.unito.io) --------- Co-authored-by: pythongosssss <125205205+pythongosssss@users.noreply.github.com>
This commit is contained in:
62
src/renderer/extensions/linearMode/AppInput.vue
Normal file
62
src/renderer/extensions/linearMode/AppInput.vue
Normal file
@@ -0,0 +1,62 @@
|
||||
<script setup lang="ts">
|
||||
import { remove } from 'es-toolkit'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import type { NodeId } from '@/lib/litegraph/src/LGraphNode'
|
||||
import { useAppModeStore } from '@/stores/appModeStore'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
const { id, name } = defineProps<{
|
||||
id: string
|
||||
isSelectMode: boolean
|
||||
name: string
|
||||
}>()
|
||||
|
||||
const appModeStore = useAppModeStore()
|
||||
const isPromoted = computed(() => appModeStore.selectedInputs.some(matchesThis))
|
||||
|
||||
function matchesThis([nodeId, widgetName]: [NodeId, string]) {
|
||||
return id == nodeId && name === widgetName
|
||||
}
|
||||
function togglePromotion() {
|
||||
if (isPromoted.value) remove(appModeStore.selectedInputs, matchesThis)
|
||||
else appModeStore.selectedInputs.push([id, name])
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div
|
||||
v-if="isSelectMode"
|
||||
class="col-span-2 flex flex-row pointer-events-auto cursor-pointer gap-1 relative"
|
||||
@pointerdown.capture.stop.prevent="togglePromotion"
|
||||
@click.capture.stop.prevent
|
||||
@pointerup.capture.stop.prevent
|
||||
@pointermove.capture.stop.prevent
|
||||
@contextmenu.capture.stop.prevent
|
||||
>
|
||||
<div
|
||||
:class="
|
||||
cn(
|
||||
'border-primary-background border rounded-sm size-4 self-center m-1',
|
||||
isPromoted && 'bg-primary-background flex items-center'
|
||||
)
|
||||
"
|
||||
>
|
||||
<i
|
||||
v-if="isPromoted"
|
||||
class="icon-[lucide--check] bg-primary-foreground place-center"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
:class="
|
||||
cn(
|
||||
'grid grid-cols-2 items-stretch ring-primary-background rounded-lg pointer-events-none flex-1',
|
||||
isPromoted && 'ring-2'
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
<div class="absolute size-full hover:bg-primary-background/10 rounded-lg" />
|
||||
</div>
|
||||
<slot v-else />
|
||||
</template>
|
||||
51
src/renderer/extensions/linearMode/AppOutput.vue
Normal file
51
src/renderer/extensions/linearMode/AppOutput.vue
Normal file
@@ -0,0 +1,51 @@
|
||||
<script setup lang="ts">
|
||||
import { remove } from 'es-toolkit'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import type { NodeId } from '@/lib/litegraph/src/LGraphNode'
|
||||
import { useAppModeStore } from '@/stores/appModeStore'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
const { id } = defineProps<{ id: string }>()
|
||||
|
||||
const appModeStore = useAppModeStore()
|
||||
const isPromoted = computed(() =>
|
||||
appModeStore.selectedOutputs.some(matchesThis)
|
||||
)
|
||||
|
||||
function matchesThis(nodeId: NodeId) {
|
||||
return id == nodeId
|
||||
}
|
||||
function togglePromotion() {
|
||||
if (isPromoted.value) remove(appModeStore.selectedOutputs, matchesThis)
|
||||
else appModeStore.selectedOutputs.push(id)
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div
|
||||
:class="
|
||||
cn(
|
||||
'absolute w-full h-full pointer-events-auto ring-warning-background/50 ring-5 rounded-2xl',
|
||||
isPromoted && 'ring-warning-background'
|
||||
)
|
||||
"
|
||||
@click.capture.stop.prevent
|
||||
@pointerup.capture.stop.prevent
|
||||
@pointermove.capture.stop.prevent
|
||||
@pointerdown.capture.stop="togglePromotion"
|
||||
@contextmenu.capture.stop.prevent
|
||||
>
|
||||
<div class="absolute top-0 right-0 size-8">
|
||||
<div
|
||||
v-if="isPromoted"
|
||||
class="absolute -top-1/2 -right-1/2 size-full p-2 bg-warning-background rounded-lg"
|
||||
>
|
||||
<i class="icon-[lucide--check] bg-text-foreground size-full" />
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="absolute -top-1/2 -right-1/2 size-full ring-warning-background/50 ring-4 ring-inset bg-component-node-background rounded-lg"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -16,7 +16,6 @@ import { useTelemetry } from '@/platform/telemetry'
|
||||
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
|
||||
import DropZone from '@/renderer/extensions/linearMode/DropZone.vue'
|
||||
import NodeWidgets from '@/renderer/extensions/vueNodes/components/NodeWidgets.vue'
|
||||
import { applyLightThemeColor } from '@/renderer/extensions/vueNodes/utils/nodeStyleUtils'
|
||||
import WidgetInputNumberInput from '@/renderer/extensions/vueNodes/widgets/components/WidgetInputNumber.vue'
|
||||
import { api } from '@/scripts/api'
|
||||
import { app } from '@/scripts/app'
|
||||
@@ -69,7 +68,11 @@ const mappedSelections = computed(() => {
|
||||
([id]) => id === nodeId
|
||||
).map(([, widgetName]) => widgetName)
|
||||
unprocessedInputs = unprocessedInputs.slice(inputGroup.length)
|
||||
const node = app.rootGraph.getNodeById(nodeId)
|
||||
const node =
|
||||
app.rootGraph.getNodeById(nodeId) ??
|
||||
[...app.rootGraph.subgraphs.values()]
|
||||
.flatMap((sg) => sg.nodes)
|
||||
.find((n) => n.id == nodeId)
|
||||
if (!node) continue
|
||||
|
||||
const nodeData = nodeToNodeData(node)
|
||||
@@ -239,7 +242,6 @@ defineExpose({ runButtonClick })
|
||||
/>
|
||||
<NodeWidgets
|
||||
:node-data
|
||||
:style="{ background: applyLightThemeColor(nodeData.bgcolor) }"
|
||||
class="py-3 gap-y-3 **:[.col-span-2]:grid-cols-1 *:has-[textarea]:h-50 rounded-lg max-w-100"
|
||||
/>
|
||||
</template>
|
||||
@@ -279,7 +281,6 @@ defineExpose({ runButtonClick })
|
||||
'ring-2 ring-inset ring-node-stroke-error'
|
||||
)
|
||||
"
|
||||
:style="{ background: applyLightThemeColor(nodeData.bgcolor) }"
|
||||
/>
|
||||
</DropZone>
|
||||
</template>
|
||||
|
||||
@@ -50,6 +50,10 @@
|
||||
@dragleave="handleDragLeave"
|
||||
@drop.stop.prevent="handleDrop"
|
||||
>
|
||||
<AppOutput
|
||||
v-if="lgraphNode?.constructor?.nodeData?.output_node && isSelectMode"
|
||||
:id="nodeData.id"
|
||||
/>
|
||||
<div
|
||||
v-if="displayHeader"
|
||||
class="flex flex-col justify-center items-center relative"
|
||||
@@ -265,6 +269,7 @@ import { useI18n } from 'vue-i18n'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
|
||||
import { showNodeOptions } from '@/composables/graph/useMoreOptionsMenu'
|
||||
import { useAppMode } from '@/composables/useAppMode'
|
||||
import { useErrorHandling } from '@/composables/useErrorHandling'
|
||||
import { hasUnpromotedWidgets } from '@/core/graph/subgraph/unpromotedWidgetUtils'
|
||||
import { st } from '@/i18n'
|
||||
@@ -284,6 +289,7 @@ import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
|
||||
import { usePromotedPreviews } from '@/composables/node/usePromotedPreviews'
|
||||
import NodeBadges from '@/renderer/extensions/vueNodes/components/NodeBadges.vue'
|
||||
import { LayoutSource } from '@/renderer/core/layout/types'
|
||||
import AppOutput from '@/renderer/extensions/linearMode/AppOutput.vue'
|
||||
import SlotConnectionDot from '@/renderer/extensions/vueNodes/components/SlotConnectionDot.vue'
|
||||
import { useNodeEventHandlers } from '@/renderer/extensions/vueNodes/composables/useNodeEventHandlers'
|
||||
import { useNodePointerInteractions } from '@/renderer/extensions/vueNodes/composables/useNodePointerInteractions'
|
||||
@@ -329,6 +335,7 @@ const { nodeData, error = null } = defineProps<LGraphNodeProps>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const { isSelectMode } = useAppMode()
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
const { handleNodeCollapse, handleNodeTitleUpdate, handleNodeRightClick } =
|
||||
|
||||
@@ -28,7 +28,6 @@
|
||||
<div
|
||||
v-if="!widget.hidden && (!widget.advanced || showAdvanced)"
|
||||
class="lg-node-widget group col-span-full grid grid-cols-subgrid items-stretch"
|
||||
:data-widget-name="widget.name"
|
||||
>
|
||||
<!-- Widget Input Slot Dot -->
|
||||
<div
|
||||
@@ -54,21 +53,24 @@
|
||||
/>
|
||||
</div>
|
||||
<!-- Widget Component -->
|
||||
<component
|
||||
:is="widget.vueComponent"
|
||||
v-model="widget.value"
|
||||
v-tooltip.left="widget.tooltipConfig"
|
||||
:widget="widget.simplified"
|
||||
:node-id="nodeData?.id != null ? String(nodeData.id) : ''"
|
||||
:node-type="nodeType"
|
||||
:class="
|
||||
cn(
|
||||
'col-span-2',
|
||||
widget.hasError && 'text-node-stroke-error font-bold'
|
||||
)
|
||||
"
|
||||
@update:model-value="widget.updateHandler"
|
||||
/>
|
||||
<AppInput :id="widget.id" :name="widget.name" :is-select-mode>
|
||||
<component
|
||||
:is="widget.vueComponent"
|
||||
v-model="widget.value"
|
||||
v-tooltip.left="widget.tooltipConfig"
|
||||
:widget="widget.simplified"
|
||||
:node-id="nodeData?.id != null ? String(nodeData.id) : ''"
|
||||
:node-type="nodeType"
|
||||
:class="
|
||||
cn(
|
||||
'col-span-2',
|
||||
widget.hasError && 'text-node-stroke-error font-bold'
|
||||
)
|
||||
"
|
||||
@update:model-value="widget.updateHandler"
|
||||
@contextmenu="widget.handleContextMenu"
|
||||
/>
|
||||
</AppInput>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
@@ -83,12 +85,16 @@ import type {
|
||||
VueNodeData,
|
||||
WidgetSlotMetadata
|
||||
} from '@/composables/graph/useGraphNodeManager'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useAppMode } from '@/composables/useAppMode'
|
||||
import { showNodeOptions } from '@/composables/graph/useMoreOptionsMenu'
|
||||
import { useErrorHandling } from '@/composables/useErrorHandling'
|
||||
import { st } from '@/i18n'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import AppInput from '@/renderer/extensions/linearMode/AppInput.vue'
|
||||
import { useNodeTooltips } from '@/renderer/extensions/vueNodes/composables/useNodeTooltips'
|
||||
import { useNodeEventHandlers } from '@/renderer/extensions/vueNodes/composables/useNodeEventHandlers'
|
||||
import { useNodeZIndex } from '@/renderer/extensions/vueNodes/composables/useNodeZIndex'
|
||||
import WidgetDOM from '@/renderer/extensions/vueNodes/widgets/components/WidgetDOM.vue'
|
||||
// Import widget components directly
|
||||
@@ -117,6 +123,7 @@ const { nodeData } = defineProps<NodeWidgetsProps>()
|
||||
|
||||
const { shouldHandleNodePointerEvents, forwardEventToCanvas } =
|
||||
useCanvasInteractions()
|
||||
const { isSelectMode } = useAppMode()
|
||||
const canvasStore = useCanvasStore()
|
||||
const { bringNodeToFront } = useNodeZIndex()
|
||||
const promotionStore = usePromotionStore()
|
||||
@@ -134,6 +141,8 @@ function handleBringToFront() {
|
||||
}
|
||||
}
|
||||
|
||||
const { handleNodeRightClick } = useNodeEventHandlers()
|
||||
|
||||
// Error boundary implementation
|
||||
const renderError = ref<string | null>(null)
|
||||
|
||||
@@ -159,9 +168,11 @@ const widgetValueStore = useWidgetValueStore()
|
||||
|
||||
interface ProcessedWidget {
|
||||
advanced: boolean
|
||||
handleContextMenu: (e: PointerEvent) => void
|
||||
hasLayoutSize: boolean
|
||||
hasError: boolean
|
||||
hidden: boolean
|
||||
id: string
|
||||
name: string
|
||||
simplified: SimplifiedWidget
|
||||
tooltipConfig: TooltipOptions
|
||||
@@ -240,15 +251,29 @@ const processedWidgets = computed((): ProcessedWidget[] => {
|
||||
|
||||
const tooltipText = getWidgetTooltip(widget)
|
||||
const tooltipConfig = createTooltipConfig(tooltipText)
|
||||
const handleContextMenu = (e: PointerEvent) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
handleNodeRightClick(e, nodeId)
|
||||
showNodeOptions(
|
||||
e,
|
||||
widget.name,
|
||||
widget.nodeId !== undefined
|
||||
? String(stripGraphPrefix(widget.nodeId))
|
||||
: undefined
|
||||
)
|
||||
}
|
||||
|
||||
result.push({
|
||||
advanced: widget.options?.advanced ?? false,
|
||||
handleContextMenu,
|
||||
hasLayoutSize: widget.hasLayoutSize ?? false,
|
||||
hasError:
|
||||
nodeErrors?.errors?.some(
|
||||
(error) => error.extra_info?.input_name === widget.name
|
||||
) ?? false,
|
||||
hidden: widget.options?.hidden ?? false,
|
||||
id: String(bareWidgetId),
|
||||
name: widget.name,
|
||||
type: widget.type,
|
||||
vueComponent,
|
||||
|
||||
Reference in New Issue
Block a user