fix: default image input for the template is displayed as empty on dropdown selection (#8276)

The image input for nodes loaded from templates appears empty in the
Properties Panel.

When the widget's current value (saved in the template) is not in the
available file list returned by the server, the selectedSet is empty,
causing a placeholder to be displayed instead of the actual value.

Added a missingValueItem computed property in WidgetSelectDropdown.vue.
When the current value is not in inputItems or outputItems, it creates a
fallback item and adds it to dropdownItems.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8276-fix-default-image-input-for-the-template-is-displayed-as-empty-on-dropdown-selection-2f16d73d3650817eaad5e4e33637fb74)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Alexander Brown <drjkl@comfy.org>
Co-authored-by: github-actions <github-actions@github.com>
This commit is contained in:
Rizumu Ayaka
2026-01-30 05:19:52 +07:00
committed by GitHub
parent faad2c03de
commit fc38f16543
3 changed files with 206 additions and 5 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

After

Width:  |  Height:  |  Size: 80 KiB

View File

@@ -152,6 +152,50 @@ describe('WidgetSelectDropdown custom label mapping', () => {
expect(consoleErrorSpy).toHaveBeenCalled()
consoleErrorSpy.mockRestore()
})
it('falls back to original value when label mapping returns empty string', () => {
const getOptionLabel = vi.fn((value: string | null) => {
if (value === 'photo_abc.jpg') {
return ''
}
return `Labeled: ${value}`
})
const widget = createMockWidget('img_001.png', {
getOptionLabel
})
const wrapper = mountComponent(widget, 'img_001.png')
const inputItems = wrapper.vm.inputItems
expect(inputItems[0].name).toBe('img_001.png')
expect(inputItems[0].label).toBe('Labeled: img_001.png')
expect(inputItems[1].name).toBe('photo_abc.jpg')
expect(inputItems[1].label).toBe('photo_abc.jpg')
expect(inputItems[2].name).toBe('hash789.png')
expect(inputItems[2].label).toBe('Labeled: hash789.png')
})
it('falls back to original value when label mapping returns undefined', () => {
const getOptionLabel = vi.fn((value: string | null) => {
if (value === 'hash789.png') {
return undefined as unknown as string
}
return `Labeled: ${value}`
})
const widget = createMockWidget('img_001.png', {
getOptionLabel
})
const wrapper = mountComponent(widget, 'img_001.png')
const inputItems = wrapper.vm.inputItems
expect(inputItems[0].name).toBe('img_001.png')
expect(inputItems[0].label).toBe('Labeled: img_001.png')
expect(inputItems[1].name).toBe('photo_abc.jpg')
expect(inputItems[1].label).toBe('Labeled: photo_abc.jpg')
expect(inputItems[2].name).toBe('hash789.png')
expect(inputItems[2].label).toBe('hash789.png')
})
})
describe('output items with custom label mapping', () => {
@@ -171,4 +215,102 @@ describe('WidgetSelectDropdown custom label mapping', () => {
expect(Array.isArray(outputItems)).toBe(true)
})
})
describe('missing value handling for template-loaded nodes', () => {
it('creates a fallback item in "all" filter when modelValue is not in available items', () => {
const widget = createMockWidget('template_image.png', {
values: ['img_001.png', 'photo_abc.jpg']
})
const wrapper = mountComponent(widget, 'template_image.png')
const inputItems = wrapper.vm.inputItems
expect(inputItems).toHaveLength(2)
expect(
inputItems.some((item) => item.name === 'template_image.png')
).toBe(false)
// The missing value should be accessible via dropdownItems when filter is 'all' (default)
const dropdownItems = (
wrapper.vm as unknown as { dropdownItems: DropdownItem[] }
).dropdownItems
expect(
dropdownItems.some((item) => item.name === 'template_image.png')
).toBe(true)
expect(dropdownItems[0].name).toBe('template_image.png')
expect(dropdownItems[0].id).toBe('missing-template_image.png')
})
it('does not include fallback item when filter is "inputs"', async () => {
const widget = createMockWidget('template_image.png', {
values: ['img_001.png', 'photo_abc.jpg']
})
const wrapper = mountComponent(widget, 'template_image.png')
const vmWithFilter = wrapper.vm as unknown as {
filterSelected: string
dropdownItems: DropdownItem[]
}
vmWithFilter.filterSelected = 'inputs'
await wrapper.vm.$nextTick()
const dropdownItems = vmWithFilter.dropdownItems
expect(dropdownItems).toHaveLength(2)
expect(
dropdownItems.every((item) => !String(item.id).startsWith('missing-'))
).toBe(true)
})
it('does not include fallback item when filter is "outputs"', async () => {
const widget = createMockWidget('template_image.png', {
values: ['img_001.png', 'photo_abc.jpg']
})
const wrapper = mountComponent(widget, 'template_image.png')
const vmWithFilter = wrapper.vm as unknown as {
filterSelected: string
dropdownItems: DropdownItem[]
outputItems: DropdownItem[]
}
vmWithFilter.filterSelected = 'outputs'
await wrapper.vm.$nextTick()
const dropdownItems = vmWithFilter.dropdownItems
expect(dropdownItems).toHaveLength(wrapper.vm.outputItems.length)
expect(
dropdownItems.every((item) => !String(item.id).startsWith('missing-'))
).toBe(true)
})
it('does not create a fallback item when modelValue exists in available items', () => {
const widget = createMockWidget('img_001.png', {
values: ['img_001.png', 'photo_abc.jpg']
})
const wrapper = mountComponent(widget, 'img_001.png')
const dropdownItems = (
wrapper.vm as unknown as { dropdownItems: DropdownItem[] }
).dropdownItems
expect(dropdownItems).toHaveLength(2)
expect(
dropdownItems.every((item) => !String(item.id).startsWith('missing-'))
).toBe(true)
})
it('does not create a fallback item when modelValue is undefined', () => {
const widget = createMockWidget(undefined as unknown as string, {
values: ['img_001.png', 'photo_abc.jpg']
})
const wrapper = mountComponent(widget, undefined)
const dropdownItems = (
wrapper.vm as unknown as { dropdownItems: DropdownItem[] }
).dropdownItems
expect(dropdownItems).toHaveLength(2)
expect(
dropdownItems.every((item) => !String(item.id).startsWith('missing-'))
).toBe(true)
})
})
})

View File

@@ -85,14 +85,15 @@ const selectedSet = ref<Set<SelectedKey>>(new Set())
/**
* Transforms a value using getOptionLabel if available.
* Falls back to the original value if getOptionLabel is not provided or throws an error.
* Falls back to the original value if getOptionLabel is not provided,
* returns undefined/null, or throws an error.
*/
function getDisplayLabel(value: string): string {
const getOptionLabel = props.widget.options?.getOptionLabel
if (!getOptionLabel) return value
try {
return getOptionLabel(value)
return getOptionLabel(value) || value
} catch (e) {
console.error('Failed to map value:', e)
return value
@@ -146,11 +147,69 @@ const outputItems = computed<DropdownItem[]>(() => {
}))
})
/**
* Creates a fallback item for the current modelValue when it doesn't exist
* in the available items list. This handles cases like template-loaded nodes
* where the saved value may not exist in the current server environment.
* Works for both local mode (inputItems/outputItems) and cloud mode (assetData).
*/
const missingValueItem = computed<DropdownItem | undefined>(() => {
const currentValue = modelValue.value
if (!currentValue) return undefined
// Check in cloud mode assets
if (props.isAssetMode && assetData) {
const existsInAssets = assetData.dropdownItems.value.some(
(item) => item.name === currentValue
)
if (existsInAssets) return undefined
return {
id: `missing-${currentValue}`,
mediaSrc: '',
name: currentValue,
label: getDisplayLabel(currentValue),
metadata: ''
}
}
// Check in local mode inputs/outputs
const existsInInputs = inputItems.value.some(
(item) => item.name === currentValue
)
const existsInOutputs = outputItems.value.some(
(item) => item.name === currentValue
)
if (existsInInputs || existsInOutputs) return undefined
const isOutput = currentValue.endsWith(' [output]')
const strippedValue = isOutput
? currentValue.replace(' [output]', '')
: currentValue
return {
id: `missing-${currentValue}`,
mediaSrc: getMediaUrl(strippedValue, isOutput ? 'output' : 'input'),
name: currentValue,
label: getDisplayLabel(currentValue),
metadata: ''
}
})
const allItems = computed<DropdownItem[]>(() => {
if (props.isAssetMode && assetData) {
return assetData.dropdownItems.value
const items = assetData.dropdownItems.value
if (missingValueItem.value) {
return [missingValueItem.value, ...items]
}
return items
}
return [...inputItems.value, ...outputItems.value]
return [
...(missingValueItem.value ? [missingValueItem.value] : []),
...inputItems.value,
...outputItems.value
]
})
const dropdownItems = computed<DropdownItem[]>(() => {
@@ -165,7 +224,7 @@ const dropdownItems = computed<DropdownItem[]>(() => {
return outputItems.value
case 'all':
default:
return [...inputItems.value, ...outputItems.value]
return allItems.value
}
})