mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-02 14:27:40 +00:00
[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:
@@ -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'
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
}"
|
||||
:node-id="nodeData?.id != null ? String(nodeData.id) : ''"
|
||||
:index="widget.slotMetadata.index"
|
||||
:socketless="widget.simplified.spec?.socketless"
|
||||
dot-only
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -610,8 +610,7 @@ describe('useRemoteWidget', () => {
|
||||
false,
|
||||
expect.any(Function),
|
||||
{
|
||||
serialize: false,
|
||||
canvasOnly: true
|
||||
serialize: false
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user