Skip to content

Commit 6726c2d

Browse files
LittleSoundDrJKLgithub-actions
committed
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>
1 parent 9bc8581 commit 6726c2d

3 files changed

Lines changed: 206 additions & 5 deletions

File tree

-529 Bytes
Loading

src/renderer/extensions/vueNodes/widgets/components/WidgetSelectDropdown.test.ts

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,50 @@ describe('WidgetSelectDropdown custom label mapping', () => {
152152
expect(consoleErrorSpy).toHaveBeenCalled()
153153
consoleErrorSpy.mockRestore()
154154
})
155+
156+
it('falls back to original value when label mapping returns empty string', () => {
157+
const getOptionLabel = vi.fn((value: string | null) => {
158+
if (value === 'photo_abc.jpg') {
159+
return ''
160+
}
161+
return `Labeled: ${value}`
162+
})
163+
164+
const widget = createMockWidget('img_001.png', {
165+
getOptionLabel
166+
})
167+
const wrapper = mountComponent(widget, 'img_001.png')
168+
169+
const inputItems = wrapper.vm.inputItems
170+
expect(inputItems[0].name).toBe('img_001.png')
171+
expect(inputItems[0].label).toBe('Labeled: img_001.png')
172+
expect(inputItems[1].name).toBe('photo_abc.jpg')
173+
expect(inputItems[1].label).toBe('photo_abc.jpg')
174+
expect(inputItems[2].name).toBe('hash789.png')
175+
expect(inputItems[2].label).toBe('Labeled: hash789.png')
176+
})
177+
178+
it('falls back to original value when label mapping returns undefined', () => {
179+
const getOptionLabel = vi.fn((value: string | null) => {
180+
if (value === 'hash789.png') {
181+
return undefined as unknown as string
182+
}
183+
return `Labeled: ${value}`
184+
})
185+
186+
const widget = createMockWidget('img_001.png', {
187+
getOptionLabel
188+
})
189+
const wrapper = mountComponent(widget, 'img_001.png')
190+
191+
const inputItems = wrapper.vm.inputItems
192+
expect(inputItems[0].name).toBe('img_001.png')
193+
expect(inputItems[0].label).toBe('Labeled: img_001.png')
194+
expect(inputItems[1].name).toBe('photo_abc.jpg')
195+
expect(inputItems[1].label).toBe('Labeled: photo_abc.jpg')
196+
expect(inputItems[2].name).toBe('hash789.png')
197+
expect(inputItems[2].label).toBe('hash789.png')
198+
})
155199
})
156200

157201
describe('output items with custom label mapping', () => {
@@ -171,4 +215,102 @@ describe('WidgetSelectDropdown custom label mapping', () => {
171215
expect(Array.isArray(outputItems)).toBe(true)
172216
})
173217
})
218+
219+
describe('missing value handling for template-loaded nodes', () => {
220+
it('creates a fallback item in "all" filter when modelValue is not in available items', () => {
221+
const widget = createMockWidget('template_image.png', {
222+
values: ['img_001.png', 'photo_abc.jpg']
223+
})
224+
const wrapper = mountComponent(widget, 'template_image.png')
225+
226+
const inputItems = wrapper.vm.inputItems
227+
expect(inputItems).toHaveLength(2)
228+
expect(
229+
inputItems.some((item) => item.name === 'template_image.png')
230+
).toBe(false)
231+
232+
// The missing value should be accessible via dropdownItems when filter is 'all' (default)
233+
const dropdownItems = (
234+
wrapper.vm as unknown as { dropdownItems: DropdownItem[] }
235+
).dropdownItems
236+
expect(
237+
dropdownItems.some((item) => item.name === 'template_image.png')
238+
).toBe(true)
239+
expect(dropdownItems[0].name).toBe('template_image.png')
240+
expect(dropdownItems[0].id).toBe('missing-template_image.png')
241+
})
242+
243+
it('does not include fallback item when filter is "inputs"', async () => {
244+
const widget = createMockWidget('template_image.png', {
245+
values: ['img_001.png', 'photo_abc.jpg']
246+
})
247+
const wrapper = mountComponent(widget, 'template_image.png')
248+
249+
const vmWithFilter = wrapper.vm as unknown as {
250+
filterSelected: string
251+
dropdownItems: DropdownItem[]
252+
}
253+
254+
vmWithFilter.filterSelected = 'inputs'
255+
await wrapper.vm.$nextTick()
256+
257+
const dropdownItems = vmWithFilter.dropdownItems
258+
expect(dropdownItems).toHaveLength(2)
259+
expect(
260+
dropdownItems.every((item) => !String(item.id).startsWith('missing-'))
261+
).toBe(true)
262+
})
263+
264+
it('does not include fallback item when filter is "outputs"', async () => {
265+
const widget = createMockWidget('template_image.png', {
266+
values: ['img_001.png', 'photo_abc.jpg']
267+
})
268+
const wrapper = mountComponent(widget, 'template_image.png')
269+
270+
const vmWithFilter = wrapper.vm as unknown as {
271+
filterSelected: string
272+
dropdownItems: DropdownItem[]
273+
outputItems: DropdownItem[]
274+
}
275+
276+
vmWithFilter.filterSelected = 'outputs'
277+
await wrapper.vm.$nextTick()
278+
279+
const dropdownItems = vmWithFilter.dropdownItems
280+
expect(dropdownItems).toHaveLength(wrapper.vm.outputItems.length)
281+
expect(
282+
dropdownItems.every((item) => !String(item.id).startsWith('missing-'))
283+
).toBe(true)
284+
})
285+
286+
it('does not create a fallback item when modelValue exists in available items', () => {
287+
const widget = createMockWidget('img_001.png', {
288+
values: ['img_001.png', 'photo_abc.jpg']
289+
})
290+
const wrapper = mountComponent(widget, 'img_001.png')
291+
292+
const dropdownItems = (
293+
wrapper.vm as unknown as { dropdownItems: DropdownItem[] }
294+
).dropdownItems
295+
expect(dropdownItems).toHaveLength(2)
296+
expect(
297+
dropdownItems.every((item) => !String(item.id).startsWith('missing-'))
298+
).toBe(true)
299+
})
300+
301+
it('does not create a fallback item when modelValue is undefined', () => {
302+
const widget = createMockWidget(undefined as unknown as string, {
303+
values: ['img_001.png', 'photo_abc.jpg']
304+
})
305+
const wrapper = mountComponent(widget, undefined)
306+
307+
const dropdownItems = (
308+
wrapper.vm as unknown as { dropdownItems: DropdownItem[] }
309+
).dropdownItems
310+
expect(dropdownItems).toHaveLength(2)
311+
expect(
312+
dropdownItems.every((item) => !String(item.id).startsWith('missing-'))
313+
).toBe(true)
314+
})
315+
})
174316
})

src/renderer/extensions/vueNodes/widgets/components/WidgetSelectDropdown.vue

Lines changed: 64 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -85,14 +85,15 @@ const selectedSet = ref<Set<SelectedKey>>(new Set())
8585
8686
/**
8787
* Transforms a value using getOptionLabel if available.
88-
* Falls back to the original value if getOptionLabel is not provided or throws an error.
88+
* Falls back to the original value if getOptionLabel is not provided,
89+
* returns undefined/null, or throws an error.
8990
*/
9091
function getDisplayLabel(value: string): string {
9192
const getOptionLabel = props.widget.options?.getOptionLabel
9293
if (!getOptionLabel) return value
9394
9495
try {
95-
return getOptionLabel(value)
96+
return getOptionLabel(value) || value
9697
} catch (e) {
9798
console.error('Failed to map value:', e)
9899
return value
@@ -146,11 +147,69 @@ const outputItems = computed<DropdownItem[]>(() => {
146147
}))
147148
})
148149
150+
/**
151+
* Creates a fallback item for the current modelValue when it doesn't exist
152+
* in the available items list. This handles cases like template-loaded nodes
153+
* where the saved value may not exist in the current server environment.
154+
* Works for both local mode (inputItems/outputItems) and cloud mode (assetData).
155+
*/
156+
const missingValueItem = computed<DropdownItem | undefined>(() => {
157+
const currentValue = modelValue.value
158+
if (!currentValue) return undefined
159+
160+
// Check in cloud mode assets
161+
if (props.isAssetMode && assetData) {
162+
const existsInAssets = assetData.dropdownItems.value.some(
163+
(item) => item.name === currentValue
164+
)
165+
if (existsInAssets) return undefined
166+
167+
return {
168+
id: `missing-${currentValue}`,
169+
mediaSrc: '',
170+
name: currentValue,
171+
label: getDisplayLabel(currentValue),
172+
metadata: ''
173+
}
174+
}
175+
176+
// Check in local mode inputs/outputs
177+
const existsInInputs = inputItems.value.some(
178+
(item) => item.name === currentValue
179+
)
180+
const existsInOutputs = outputItems.value.some(
181+
(item) => item.name === currentValue
182+
)
183+
184+
if (existsInInputs || existsInOutputs) return undefined
185+
186+
const isOutput = currentValue.endsWith(' [output]')
187+
const strippedValue = isOutput
188+
? currentValue.replace(' [output]', '')
189+
: currentValue
190+
191+
return {
192+
id: `missing-${currentValue}`,
193+
mediaSrc: getMediaUrl(strippedValue, isOutput ? 'output' : 'input'),
194+
name: currentValue,
195+
label: getDisplayLabel(currentValue),
196+
metadata: ''
197+
}
198+
})
199+
149200
const allItems = computed<DropdownItem[]>(() => {
150201
if (props.isAssetMode && assetData) {
151-
return assetData.dropdownItems.value
202+
const items = assetData.dropdownItems.value
203+
if (missingValueItem.value) {
204+
return [missingValueItem.value, ...items]
205+
}
206+
return items
152207
}
153-
return [...inputItems.value, ...outputItems.value]
208+
return [
209+
...(missingValueItem.value ? [missingValueItem.value] : []),
210+
...inputItems.value,
211+
...outputItems.value
212+
]
154213
})
155214
156215
const dropdownItems = computed<DropdownItem[]>(() => {
@@ -165,7 +224,7 @@ const dropdownItems = computed<DropdownItem[]>(() => {
165224
return outputItems.value
166225
case 'all':
167226
default:
168-
return [...inputItems.value, ...outputItems.value]
227+
return allItems.value
169228
}
170229
})
171230

0 commit comments

Comments
 (0)