mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-08 06:30:04 +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:
@@ -155,7 +155,7 @@ const unifiedWidth = computed(() =>
|
||||
|
||||
const { focusMode } = storeToRefs(workspaceStore)
|
||||
|
||||
const { isSelectMode } = useAppMode()
|
||||
const { isSelectMode, isBuilderMode } = useAppMode()
|
||||
const { activeSidebarTabId, activeSidebarTab } = storeToRefs(sidebarTabStore)
|
||||
const { bottomPanelVisible } = storeToRefs(useBottomPanelStore())
|
||||
const { isOpen: rightSidePanelVisible } = storeToRefs(rightSidePanelStore)
|
||||
@@ -163,7 +163,9 @@ const showOffsideSplitter = computed(
|
||||
() => rightSidePanelVisible.value || isSelectMode.value
|
||||
)
|
||||
|
||||
const sidebarPanelVisible = computed(() => activeSidebarTab.value !== null)
|
||||
const sidebarPanelVisible = computed(
|
||||
() => activeSidebarTab.value !== null && !isBuilderMode.value
|
||||
)
|
||||
|
||||
const sidebarStateKey = computed(() => {
|
||||
return unifiedWidth.value
|
||||
|
||||
@@ -38,20 +38,28 @@ const workflowStore = useWorkflowStore()
|
||||
const { t } = useI18n()
|
||||
const canvas: LGraphCanvas = canvasStore.getCanvas()
|
||||
|
||||
const { mode, isArrangeMode } = useAppMode()
|
||||
const { isSelectMode, isArrangeMode } = useAppMode()
|
||||
const hoveringSelectable = ref(false)
|
||||
|
||||
provide(HideLayoutFieldKey, true)
|
||||
|
||||
workflowStore.activeWorkflow?.changeTracker?.reset()
|
||||
|
||||
function resolveNode(nodeId: NodeId) {
|
||||
return (
|
||||
app.rootGraph.getNodeById(nodeId) ??
|
||||
[...app.rootGraph.subgraphs.values()]
|
||||
.flatMap((sg) => sg.nodes)
|
||||
.find((n) => n.id == nodeId)
|
||||
)
|
||||
}
|
||||
|
||||
// Prune stale entries whose node/widget no longer exists, so the
|
||||
// DraggableList model always matches the rendered items.
|
||||
watchEffect(() => {
|
||||
const valid = appModeStore.selectedInputs.filter(([nodeId, widgetName]) => {
|
||||
const node = app.rootGraph.getNodeById(nodeId)
|
||||
return node?.widgets?.some((w) => w.name === widgetName)
|
||||
})
|
||||
const valid = appModeStore.selectedInputs.filter(([nodeId, widgetName]) =>
|
||||
resolveNode(nodeId)?.widgets?.some((w) => w.name === widgetName)
|
||||
)
|
||||
if (valid.length < appModeStore.selectedInputs.length) {
|
||||
appModeStore.selectedInputs = valid
|
||||
}
|
||||
@@ -60,7 +68,7 @@ watchEffect(() => {
|
||||
const arrangeInputs = computed(() =>
|
||||
appModeStore.selectedInputs
|
||||
.map(([nodeId, widgetName]) => {
|
||||
const node = app.rootGraph.getNodeById(nodeId)
|
||||
const node = resolveNode(nodeId)
|
||||
const widget = node?.widgets?.find((w) => w.name === widgetName)
|
||||
if (!node || !widget) return null
|
||||
return { nodeId, widgetName, node, widget }
|
||||
@@ -70,7 +78,7 @@ const arrangeInputs = computed(() =>
|
||||
|
||||
const inputsWithState = computed(() =>
|
||||
appModeStore.selectedInputs.map(([nodeId, widgetName]) => {
|
||||
const node = app.rootGraph.getNodeById(nodeId)
|
||||
const node = resolveNode(nodeId)
|
||||
const widget = node?.widgets?.find((w) => w.name === widgetName)
|
||||
if (!node || !widget) return { nodeId, widgetName }
|
||||
|
||||
@@ -168,14 +176,14 @@ function handleClick(e: MouseEvent) {
|
||||
if (!widget) {
|
||||
if (!node.constructor.nodeData?.output_node)
|
||||
return canvasInteractions.forwardEventToCanvas(e)
|
||||
const index = appModeStore.selectedOutputs.findIndex((id) => id === node.id)
|
||||
const index = appModeStore.selectedOutputs.findIndex((id) => id == node.id)
|
||||
if (index === -1) appModeStore.selectedOutputs.push(node.id)
|
||||
else appModeStore.selectedOutputs.splice(index, 1)
|
||||
return
|
||||
}
|
||||
|
||||
const index = appModeStore.selectedInputs.findIndex(
|
||||
([nodeId, widgetName]) => node.id === nodeId && widget.name === widgetName
|
||||
([nodeId, widgetName]) => node.id == nodeId && widget.name === widgetName
|
||||
)
|
||||
if (index === -1) appModeStore.selectedInputs.push([node.id, widget.name])
|
||||
else appModeStore.selectedInputs.splice(index, 1)
|
||||
@@ -239,6 +247,7 @@ const renderedInputs = computed<[string, MaybeRef<BoundStyle> | undefined][]>(
|
||||
:disabled="!appModeStore.selectedInputs.length"
|
||||
class="border-border-subtle border-b"
|
||||
:tooltip="`${t('linearMode.builder.inputsDesc')}\n${t('linearMode.builder.inputsExample')}`"
|
||||
:tooltip-delay="100"
|
||||
>
|
||||
<template #label>
|
||||
<div class="flex gap-3">
|
||||
@@ -274,7 +283,7 @@ const renderedInputs = computed<[string, MaybeRef<BoundStyle> | undefined][]>(
|
||||
() =>
|
||||
remove(
|
||||
appModeStore.selectedInputs,
|
||||
([id, name]) => nodeId === id && widgetName === name
|
||||
([id, name]) => nodeId == id && widgetName === name
|
||||
)
|
||||
"
|
||||
/>
|
||||
@@ -286,6 +295,7 @@ const renderedInputs = computed<[string, MaybeRef<BoundStyle> | undefined][]>(
|
||||
enable-empty-state
|
||||
:disabled="!appModeStore.selectedOutputs.length"
|
||||
:tooltip="`${t('linearMode.builder.outputsDesc')}\n${t('linearMode.builder.outputsExample')}`"
|
||||
:tooltip-delay="100"
|
||||
>
|
||||
<template #label>
|
||||
<div class="flex gap-3">
|
||||
@@ -319,12 +329,15 @@ const renderedInputs = computed<[string, MaybeRef<BoundStyle> | undefined][]>(
|
||||
"
|
||||
:title
|
||||
:sub-title="String(key)"
|
||||
:remove="() => remove(appModeStore.selectedOutputs, (k) => k === key)"
|
||||
:remove="() => remove(appModeStore.selectedOutputs, (k) => k == key)"
|
||||
/>
|
||||
</DraggableList>
|
||||
</PropertiesAccordionItem>
|
||||
|
||||
<Teleport v-if="mode === 'builder:select'" to="body">
|
||||
<Teleport
|
||||
v-if="isSelectMode && !settingStore.get('Comfy.VueNodes.Enabled')"
|
||||
to="body"
|
||||
>
|
||||
<div
|
||||
:class="
|
||||
cn(
|
||||
@@ -358,13 +371,19 @@ const renderedInputs = computed<[string, MaybeRef<BoundStyle> | undefined][]>(
|
||||
<div class="absolute top-0 right-0 size-8">
|
||||
<div
|
||||
v-if="isSelected"
|
||||
class="absolute -top-1/2 -right-1/2 size-full p-2 bg-warning-background rounded-lg"
|
||||
class="absolute -top-1/2 -right-1/2 size-full p-2 bg-warning-background rounded-lg cursor-pointer pointer-events-auto"
|
||||
@click.stop="
|
||||
remove(appModeStore.selectedOutputs, (k) => k == key)
|
||||
"
|
||||
@pointerdown.stop
|
||||
>
|
||||
<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"
|
||||
class="absolute -top-1/2 -right-1/2 size-full ring-warning-background/50 ring-4 ring-inset bg-component-node-background rounded-lg cursor-pointer pointer-events-auto"
|
||||
@click.stop="appModeStore.selectedOutputs.push(key)"
|
||||
@pointerdown.stop
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -33,8 +33,11 @@ const entries = computed(() => {
|
||||
</script>
|
||||
<template>
|
||||
<div class="p-2 my-2 rounded-lg flex items-center-safe">
|
||||
<span class="mr-auto" v-text="title" />
|
||||
<span class="text-muted-foreground mr-2 text-end" v-text="subTitle" />
|
||||
<span class="mr-auto truncate shrink-1" v-text="title" />
|
||||
<span
|
||||
class="text-muted-foreground mr-2 text-end truncate shrink-3"
|
||||
v-text="subTitle"
|
||||
/>
|
||||
<Popover :entries>
|
||||
<template #button>
|
||||
<Button variant="muted-textonly">
|
||||
|
||||
@@ -11,6 +11,7 @@ const {
|
||||
enableEmptyState,
|
||||
tooltip,
|
||||
size = 'default',
|
||||
tooltipDelay = 1000,
|
||||
class: className
|
||||
} = defineProps<{
|
||||
disabled?: boolean
|
||||
@@ -18,6 +19,7 @@ const {
|
||||
enableEmptyState?: boolean
|
||||
tooltip?: string
|
||||
size?: 'default' | 'lg'
|
||||
tooltipDelay?: number
|
||||
class?: string
|
||||
}>()
|
||||
|
||||
@@ -27,7 +29,7 @@ const isExpanded = computed(() => !isCollapse.value && !disabled)
|
||||
|
||||
const tooltipConfig = computed(() => {
|
||||
if (!tooltip) return undefined
|
||||
return { value: tooltip, showDelay: 1000 }
|
||||
return { value: tooltip, showDelay: tooltipDelay }
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { computed, ref } from 'vue'
|
||||
import type { Ref } from 'vue'
|
||||
|
||||
import type { LGraphGroup, LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { isPromotedWidgetView } from '@/core/graph/subgraph/promotedWidgetTypes'
|
||||
import type {
|
||||
LGraphGroup,
|
||||
LGraphNode,
|
||||
NodeId
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { getExtraOptionsForWidget } from '@/services/litegraphService'
|
||||
import { isLGraphGroup } from '@/utils/litegraphUtil'
|
||||
@@ -46,7 +51,7 @@ export enum BadgeVariant {
|
||||
// Global singleton for NodeOptions component reference
|
||||
let nodeOptionsInstance: null | NodeOptionsInstance = null
|
||||
|
||||
const hoveredWidgetName = ref<string>()
|
||||
const hoveredWidget = ref<[string, NodeId | undefined]>()
|
||||
|
||||
/**
|
||||
* Toggle the node options popover
|
||||
@@ -63,14 +68,12 @@ export function toggleNodeOptions(event: Event) {
|
||||
* Use this for contextmenu events where we always want to show at the new position
|
||||
* @param event - The trigger event (must be MouseEvent for position)
|
||||
*/
|
||||
export function showNodeOptions(event: MouseEvent) {
|
||||
hoveredWidgetName.value = undefined
|
||||
const target = event.target
|
||||
if (target instanceof HTMLElement) {
|
||||
const widgetEl = target.closest('.lg-node-widget')
|
||||
if (widgetEl instanceof HTMLElement)
|
||||
hoveredWidgetName.value = widgetEl.dataset.widgetName
|
||||
}
|
||||
export function showNodeOptions(
|
||||
event: MouseEvent,
|
||||
widgetName?: string,
|
||||
nodeId?: NodeId
|
||||
) {
|
||||
hoveredWidget.value = widgetName ? [widgetName, nodeId] : undefined
|
||||
if (nodeOptionsInstance?.show) {
|
||||
nodeOptionsInstance.show(event)
|
||||
}
|
||||
@@ -259,8 +262,16 @@ export function useMoreOptionsMenu() {
|
||||
options.push(...getImageMenuOptions(selectedNodes.value[0]))
|
||||
options.push({ type: 'divider' })
|
||||
}
|
||||
const rawName = hoveredWidgetName.value
|
||||
const widget = node?.widgets?.find((w) => w.name === rawName)
|
||||
const [widgetName, nodeId] = hoveredWidget.value ?? []
|
||||
const widget =
|
||||
nodeId !== undefined
|
||||
? node?.widgets?.find(
|
||||
(w) =>
|
||||
isPromotedWidgetView(w) &&
|
||||
w.sourceWidgetName === widgetName &&
|
||||
w.sourceNodeId === nodeId
|
||||
)
|
||||
: node?.widgets?.find((w) => w.name === widgetName)
|
||||
if (widget) {
|
||||
const widgetOptions = convertContextMenuToOptions(
|
||||
getExtraOptionsForWidget(node, widget)
|
||||
|
||||
@@ -3056,11 +3056,11 @@
|
||||
"promptAddInputs": "Click on node parameters to add them here as inputs",
|
||||
"noInputs": "No inputs added yet",
|
||||
"inputsDesc": "Users will interact and adjust these to generate their outputs.",
|
||||
"inputsExample": "Examples: “Load image”, “Text prompt”, “Steps”",
|
||||
"inputsExample": "Examples: “Load\u00a0image”, “Text\u00a0prompt”, “Steps”",
|
||||
"promptAddOutputs": "Click on output nodes to add them here. These will be the generated results.",
|
||||
"noOutputs": "No output nodes added yet",
|
||||
"outputsDesc": "Connect at least one output node so users can see results after running.",
|
||||
"outputsExample": "Examples: “Save Image” or “Save Video”"
|
||||
"outputsExample": "Examples: “Save\u00a0Image” or “Save\u00a0Video”"
|
||||
},
|
||||
"queue": {
|
||||
"clickToClear": "Click to clear queue",
|
||||
|
||||
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