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:
Maanil Verma
2026-07-18 18:13:03 +05:30
parent b1bbef7932
commit b2b1b4dfbb
3 changed files with 80 additions and 9 deletions

View File

@@ -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()

View File

@@ -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') : ''
}

View File

@@ -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]
/**