[backport cloud/1.32] Feat: Load Image (from Outputs) support in Vue Nodes (#6872)

Backport of #6836 to `cloud/1.32`

Automatically created by backport workflow.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6872-backport-cloud-1-32-Feat-Load-Image-from-Outputs-support-in-Vue-Nodes-2b46d73d365081e3a39bc0ba9e19accf)
by [Unito](https://www.unito.io)

Co-authored-by: Alexander Brown <drjkl@comfy.org>
This commit is contained in:
Comfy Org PR Bot
2025-11-24 06:01:43 +09:00
committed by GitHub
parent 50c353ebb6
commit 19b673cbf5
9 changed files with 39 additions and 53 deletions

View File

@@ -5,7 +5,7 @@
<SlotConnectionDot
ref="connectionDotRef"
:color="slotColor"
:class="cn('-translate-x-1/2', 'w-3', errorClassesDot)"
:class="cn('-translate-x-1/2 w-3', errorClassesDot)"
@pointerdown="onPointerDown"
/>
@@ -48,6 +48,7 @@ interface InputSlotProps {
connected?: boolean
compatible?: boolean
dotOnly?: boolean
socketless?: boolean
}
const props = defineProps<InputSlotProps>()
@@ -121,7 +122,8 @@ const slotWrapperClass = computed(() =>
'lg-slot--connected': props.connected,
'lg-slot--compatible': props.compatible,
'opacity-40': shouldDim.value
}
},
props.socketless && 'pointer-events-none invisible'
)
)

View File

@@ -40,6 +40,7 @@
}"
:node-id="nodeData?.id != null ? String(nodeData.id) : ''"
:index="widget.slotMetadata.index"
:socketless="widget.simplified.spec?.socketless"
dot-only
/>
</div>

View File

@@ -82,21 +82,11 @@ describe('WidgetButton Interactions', () => {
expect(button.exists()).toBe(true)
})
it('renders widget label when name is provided', () => {
it('renders widget text when name is provided', () => {
const widget = createMockWidget()
const wrapper = mountComponent(widget)
const label = wrapper.find('label')
expect(label.exists()).toBe(true)
expect(label.text()).toBe('test_button')
})
it('does not render label when widget name is empty', () => {
const widget = createMockWidget({}, undefined, '')
const wrapper = mountComponent(widget)
const label = wrapper.find('label')
expect(label.exists()).toBe(false)
expect(wrapper.text()).toBe('test_button')
})
it('sets button size to small', () => {

View File

@@ -1,14 +1,15 @@
<template>
<div class="flex flex-col gap-1">
<label v-if="widget.name" class="text-secondary text-sm">{{
widget.name
}}</label>
<Button
v-bind="filteredProps"
:aria-label="widget.name || widget.label"
size="small"
@click="handleClick"
/>
>
<template v-if="widget.name">
{{ widget.name }}
</template>
</Button>
</div>
</template>

View File

@@ -136,8 +136,8 @@ const outputItems = computed<DropdownItem[]>(() => {
})
})
return Array.from(outputs).map((output, index) => ({
id: `output-${index}`,
return Array.from(outputs).map((output) => ({
id: `output-${output}`,
mediaSrc: getMediaUrl(output.replace(' [output]', ''), 'output'),
name: output,
label: getDisplayLabel(output),
@@ -215,16 +215,14 @@ const layoutMode = ref<LayoutMode>(props.defaultLayoutMode ?? 'grid')
watch(
modelValue,
(currentValue) => {
if (currentValue !== undefined) {
const item = dropdownItems.value.find(
(item) => item.name === currentValue
)
if (item) {
selectedSet.value.clear()
selectedSet.value.add(item.id)
}
} else {
if (currentValue === undefined) {
selectedSet.value.clear()
return
}
const item = dropdownItems.value.find((item) => item.name === currentValue)
if (item) {
selectedSet.value.clear()
selectedSet.value.add(item.id)
}
},
{ immediate: true }

View File

@@ -32,24 +32,13 @@ const selectedItems = computed(() => {
return props.items.filter((item) => props.selected.has(item.id))
})
const chevronClass = computed(() =>
cn(
'mr-2 size-4 transition-transform duration-200 flex-shrink-0 text-component-node-foreground-secondary',
{
'rotate-180': props.isOpen
}
)
)
const theButtonStyle = computed(() =>
cn(
'border-0 bg-component-node-widget-background outline-none text-text-secondary',
{
'hover:bg-component-node-widget-background-hovered cursor-pointer':
!props.disabled,
'cursor-not-allowed': props.disabled,
'text-text-primary': selectedItems.value.length > 0
}
props.disabled
? 'cursor-not-allowed'
: 'hover:bg-component-node-widget-background-hovered cursor-pointer',
selectedItems.value.length > 0 && 'text-text-primary'
)
)
</script>
@@ -78,13 +67,21 @@ const theButtonStyle = computed(() =>
>
<span class="min-w-0 flex-1 px-1 py-2 text-left truncate">
<span v-if="!selectedItems.length">
{{ props.placeholder }}
{{ placeholder }}
</span>
<span v-else>
{{ selectedItems.map((item) => item.label ?? item.name).join(', ') }}
</span>
</span>
<i class="icon-[lucide--chevron-down]" :class="chevronClass" />
<i
class="icon-[lucide--chevron-down]"
:class="
cn(
'mr-2 size-4 transition-transform duration-200 flex-shrink-0 text-component-node-foreground-secondary',
isOpen && 'rotate-180'
)
"
/>
</button>
<!-- Open File -->
<label

View File

@@ -236,9 +236,7 @@ export function useRemoteWidget<
* Add a refresh button to the node that, when clicked, will force the widget to refresh
*/
function addRefreshButton() {
node.addWidget('button', 'refresh', 'refresh', widget.refresh, {
canvasOnly: true
})
node.addWidget('button', 'refresh', 'refresh', widget.refresh)
}
/**
@@ -263,8 +261,7 @@ export function useRemoteWidget<
autoRefreshEnabled = value
},
{
serialize: false,
canvasOnly: true
serialize: false
}
)

View File

@@ -29,6 +29,7 @@ export const zBaseInputOptions = z
defaultInput: z.boolean().optional(),
forceInput: z.boolean().optional(),
tooltip: z.string().optional(),
socketless: z.boolean().optional(),
hidden: z.boolean().optional(),
advanced: z.boolean().optional(),
widgetType: z.string().optional(),

View File

@@ -610,8 +610,7 @@ describe('useRemoteWidget', () => {
false,
expect.any(Function),
{
serialize: false,
canvasOnly: true
serialize: false
}
)
})