mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-19 22:09:37 +00:00
[backport core/1.42] fix/feat: App mode - Persist user resized widget heights (#11142)
Backport of #10993 to `core/1.42` Automatically created by backport workflow. ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-11142-backport-core-1-42-fix-feat-App-mode-Persist-user-resized-widget-heights-33e6d73d3650811a8960cf8be33dd27a) by [Unito](https://www.unito.io) --------- Co-authored-by: pythongosssss <125205205+pythongosssss@users.noreply.github.com>
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { useEventListener } from '@vueuse/core'
|
||||
import { computed, provide, shallowRef } from 'vue'
|
||||
|
||||
import { useAppModeWidgetResizing } from '@/components/builder/useAppModeWidgetResizing'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import Popover from '@/components/ui/Popover.vue'
|
||||
@@ -8,7 +10,7 @@ import Button from '@/components/ui/button/Button.vue'
|
||||
import { extractVueNodeData } from '@/composables/graph/useGraphNodeManager'
|
||||
import { OverlayAppendToKey } from '@/composables/useTransformCompatOverlayProps'
|
||||
import { isPromotedWidgetView } from '@/core/graph/subgraph/promotedWidgetTypes'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
||||
import type { LGraphNode, NodeId } from '@/lib/litegraph/src/LGraphNode'
|
||||
import { LGraphEventMode } from '@/lib/litegraph/src/types/globalEnums'
|
||||
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
import { useMaskEditor } from '@/composables/maskeditor/useMaskEditor'
|
||||
@@ -28,6 +30,9 @@ import { promptRenameWidget } from '@/utils/widgetUtil'
|
||||
|
||||
interface WidgetEntry {
|
||||
key: string
|
||||
nodeId: NodeId
|
||||
widgetName: string
|
||||
persistedHeight: number | undefined
|
||||
nodeData: ReturnType<typeof nodeToNodeData> & {
|
||||
widgets: NonNullable<ReturnType<typeof nodeToNodeData>['widgets']>
|
||||
}
|
||||
@@ -44,6 +49,11 @@ const executionErrorStore = useExecutionErrorStore()
|
||||
const appModeStore = useAppModeStore()
|
||||
const maskEditor = useMaskEditor()
|
||||
|
||||
const { onPointerDown } = useAppModeWidgetResizing(
|
||||
(nodeId, widgetName, config) =>
|
||||
appModeStore.updateInputConfig(nodeId, widgetName, config)
|
||||
)
|
||||
|
||||
provide(HideLayoutFieldKey, true)
|
||||
provide(OverlayAppendToKey, 'body')
|
||||
|
||||
@@ -61,7 +71,7 @@ const mappedSelections = computed((): WidgetEntry[] => {
|
||||
ReturnType<typeof nodeToNodeData>
|
||||
>()
|
||||
|
||||
return appModeStore.selectedInputs.flatMap(([nodeId, widgetName]) => {
|
||||
return appModeStore.selectedInputs.flatMap(([nodeId, widgetName, config]) => {
|
||||
const [node, widget] = resolveNodeWidget(nodeId, widgetName)
|
||||
if (!widget || !node || node.mode !== LGraphEventMode.ALWAYS) return []
|
||||
|
||||
@@ -90,6 +100,9 @@ const mappedSelections = computed((): WidgetEntry[] => {
|
||||
return [
|
||||
{
|
||||
key: `${nodeId}:${widgetName}`,
|
||||
nodeId,
|
||||
widgetName,
|
||||
persistedHeight: config?.height,
|
||||
nodeData: {
|
||||
...fullNodeData,
|
||||
widgets: [matchingWidget]
|
||||
@@ -142,7 +155,14 @@ function nodeToNodeData(node: LGraphNode) {
|
||||
</script>
|
||||
<template>
|
||||
<div
|
||||
v-for="{ key, nodeData, action } in mappedSelections"
|
||||
v-for="{
|
||||
key,
|
||||
nodeId,
|
||||
widgetName,
|
||||
persistedHeight,
|
||||
nodeData,
|
||||
action
|
||||
} in mappedSelections"
|
||||
:key
|
||||
:class="
|
||||
cn(
|
||||
@@ -204,8 +224,20 @@ function nodeToNodeData(node: LGraphNode) {
|
||||
</Popover>
|
||||
</div>
|
||||
<div
|
||||
:class="builderMode && 'pointer-events-none'"
|
||||
:style="
|
||||
persistedHeight
|
||||
? { '--persisted-height': `${persistedHeight}px` }
|
||||
: undefined
|
||||
"
|
||||
:class="
|
||||
cn(
|
||||
builderMode && 'pointer-events-none',
|
||||
persistedHeight &&
|
||||
'**:data-[slot=drop-zone-indicator]:h-(--persisted-height) [&_textarea]:h-(--persisted-height)'
|
||||
)
|
||||
"
|
||||
:inert="builderMode || undefined"
|
||||
@pointerdown.capture="(e) => onPointerDown(nodeId, widgetName, e)"
|
||||
>
|
||||
<DropZone
|
||||
:on-drag-over="nodeData.onDragOver"
|
||||
|
||||
210
src/components/builder/useAppModeWidgetResizing.test.ts
Normal file
210
src/components/builder/useAppModeWidgetResizing.test.ts
Normal file
@@ -0,0 +1,210 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { effectScope } from 'vue'
|
||||
|
||||
import type { NodeId } from '@/lib/litegraph/src/LGraphNode'
|
||||
import type { InputWidgetConfig } from '@/platform/workflow/management/stores/comfyWorkflow'
|
||||
|
||||
import { useAppModeWidgetResizing } from './useAppModeWidgetResizing'
|
||||
|
||||
function setHeight(el: HTMLElement, height: number) {
|
||||
Object.defineProperty(el, 'offsetHeight', {
|
||||
value: height,
|
||||
configurable: true
|
||||
})
|
||||
}
|
||||
|
||||
function wrapWithTextarea(initialHeight = 100): {
|
||||
wrapper: HTMLDivElement
|
||||
textarea: HTMLTextAreaElement
|
||||
} {
|
||||
const wrapper = document.createElement('div')
|
||||
const textarea = document.createElement('textarea')
|
||||
wrapper.appendChild(textarea)
|
||||
document.body.appendChild(wrapper)
|
||||
setHeight(textarea, initialHeight)
|
||||
return { wrapper, textarea }
|
||||
}
|
||||
|
||||
describe('useAppModeWidgetResizing', () => {
|
||||
function setup() {
|
||||
const onResize =
|
||||
vi.fn<
|
||||
(nodeId: NodeId, widgetName: string, config: InputWidgetConfig) => void
|
||||
>()
|
||||
const { onPointerDown } = useAppModeWidgetResizing(onResize)
|
||||
|
||||
function bind(wrapper: HTMLElement, nodeId: NodeId, widgetName: string) {
|
||||
wrapper.addEventListener(
|
||||
'pointerdown',
|
||||
(e) => onPointerDown(nodeId, widgetName, e as PointerEvent),
|
||||
{ capture: true }
|
||||
)
|
||||
}
|
||||
|
||||
return { onResize, bind }
|
||||
}
|
||||
|
||||
it('persists height when textarea is resized via drag', () => {
|
||||
const { bind, onResize } = setup()
|
||||
const { wrapper, textarea } = wrapWithTextarea()
|
||||
bind(wrapper, 1 as NodeId, 'prompt')
|
||||
|
||||
textarea.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true }))
|
||||
setHeight(textarea, 250)
|
||||
window.dispatchEvent(new PointerEvent('pointerup'))
|
||||
|
||||
expect(onResize).toHaveBeenCalledWith(1, 'prompt', { height: 250 })
|
||||
})
|
||||
|
||||
it('does not persist when no height change occurs (e.g. a click)', () => {
|
||||
const { bind, onResize } = setup()
|
||||
const { wrapper, textarea } = wrapWithTextarea()
|
||||
bind(wrapper, 1 as NodeId, 'prompt')
|
||||
|
||||
textarea.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true }))
|
||||
window.dispatchEvent(new PointerEvent('pointerup'))
|
||||
|
||||
expect(onResize).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('persists once per drag gesture; stray pointerup is a no-op', () => {
|
||||
const { bind, onResize } = setup()
|
||||
const { wrapper, textarea } = wrapWithTextarea()
|
||||
bind(wrapper, 1 as NodeId, 'prompt')
|
||||
|
||||
textarea.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true }))
|
||||
setHeight(textarea, 250)
|
||||
window.dispatchEvent(new PointerEvent('pointerup'))
|
||||
window.dispatchEvent(new PointerEvent('pointerup'))
|
||||
|
||||
expect(onResize).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('ignores pointerdown on non-resizable targets (label, button, popover)', () => {
|
||||
const { bind, onResize } = setup()
|
||||
const wrapper = document.createElement('div')
|
||||
const button = document.createElement('button')
|
||||
wrapper.appendChild(button)
|
||||
document.body.appendChild(wrapper)
|
||||
bind(wrapper, 1 as NodeId, 'prompt')
|
||||
|
||||
button.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true }))
|
||||
window.dispatchEvent(new PointerEvent('pointerup'))
|
||||
|
||||
expect(onResize).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('persists when target is a descendant of the drop-zone-indicator', () => {
|
||||
const { bind, onResize } = setup()
|
||||
const wrapper = document.createElement('div')
|
||||
const indicator = document.createElement('div')
|
||||
indicator.setAttribute('data-slot', 'drop-zone-indicator')
|
||||
const inner = document.createElement('span')
|
||||
indicator.appendChild(inner)
|
||||
wrapper.appendChild(indicator)
|
||||
document.body.appendChild(wrapper)
|
||||
setHeight(indicator, 100)
|
||||
bind(wrapper, 1 as NodeId, 'image')
|
||||
|
||||
inner.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true }))
|
||||
setHeight(indicator, 250)
|
||||
window.dispatchEvent(new PointerEvent('pointerup'))
|
||||
|
||||
expect(onResize).toHaveBeenCalledWith(1, 'image', { height: 250 })
|
||||
})
|
||||
|
||||
it('drops a stale gesture when a new pointerdown starts before pointerup arrives', () => {
|
||||
const { bind, onResize } = setup()
|
||||
const first = wrapWithTextarea()
|
||||
const second = wrapWithTextarea()
|
||||
bind(first.wrapper, 1 as NodeId, 'prompt')
|
||||
bind(second.wrapper, 2 as NodeId, 'other')
|
||||
|
||||
first.textarea.dispatchEvent(
|
||||
new PointerEvent('pointerdown', { bubbles: true })
|
||||
)
|
||||
setHeight(first.textarea, 250)
|
||||
|
||||
second.textarea.dispatchEvent(
|
||||
new PointerEvent('pointerdown', { bubbles: true })
|
||||
)
|
||||
setHeight(second.textarea, 300)
|
||||
window.dispatchEvent(new PointerEvent('pointerup'))
|
||||
|
||||
expect(onResize).toHaveBeenCalledTimes(1)
|
||||
expect(onResize).toHaveBeenCalledWith(2, 'other', { height: 300 })
|
||||
})
|
||||
|
||||
it('treats pointercancel as the end of a gesture and persists the new height', () => {
|
||||
const { bind, onResize } = setup()
|
||||
const { wrapper, textarea } = wrapWithTextarea()
|
||||
bind(wrapper, 1 as NodeId, 'prompt')
|
||||
|
||||
textarea.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true }))
|
||||
setHeight(textarea, 250)
|
||||
window.dispatchEvent(new PointerEvent('pointercancel'))
|
||||
|
||||
expect(onResize).toHaveBeenCalledWith(1, 'prompt', { height: 250 })
|
||||
})
|
||||
|
||||
it('after pointercancel, a subsequent stray pointerup is a no-op', () => {
|
||||
const { bind, onResize } = setup()
|
||||
const { wrapper, textarea } = wrapWithTextarea()
|
||||
bind(wrapper, 1 as NodeId, 'prompt')
|
||||
|
||||
textarea.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true }))
|
||||
setHeight(textarea, 250)
|
||||
window.dispatchEvent(new PointerEvent('pointercancel'))
|
||||
setHeight(textarea, 400)
|
||||
window.dispatchEvent(new PointerEvent('pointerup'))
|
||||
|
||||
expect(onResize).toHaveBeenCalledTimes(1)
|
||||
expect(onResize).toHaveBeenCalledWith(1, 'prompt', { height: 250 })
|
||||
})
|
||||
|
||||
it('removes global listeners when the owning scope is disposed mid-gesture', () => {
|
||||
const onResize =
|
||||
vi.fn<
|
||||
(nodeId: NodeId, widgetName: string, config: InputWidgetConfig) => void
|
||||
>()
|
||||
const scope = effectScope()
|
||||
const { onPointerDown } = scope.run(() =>
|
||||
useAppModeWidgetResizing(onResize)
|
||||
)!
|
||||
const { wrapper, textarea } = wrapWithTextarea()
|
||||
wrapper.addEventListener(
|
||||
'pointerdown',
|
||||
(e) => onPointerDown(1 as NodeId, 'prompt', e as PointerEvent),
|
||||
{ capture: true }
|
||||
)
|
||||
|
||||
textarea.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true }))
|
||||
setHeight(textarea, 250)
|
||||
scope.stop()
|
||||
window.dispatchEvent(new PointerEvent('pointerup'))
|
||||
window.dispatchEvent(new PointerEvent('pointercancel'))
|
||||
|
||||
expect(onResize).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('does not match a resizable that is an ancestor of the wrapper', () => {
|
||||
const { bind, onResize } = setup()
|
||||
// An unrelated drop-zone-indicator outside the wrapper would otherwise be
|
||||
// returned by target.closest(...) walking up the tree.
|
||||
const outerIndicator = document.createElement('div')
|
||||
outerIndicator.setAttribute('data-slot', 'drop-zone-indicator')
|
||||
const wrapper = document.createElement('div')
|
||||
const inner = document.createElement('span')
|
||||
wrapper.appendChild(inner)
|
||||
outerIndicator.appendChild(wrapper)
|
||||
document.body.appendChild(outerIndicator)
|
||||
setHeight(outerIndicator, 100)
|
||||
bind(wrapper, 1 as NodeId, 'prompt')
|
||||
|
||||
inner.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true }))
|
||||
setHeight(outerIndicator, 250)
|
||||
window.dispatchEvent(new PointerEvent('pointerup'))
|
||||
|
||||
expect(onResize).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
55
src/components/builder/useAppModeWidgetResizing.ts
Normal file
55
src/components/builder/useAppModeWidgetResizing.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { onScopeDispose } from 'vue'
|
||||
|
||||
import type { NodeId } from '@/lib/litegraph/src/LGraphNode'
|
||||
import type { InputWidgetConfig } from '@/platform/workflow/management/stores/comfyWorkflow'
|
||||
|
||||
const RESIZABLE_SELECTOR = 'textarea, [data-slot="drop-zone-indicator"]'
|
||||
|
||||
export function useAppModeWidgetResizing(
|
||||
onResize: (
|
||||
nodeId: NodeId,
|
||||
widgetName: string,
|
||||
config: InputWidgetConfig
|
||||
) => void
|
||||
) {
|
||||
let pendingHandler: (() => void) | null = null
|
||||
|
||||
function clearPendingHandler() {
|
||||
if (!pendingHandler) return
|
||||
window.removeEventListener('pointerup', pendingHandler)
|
||||
window.removeEventListener('pointercancel', pendingHandler)
|
||||
pendingHandler = null
|
||||
}
|
||||
|
||||
onScopeDispose(clearPendingHandler)
|
||||
|
||||
function onPointerDown(
|
||||
nodeId: NodeId,
|
||||
widgetName: string,
|
||||
event: PointerEvent
|
||||
) {
|
||||
const wrapper = event.currentTarget
|
||||
const target = event.target
|
||||
if (!(wrapper instanceof HTMLElement) || !(target instanceof HTMLElement))
|
||||
return
|
||||
const resizable = target.closest<HTMLElement>(RESIZABLE_SELECTOR)
|
||||
if (!resizable || !wrapper.contains(resizable)) return
|
||||
|
||||
clearPendingHandler()
|
||||
|
||||
const startHeight = resizable.offsetHeight
|
||||
const handler = () => {
|
||||
window.removeEventListener('pointerup', handler)
|
||||
window.removeEventListener('pointercancel', handler)
|
||||
pendingHandler = null
|
||||
const height = resizable.offsetHeight
|
||||
if (height === startHeight) return
|
||||
onResize(nodeId, widgetName, { height })
|
||||
}
|
||||
pendingHandler = handler
|
||||
window.addEventListener('pointerup', handler)
|
||||
window.addEventListener('pointercancel', handler)
|
||||
}
|
||||
|
||||
return { onPointerDown }
|
||||
}
|
||||
@@ -11,8 +11,14 @@ import type {
|
||||
} from '@/platform/workflow/validation/schemas/workflowSchema'
|
||||
import type { MissingNodeType } from '@/types/comfy'
|
||||
|
||||
export interface InputWidgetConfig {
|
||||
height?: number
|
||||
}
|
||||
|
||||
export type LinearInput = [NodeId, string, InputWidgetConfig?]
|
||||
|
||||
export interface LinearData {
|
||||
inputs: [NodeId, string][]
|
||||
inputs: LinearInput[]
|
||||
outputs: NodeId[]
|
||||
}
|
||||
|
||||
|
||||
@@ -66,6 +66,69 @@ describe('parseComfyWorkflow', () => {
|
||||
await expect(validateComfyWorkflow(workflow)).resolves.not.toBeNull()
|
||||
})
|
||||
|
||||
describe('linearData.inputs schema', () => {
|
||||
it('validates 2-tuple format (legacy)', async () => {
|
||||
const workflow = JSON.parse(JSON.stringify(defaultGraph))
|
||||
workflow.extra = {
|
||||
linearData: { inputs: [[1, 'prompt']], outputs: [1] }
|
||||
}
|
||||
const result = await validateComfyWorkflow(workflow)
|
||||
expect(result).not.toBeNull()
|
||||
expect(result!.extra!.linearData!.inputs).toEqual([[1, 'prompt']])
|
||||
})
|
||||
|
||||
it('validates 3-tuple format with config', async () => {
|
||||
const workflow = JSON.parse(JSON.stringify(defaultGraph))
|
||||
workflow.extra = {
|
||||
linearData: { inputs: [[1, 'prompt', { height: 200 }]], outputs: [] }
|
||||
}
|
||||
const result = await validateComfyWorkflow(workflow)
|
||||
expect(result).not.toBeNull()
|
||||
expect(result!.extra!.linearData!.inputs![0]).toEqual([
|
||||
1,
|
||||
'prompt',
|
||||
{ height: 200 }
|
||||
])
|
||||
})
|
||||
|
||||
it('validates 3-tuple format with empty config', async () => {
|
||||
const workflow = JSON.parse(JSON.stringify(defaultGraph))
|
||||
workflow.extra = {
|
||||
linearData: { inputs: [[1, 'prompt', {}]], outputs: [] }
|
||||
}
|
||||
const result = await validateComfyWorkflow(workflow)
|
||||
expect(result).not.toBeNull()
|
||||
})
|
||||
|
||||
it('validates mixed 2-tuple and 3-tuple entries', async () => {
|
||||
const workflow = JSON.parse(JSON.stringify(defaultGraph))
|
||||
workflow.extra = {
|
||||
linearData: {
|
||||
inputs: [
|
||||
[1, 'prompt'],
|
||||
[2, 'seed', { height: 100 }]
|
||||
],
|
||||
outputs: []
|
||||
}
|
||||
}
|
||||
const result = await validateComfyWorkflow(workflow)
|
||||
expect(result).not.toBeNull()
|
||||
expect(result!.extra!.linearData!.inputs).toEqual([
|
||||
[1, 'prompt'],
|
||||
[2, 'seed', { height: 100 }]
|
||||
])
|
||||
})
|
||||
|
||||
it('rejects invalid config shape', async () => {
|
||||
const workflow = JSON.parse(JSON.stringify(defaultGraph))
|
||||
workflow.extra = {
|
||||
linearData: { inputs: [[1, 'prompt', 'invalid']], outputs: [] }
|
||||
}
|
||||
const result = await validateComfyWorkflow(workflow)
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
it('workflow.nodes.pos', async () => {
|
||||
const workflow = JSON.parse(JSON.stringify(defaultGraph))
|
||||
workflow.nodes[0].pos = [1, 2, 3]
|
||||
|
||||
@@ -285,7 +285,18 @@ const zExtra = z
|
||||
linearMode: z.boolean().optional(),
|
||||
linearData: z
|
||||
.object({
|
||||
inputs: z.array(z.tuple([zNodeId, z.string()])).optional(),
|
||||
inputs: z
|
||||
.array(
|
||||
z.union([
|
||||
z.tuple([
|
||||
zNodeId,
|
||||
z.string(),
|
||||
z.object({ height: z.number().optional() }).passthrough()
|
||||
]),
|
||||
z.tuple([zNodeId, z.string()])
|
||||
])
|
||||
)
|
||||
.optional(),
|
||||
outputs: z.array(zNodeId).optional()
|
||||
})
|
||||
.optional()
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { remove } from 'es-toolkit'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import type { NodeId } from '@/lib/litegraph/src/LGraphNode'
|
||||
import type { LinearInput } from '@/platform/workflow/management/stores/comfyWorkflow'
|
||||
import { useAppModeStore } from '@/stores/appModeStore'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
@@ -15,7 +15,7 @@ const { id, name } = defineProps<{
|
||||
const appModeStore = useAppModeStore()
|
||||
const isPromoted = computed(() => appModeStore.selectedInputs.some(matchesThis))
|
||||
|
||||
function matchesThis([nodeId, widgetName]: [NodeId, string]) {
|
||||
function matchesThis([nodeId, widgetName]: LinearInput) {
|
||||
return id == nodeId && name === widgetName
|
||||
}
|
||||
function togglePromotion() {
|
||||
|
||||
@@ -346,6 +346,46 @@ describe('appModeStore', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('updateInputConfig', () => {
|
||||
it('sets config on an existing input', () => {
|
||||
store.selectedInputs.push([1, 'prompt'])
|
||||
|
||||
store.updateInputConfig(1 as NodeId, 'prompt', { height: 200 })
|
||||
|
||||
expect(store.selectedInputs[0][2]).toEqual({ height: 200 })
|
||||
})
|
||||
|
||||
it('is a no-op when entry is not found', () => {
|
||||
store.selectedInputs.push([1, 'prompt'])
|
||||
|
||||
store.updateInputConfig(99 as NodeId, 'prompt', { height: 200 })
|
||||
|
||||
expect(store.selectedInputs[0][2]).toBeUndefined()
|
||||
})
|
||||
|
||||
it('matches nodeId with loose equality', () => {
|
||||
store.selectedInputs.push(['1', 'prompt'])
|
||||
|
||||
store.updateInputConfig(1 as NodeId, 'prompt', { height: 200 })
|
||||
|
||||
expect(store.selectedInputs[0][2]).toEqual({ height: 200 })
|
||||
})
|
||||
|
||||
it('triggers linearData sync watcher', async () => {
|
||||
workflowStore.activeWorkflow = createBuilderWorkflow()
|
||||
store.selectedInputs.push([42, 'prompt'])
|
||||
await nextTick()
|
||||
|
||||
store.updateInputConfig(42 as NodeId, 'prompt', { height: 300 })
|
||||
await nextTick()
|
||||
|
||||
expect(app.rootGraph.extra.linearData).toEqual({
|
||||
inputs: [[42, 'prompt', { height: 300 }]],
|
||||
outputs: []
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('autoEnableVueNodes', () => {
|
||||
it('enables Vue nodes when entering select mode with them disabled', async () => {
|
||||
mockSettings.store['Comfy.VueNodes.Enabled'] = false
|
||||
|
||||
@@ -5,7 +5,11 @@ import { useEventListener } from '@vueuse/core'
|
||||
import { useEmptyWorkflowDialog } from '@/components/builder/useEmptyWorkflowDialog'
|
||||
import { useAppMode } from '@/composables/useAppMode'
|
||||
import type { NodeId } from '@/lib/litegraph/src/LGraphNode'
|
||||
import type { LinearData } from '@/platform/workflow/management/stores/comfyWorkflow'
|
||||
import type {
|
||||
InputWidgetConfig,
|
||||
LinearData,
|
||||
LinearInput
|
||||
} from '@/platform/workflow/management/stores/comfyWorkflow'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
|
||||
@@ -29,7 +33,7 @@ export const useAppModeStore = defineStore('appMode', () => {
|
||||
|
||||
const showVueNodeSwitchPopup = ref(false)
|
||||
|
||||
const selectedInputs = ref<[NodeId, string][]>([])
|
||||
const selectedInputs = ref<LinearInput[]>([])
|
||||
const selectedOutputs = ref<NodeId[]>([])
|
||||
const hasOutputs = computed(() => !!selectedOutputs.value.length)
|
||||
const hasNodes = computed(() => {
|
||||
@@ -156,6 +160,18 @@ export const useAppModeStore = defineStore('appMode', () => {
|
||||
if (index !== -1) selectedInputs.value.splice(index, 1)
|
||||
}
|
||||
|
||||
function updateInputConfig(
|
||||
nodeId: NodeId,
|
||||
widgetName: string,
|
||||
config: InputWidgetConfig
|
||||
) {
|
||||
const entry = selectedInputs.value.find(
|
||||
([id, name]) => nodeId == id && widgetName === name
|
||||
)
|
||||
if (!entry) return
|
||||
entry[2] = { ...entry[2], ...config }
|
||||
}
|
||||
|
||||
return {
|
||||
enterBuilder,
|
||||
exitBuilder,
|
||||
@@ -167,6 +183,7 @@ export const useAppModeStore = defineStore('appMode', () => {
|
||||
resetSelectedToWorkflow,
|
||||
selectedInputs,
|
||||
selectedOutputs,
|
||||
updateInputConfig,
|
||||
showVueNodeSwitchPopup
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user