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 |
@select → onVersionSelect (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
#toggleslot renders a plainGlButton, so the toggle exposes noaria-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.