CI/CD catalog version selector uses both v-model and @select, with two sources of truth

app/assets/javascripts/ci/catalog/components/details/ci_resource_header.vue binds both v-model and @select to the same GlCollapsibleListbox, and the two write to different properties. This is not a live bug today, but it hides a whole class of failure.

The pattern

<gl-collapsible-listbox
  v-model="selectedVersionId"
  :items="versions"
  searchable
  @select="onVersionSelect"
  @search="onVersionSearch"
>

One user action drives two independent pieces of state:

State Written by Read by
selectedVersionId (line 88, assigned at 157, bound at 245) v-model, plus one imperative assignment nothing in this component
selectedVersion @selectonVersionSelect (line 173) the toggle text, the version badge, the router push, the version-selected emit

onVersionSelect does all the real work: it resolves the version, pushes ?version= onto the route, and emits to the parent. selectedVersionId is never read by the component's own logic. It exists only so the listbox can mark the current item as selected.

Why this matters

Because @select drives everything visible, a v-model breakage is close to silent. v-model on a component compiles to value + input under Vue 2 and to modelValue + update:modelValue under Vue 3. If that binding ever stops firing, the page keeps working — correct toggle text, correct badge, correct URL, correct data — and the only symptom is that the open dropdown no longer highlights the current version. That is easy to miss in review and in manual testing.

Two sources of truth for one selection is also just harder to reason about than it needs to be.

Suggested fix

Drop v-model and derive the listbox's :selected from the state that already exists, so there is one source of truth:

<gl-collapsible-listbox
  :selected="selectedVersion.value"
  :items="versions"
  searchable
  @select="onVersionSelect"
  @search="onVersionSearch"
>

GlCollapsibleListbox supports an explicit selected prop, so @select can remain the only write path.

Verification already done

Found while verifying the Vue 3 migration of this page (!250411 (merged)). Selecting a non-latest version behaves identically under Vue 2 and Vue 3 today:

Check Vue 2 Vue 3
URL ?version=v1.0.0 ?version=v1.0.0
Toggle text v1.0.0 (2026-08-17) v1.0.0 (2026-08-17)
Version badge v1.0.0 v1.0.0
include: snippet component@v1.0.0 component@v1.0.0

So this is a refactor for robustness, not a regression fix.

Two smaller observations

  • The custom #toggle slot renders a plain GlButton, so the toggle exposes no aria-expanded. Screen reader users get no indication that it opens a listbox.
  • After selecting an item, the listbox stays open ([role="listbox"] is still in the DOM). This happens in both Vue 2 and Vue 3, so it is pre-existing, but it is unusual for a single-select listbox and may be worth a separate look.