mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-30 19:21:54 +00:00
[backport core/1.32] Feat: Load Image (from Outputs) support in Vue Nodes (#6871)
Backport of #6836 to `core/1.32` Automatically created by backport workflow. ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-6871-backport-core-1-32-Feat-Load-Image-from-Outputs-support-in-Vue-Nodes-2b46d73d36508121b3b3c9dac9e3de5a) by [Unito](https://www.unito.io) Co-authored-by: Alexander Brown <drjkl@comfy.org>
This commit is contained in:
@@ -5,7 +5,7 @@
|
|||||||
<SlotConnectionDot
|
<SlotConnectionDot
|
||||||
ref="connectionDotRef"
|
ref="connectionDotRef"
|
||||||
:color="slotColor"
|
:color="slotColor"
|
||||||
:class="cn('-translate-x-1/2', 'w-3', errorClassesDot)"
|
:class="cn('-translate-x-1/2 w-3', errorClassesDot)"
|
||||||
@pointerdown="onPointerDown"
|
@pointerdown="onPointerDown"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -48,6 +48,7 @@ interface InputSlotProps {
|
|||||||
connected?: boolean
|
connected?: boolean
|
||||||
compatible?: boolean
|
compatible?: boolean
|
||||||
dotOnly?: boolean
|
dotOnly?: boolean
|
||||||
|
socketless?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps<InputSlotProps>()
|
const props = defineProps<InputSlotProps>()
|
||||||
@@ -121,7 +122,8 @@ const slotWrapperClass = computed(() =>
|
|||||||
'lg-slot--connected': props.connected,
|
'lg-slot--connected': props.connected,
|
||||||
'lg-slot--compatible': props.compatible,
|
'lg-slot--compatible': props.compatible,
|
||||||
'opacity-40': shouldDim.value
|
'opacity-40': shouldDim.value
|
||||||
}
|
},
|
||||||
|
props.socketless && 'pointer-events-none invisible'
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,7 @@
|
|||||||
}"
|
}"
|
||||||
:node-id="nodeData?.id != null ? String(nodeData.id) : ''"
|
:node-id="nodeData?.id != null ? String(nodeData.id) : ''"
|
||||||
:index="widget.slotMetadata.index"
|
:index="widget.slotMetadata.index"
|
||||||
|
:socketless="widget.simplified.spec?.socketless"
|
||||||
dot-only
|
dot-only
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -82,21 +82,11 @@ describe('WidgetButton Interactions', () => {
|
|||||||
expect(button.exists()).toBe(true)
|
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 widget = createMockWidget()
|
||||||
const wrapper = mountComponent(widget)
|
const wrapper = mountComponent(widget)
|
||||||
|
|
||||||
const label = wrapper.find('label')
|
expect(wrapper.text()).toBe('test_button')
|
||||||
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)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('sets button size to small', () => {
|
it('sets button size to small', () => {
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col gap-1">
|
<div class="flex flex-col gap-1">
|
||||||
<label v-if="widget.name" class="text-secondary text-sm">{{
|
|
||||||
widget.name
|
|
||||||
}}</label>
|
|
||||||
<Button
|
<Button
|
||||||
v-bind="filteredProps"
|
v-bind="filteredProps"
|
||||||
:aria-label="widget.name || widget.label"
|
:aria-label="widget.name || widget.label"
|
||||||
size="small"
|
size="small"
|
||||||
@click="handleClick"
|
@click="handleClick"
|
||||||
/>
|
>
|
||||||
|
<template v-if="widget.name">
|
||||||
|
{{ widget.name }}
|
||||||
|
</template>
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -136,8 +136,8 @@ const outputItems = computed<DropdownItem[]>(() => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
return Array.from(outputs).map((output, index) => ({
|
return Array.from(outputs).map((output) => ({
|
||||||
id: `output-${index}`,
|
id: `output-${output}`,
|
||||||
mediaSrc: getMediaUrl(output.replace(' [output]', ''), 'output'),
|
mediaSrc: getMediaUrl(output.replace(' [output]', ''), 'output'),
|
||||||
name: output,
|
name: output,
|
||||||
label: getDisplayLabel(output),
|
label: getDisplayLabel(output),
|
||||||
@@ -215,16 +215,14 @@ const layoutMode = ref<LayoutMode>(props.defaultLayoutMode ?? 'grid')
|
|||||||
watch(
|
watch(
|
||||||
modelValue,
|
modelValue,
|
||||||
(currentValue) => {
|
(currentValue) => {
|
||||||
if (currentValue !== undefined) {
|
if (currentValue === undefined) {
|
||||||
const item = dropdownItems.value.find(
|
|
||||||
(item) => item.name === currentValue
|
|
||||||
)
|
|
||||||
if (item) {
|
|
||||||
selectedSet.value.clear()
|
|
||||||
selectedSet.value.add(item.id)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
selectedSet.value.clear()
|
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 }
|
{ immediate: true }
|
||||||
|
|||||||
@@ -32,24 +32,13 @@ const selectedItems = computed(() => {
|
|||||||
return props.items.filter((item) => props.selected.has(item.id))
|
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(() =>
|
const theButtonStyle = computed(() =>
|
||||||
cn(
|
cn(
|
||||||
'border-0 bg-component-node-widget-background outline-none text-text-secondary',
|
'border-0 bg-component-node-widget-background outline-none text-text-secondary',
|
||||||
{
|
props.disabled
|
||||||
'hover:bg-component-node-widget-background-hovered cursor-pointer':
|
? 'cursor-not-allowed'
|
||||||
!props.disabled,
|
: 'hover:bg-component-node-widget-background-hovered cursor-pointer',
|
||||||
'cursor-not-allowed': props.disabled,
|
selectedItems.value.length > 0 && 'text-text-primary'
|
||||||
'text-text-primary': selectedItems.value.length > 0
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
</script>
|
</script>
|
||||||
@@ -78,13 +67,21 @@ const theButtonStyle = computed(() =>
|
|||||||
>
|
>
|
||||||
<span class="min-w-0 flex-1 px-1 py-2 text-left truncate">
|
<span class="min-w-0 flex-1 px-1 py-2 text-left truncate">
|
||||||
<span v-if="!selectedItems.length">
|
<span v-if="!selectedItems.length">
|
||||||
{{ props.placeholder }}
|
{{ placeholder }}
|
||||||
</span>
|
</span>
|
||||||
<span v-else>
|
<span v-else>
|
||||||
{{ selectedItems.map((item) => item.label ?? item.name).join(', ') }}
|
{{ selectedItems.map((item) => item.label ?? item.name).join(', ') }}
|
||||||
</span>
|
</span>
|
||||||
</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>
|
</button>
|
||||||
<!-- Open File -->
|
<!-- Open File -->
|
||||||
<label
|
<label
|
||||||
|
|||||||
@@ -236,9 +236,7 @@ export function useRemoteWidget<
|
|||||||
* Add a refresh button to the node that, when clicked, will force the widget to refresh
|
* Add a refresh button to the node that, when clicked, will force the widget to refresh
|
||||||
*/
|
*/
|
||||||
function addRefreshButton() {
|
function addRefreshButton() {
|
||||||
node.addWidget('button', 'refresh', 'refresh', widget.refresh, {
|
node.addWidget('button', 'refresh', 'refresh', widget.refresh)
|
||||||
canvasOnly: true
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -263,8 +261,7 @@ export function useRemoteWidget<
|
|||||||
autoRefreshEnabled = value
|
autoRefreshEnabled = value
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
serialize: false,
|
serialize: false
|
||||||
canvasOnly: true
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ export const zBaseInputOptions = z
|
|||||||
defaultInput: z.boolean().optional(),
|
defaultInput: z.boolean().optional(),
|
||||||
forceInput: z.boolean().optional(),
|
forceInput: z.boolean().optional(),
|
||||||
tooltip: z.string().optional(),
|
tooltip: z.string().optional(),
|
||||||
|
socketless: z.boolean().optional(),
|
||||||
hidden: z.boolean().optional(),
|
hidden: z.boolean().optional(),
|
||||||
advanced: z.boolean().optional(),
|
advanced: z.boolean().optional(),
|
||||||
widgetType: z.string().optional(),
|
widgetType: z.string().optional(),
|
||||||
|
|||||||
@@ -610,8 +610,7 @@ describe('useRemoteWidget', () => {
|
|||||||
false,
|
false,
|
||||||
expect.any(Function),
|
expect.any(Function),
|
||||||
{
|
{
|
||||||
serialize: false,
|
serialize: false
|
||||||
canvasOnly: true
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user