mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-07-18 17:58:23 +00:00
feat(onboarding): backfill Getting Started cards from fallback templates
A curated id missing from the backend data was dropped silently, shrinking the grid below the four skeletons it renders. - Fill the shortfall, in order, from FALLBACK_TEMPLATE_IDS - Resolve tutorial thumbnails against a loaded template so a missing one no longer renders an empty src
This commit is contained in:
@@ -65,7 +65,7 @@ vi.mock('./useFirstRunTourController', () => ({
|
||||
}))
|
||||
|
||||
import GettingStartedScreen from './GettingStartedScreen.vue'
|
||||
import { tutorialCards } from './tutorialCards'
|
||||
import { FALLBACK_TEMPLATE_IDS, tutorialCards } from './tutorialCards'
|
||||
import { useOnboardingEntryStore } from '@/platform/workflow/persistence/onboardingEntryStore'
|
||||
|
||||
import enMessages from '@/locales/en/main.json'
|
||||
@@ -106,6 +106,51 @@ describe('GettingStartedScreen', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('backfills only the shortfall when curated templates are missing', () => {
|
||||
mocks.templatesByName.delete('video_ltx2_3_i2v')
|
||||
FALLBACK_TEMPLATE_IDS.forEach((id) =>
|
||||
mocks.templatesByName.set(id, makeTemplate(id, id))
|
||||
)
|
||||
renderScreen()
|
||||
|
||||
expect(screen.getAllByTestId(/^getting-started-card-/)).toHaveLength(
|
||||
CARD_IDS.length
|
||||
)
|
||||
expect(
|
||||
screen.getByTestId(`getting-started-card-${FALLBACK_TEMPLATE_IDS[0]}`)
|
||||
).toBeTruthy()
|
||||
expect(
|
||||
screen.queryByTestId(`getting-started-card-${FALLBACK_TEMPLATE_IDS[1]}`)
|
||||
).toBeNull()
|
||||
})
|
||||
|
||||
it('leaves the grid untouched when every curated template resolves', () => {
|
||||
FALLBACK_TEMPLATE_IDS.forEach((id) =>
|
||||
mocks.templatesByName.set(id, makeTemplate(id, id))
|
||||
)
|
||||
renderScreen()
|
||||
|
||||
for (const id of CARD_IDS) {
|
||||
expect(screen.getByTestId(`getting-started-card-${id}`)).toBeTruthy()
|
||||
}
|
||||
expect(
|
||||
screen.queryByTestId(`getting-started-card-${FALLBACK_TEMPLATE_IDS[0]}`)
|
||||
).toBeNull()
|
||||
})
|
||||
|
||||
it('keeps tutorial thumbnails resolvable when their template is missing', async () => {
|
||||
mocks.templatesByName.delete('video_ltx2_3_i2v')
|
||||
renderScreen()
|
||||
mocks.getTemplateThumbnailUrl.mockClear()
|
||||
await userEvent.click(screen.getByRole('tab', { name: /tutorials/i }))
|
||||
|
||||
// Every tutorial resolves a thumbnail from some loaded template, so none
|
||||
// renders an empty src.
|
||||
expect(mocks.getTemplateThumbnailUrl).toHaveBeenCalledTimes(
|
||||
tutorialCards.length
|
||||
)
|
||||
})
|
||||
|
||||
it('loads the templates store when opened unloaded so cards can resolve', () => {
|
||||
mocks.templatesLoaded = false
|
||||
renderScreen()
|
||||
|
||||
@@ -80,9 +80,11 @@
|
||||
class="grid grid-cols-2 gap-5 sm:grid-cols-3 lg:grid-cols-4"
|
||||
>
|
||||
<GettingStartedCard
|
||||
v-for="tutorial in tutorialCards"
|
||||
v-for="(tutorial, index) in tutorialCards"
|
||||
:key="tutorial.id"
|
||||
:image-src="tutorialThumbnail(tutorial.thumbnailTemplate)"
|
||||
:image-src="
|
||||
tutorialThumbnail(tutorial.thumbnailTemplate, index)
|
||||
"
|
||||
:title="t(tutorial.titleKey)"
|
||||
:badge-icon="TUTORIAL_BADGE_ICON"
|
||||
:testid="`getting-started-tutorial-${tutorial.id}`"
|
||||
@@ -118,6 +120,7 @@ import GettingStartedTemplateCard from './GettingStartedTemplateCard.vue'
|
||||
import type { TutorialCard } from './tutorialCards'
|
||||
import {
|
||||
CURATED_TEMPLATE_IDS,
|
||||
FALLBACK_TEMPLATE_IDS,
|
||||
TUTORIAL_BADGE_ICON,
|
||||
tutorialCards
|
||||
} from './tutorialCards'
|
||||
@@ -153,11 +156,21 @@ const loadFailed = ref(false)
|
||||
|
||||
const screenRef = useTemplateRef<HTMLElement>('screenRef')
|
||||
|
||||
const cards = computed(() =>
|
||||
CURATED_TEMPLATE_IDS.map((id) => templatesStore.getTemplateByName(id)).filter(
|
||||
(template) => template !== undefined
|
||||
function resolveTemplates(ids: readonly string[]) {
|
||||
return ids
|
||||
.map((id) => templatesStore.getTemplateByName(id))
|
||||
.filter((template) => template !== undefined)
|
||||
}
|
||||
|
||||
/** Tops the grid back up to its skeleton count when curated data is incomplete. */
|
||||
const cards = computed(() => {
|
||||
const curated = resolveTemplates(CURATED_TEMPLATE_IDS)
|
||||
const chosen = new Set(curated.map((template) => template.name))
|
||||
const backfill = resolveTemplates(FALLBACK_TEMPLATE_IDS).filter(
|
||||
(template) => !chosen.has(template.name)
|
||||
)
|
||||
)
|
||||
return [...curated, ...backfill].slice(0, CURATED_TEMPLATE_IDS.length)
|
||||
})
|
||||
|
||||
// Cards and per-card loads no-op until the templates store is loaded; nothing
|
||||
// else loads it on this path, so load it (and focus the takeover) on open.
|
||||
@@ -177,8 +190,13 @@ watch(
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
function tutorialThumbnail(id: TutorialCard['thumbnailTemplate']) {
|
||||
const template = templatesStore.getTemplateByName(id)
|
||||
function tutorialThumbnail(
|
||||
id: TutorialCard['thumbnailTemplate'],
|
||||
index: number
|
||||
) {
|
||||
const template =
|
||||
templatesStore.getTemplateByName(id) ??
|
||||
cards.value[index % cards.value.length]
|
||||
return template ? getTemplateThumbnailUrl(template, 'default') : ''
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,14 @@ export const CURATED_TEMPLATE_IDS = [
|
||||
'video_wan2_2_14B_i2v'
|
||||
] as const satisfies readonly CuratedTemplateId[]
|
||||
|
||||
/** Fills the grid, in order, when a curated template is missing from the data. */
|
||||
export const FALLBACK_TEMPLATE_IDS = [
|
||||
'gsc_advanced_3_2',
|
||||
'image_qwen_image_edit_2509',
|
||||
'templates-qwen_multiangle.app',
|
||||
'gsc_advanced_3_1'
|
||||
] as const
|
||||
|
||||
type LoadedTemplateId = (typeof CURATED_TEMPLATE_IDS)[number]
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user