fix(widgets): fall back to id when item name is empty string

Some remote assets (e.g. /proxy/seedance/assets) return name='' for items the user never titled. Trigger and list rows rendered blank because nullish coalescing (??) only catches null/undefined, not empty strings.

Add displayName(item) helper in base/remote/itemSchema.ts using logical-or fallback (matches the FormDropdownInput pattern in PR #11310) and use it in Trigger.vue's selected-label computed and Item.vue's name span, img alt, and video aria-label so the accessibility names also fall back instead of going empty.
This commit is contained in:
Glary-Bot
2026-05-17 15:53:13 +00:00
parent 0fe8cacf5e
commit 65b436daa9
4 changed files with 27 additions and 4 deletions

View File

@@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest'
import {
buildSearchText,
displayName,
extractItems,
getByPath,
mapToDropdownItem,
@@ -341,3 +342,13 @@ describe('mapToDropdownItem preview_url normalization', () => {
expect(item.preview_url).toBeUndefined()
})
})
describe('displayName', () => {
it('returns name when present', () => {
expect(displayName({ id: 'abc', name: 'Cool Asset' })).toBe('Cool Asset')
})
it('falls back to id when name is empty string', () => {
expect(displayName({ id: 'abc-123', name: '' })).toBe('abc-123')
})
})

View File

@@ -7,6 +7,14 @@ export interface DropdownItemShape {
preview_url?: string
}
/**
* User-facing label for a dropdown item. Falls back to id when name
* is missing or empty, so trigger/list rows never render blank.
*/
export function displayName(item: DropdownItemShape): string {
return item.name || item.id
}
export function getByPath(obj: unknown, path: string): unknown {
return path.split('.').reduce((acc: unknown, key: string) => {
if (acc == null) return undefined

View File

@@ -5,6 +5,7 @@ import { useI18n } from 'vue-i18n'
import { cn } from '@comfyorg/tailwind-utils'
import { displayName } from '@/base/remote/itemSchema'
import type { DropdownItemShape } from '@/base/remote/itemSchema'
import { itemVariants } from './remoteCombo.variants'
@@ -27,6 +28,7 @@ const { t } = useI18n()
const isSelected = computed(() => ctx.selectedValue.value === props.item.id)
const hasPreview = computed(() => !!props.item.preview_url)
const label = computed(() => displayName(props.item))
const audioEl = useTemplateRef<HTMLAudioElement>('audioEl')
const isPlaying = ref(false)
@@ -60,7 +62,7 @@ function handleAudioEnded() {
<template v-if="hasPreview && ctx.previewType.value === 'image'">
<img
:src="item.preview_url"
:alt="item.name"
:alt="label"
class="size-10 shrink-0 rounded-sm object-cover"
loading="lazy"
decoding="async"
@@ -69,11 +71,11 @@ function handleAudioEnded() {
<template v-else-if="hasPreview && ctx.previewType.value === 'video'">
<video
:src="item.preview_url"
:aria-label="label"
class="size-10 shrink-0 rounded-sm object-cover"
preload="metadata"
muted
playsinline
aria-hidden="true"
/>
</template>
<template v-else-if="hasPreview && ctx.previewType.value === 'audio'">
@@ -108,7 +110,7 @@ function handleAudioEnded() {
</button>
</template>
<div class="flex flex-1 flex-col gap-0.5 overflow-hidden">
<span class="truncate">{{ item.name }}</span>
<span class="truncate">{{ label }}</span>
<span
v-if="item.description"
class="truncate text-[10px] text-muted-foreground"

View File

@@ -5,6 +5,8 @@ import { useI18n } from 'vue-i18n'
import { cn } from '@comfyorg/tailwind-utils'
import { displayName } from '@/base/remote/itemSchema'
import { triggerVariants } from './remoteCombo.variants'
import type { TriggerVariants } from './remoteCombo.variants'
import { RemoteComboKey } from './state'
@@ -31,7 +33,7 @@ const displayLabel = computed(() => {
const id = ctx.selectedValue.value
if (!id) return props.placeholder ?? t('widgets.uploadSelect.placeholder')
const item = ctx.items.value.find((i) => i.id === id)
return item?.name ?? id
return item ? displayName(item) : id
})
const computedBorder = computed<TriggerVariants['border']>(() => {