mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-01 05:49:54 +00:00
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:
Binary file not shown.
|
Before Width: | Height: | Size: 81 KiB After Width: | Height: | Size: 80 KiB |
@@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user