Skip to content

Commit 12279e6

Browse files
committed
feat: implement version dropdown with backend integration
1 parent 4c4c153 commit 12279e6

9 files changed

Lines changed: 392 additions & 36 deletions

File tree

config/settings.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,7 @@
179179
"django.contrib.auth.context_processors.auth",
180180
"django.contrib.messages.context_processors.messages",
181181
"core.context_processors.current_version",
182+
"core.context_processors.selected_version",
182183
"core.context_processors.active_nav_item",
183184
"core.context_processors.header_context",
184185
"core.context_processors.debug",

config/urls.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@
101101
VersionDetail,
102102
ReportPreviewView,
103103
ReportPreviewGenerateView,
104+
set_version,
104105
)
105106

106107
djdt_urls = []
@@ -218,6 +219,7 @@
218219
name="boost-development",
219220
),
220221
# Boost versions views
222+
path("set-version/", set_version, name="set-version"),
221223
path("releases/", VersionDetail.as_view(), name="releases-most-recent"),
222224
path(
223225
"releases/boost-in-progress/",

core/context_processors.py

Lines changed: 133 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22
from enum import StrEnum
33

44
from django.conf import settings
5-
from django.urls import reverse
5+
from django.urls import NoReverseMatch, reverse
66

7+
from libraries.constants import LATEST_RELEASE_URL_PATH_STR
8+
from libraries.utils import get_version_from_cookie
79
from versions.models import Version
810

911

@@ -12,6 +14,136 @@ def current_version(request):
1214
return {"current_version": Version.objects.most_recent()}
1315

1416

17+
def selected_version(request):
18+
"""User's active Boost version + data to render the navbar version dropdown.
19+
20+
Resolution priority for `selected_version`:
21+
1. URL `version_slug` kwarg (on `/releases/`, `/libraries/`, `/library/` routes)
22+
2. `boost_version` cookie
23+
3. Most recent release (fallback)
24+
25+
When the version comes from the URL, the dropdown renders anchor links that
26+
swap the version segment of the current path — shareable. Otherwise it
27+
renders POST forms that write the cookie without navigating.
28+
29+
Examples
30+
--------
31+
GET /releases/1.88.0/ (no cookie)
32+
selected_version -> Version(slug="boost-1-88-0")
33+
selected_version_is_url_driven -> True (URL mode: render <a>s)
34+
selected_version_is_explicit -> True (button reads "1.88.0")
35+
selected_version_label -> "1.88.0"
36+
version_dropdown_options[i] -> Version with `.href` attached,
37+
e.g. "/releases/1.89.0/"
38+
latest_href -> "/releases/latest/"
39+
40+
GET / (cookie boost_version="boost-1-87-0")
41+
selected_version -> Version(slug="boost-1-87-0")
42+
selected_version_is_url_driven -> False (cookie mode: render <form>s)
43+
selected_version_is_explicit -> True (button reads "1.87.0")
44+
selected_version_label -> "1.87.0"
45+
version_dropdown_options[i] -> Version (no `.href` needed)
46+
latest_href -> ""
47+
48+
GET / (no cookie)
49+
selected_version -> Version.objects.most_recent()
50+
selected_version_is_url_driven -> False
51+
selected_version_is_explicit -> False (button reads "Latest")
52+
selected_version_label -> "Latest"
53+
54+
GET /library/1.88.0/foo/ (target version "boost-1-70-0" predates "foo")
55+
version_dropdown_options[…].href for that version
56+
-> "/library/1.70.0/foo/"
57+
(plain version swap; target view
58+
renders the missing-version state)
59+
"""
60+
url_version_slug = None
61+
resolver_match = getattr(request, "resolver_match", None)
62+
if resolver_match:
63+
url_version_slug = resolver_match.kwargs.get("version_slug")
64+
65+
is_url_driven = bool(url_version_slug)
66+
cookie_slug = get_version_from_cookie(request)
67+
68+
resolved_slug = url_version_slug or cookie_slug
69+
version = None
70+
if resolved_slug and resolved_slug != LATEST_RELEASE_URL_PATH_STR:
71+
version = Version.objects.filter(slug=resolved_slug).first()
72+
if version is None:
73+
version = Version.objects.most_recent()
74+
75+
is_explicit_non_latest_url = bool(
76+
url_version_slug and url_version_slug != LATEST_RELEASE_URL_PATH_STR
77+
)
78+
is_explicit_cookie = bool(
79+
not url_version_slug
80+
and cookie_slug
81+
and cookie_slug != LATEST_RELEASE_URL_PATH_STR
82+
)
83+
is_explicit = is_explicit_non_latest_url or is_explicit_cookie
84+
85+
label = version.display_name if (is_explicit and version) else "Latest"
86+
87+
# Materialize: _annotate_option_hrefs mutates each option with a `.href`.
88+
options = list(Version.objects.get_dropdown_versions())
89+
90+
latest_href = ""
91+
if is_url_driven and resolver_match and resolver_match.view_name:
92+
latest_href = _annotate_option_hrefs(
93+
view_name=resolver_match.view_name,
94+
url_kwargs=dict(resolver_match.kwargs),
95+
options=options,
96+
)
97+
98+
return {
99+
"selected_version": version,
100+
"selected_version_is_url_driven": is_url_driven,
101+
"selected_version_is_explicit": is_explicit,
102+
"selected_version_label": label,
103+
"version_dropdown_options": options,
104+
"latest_href": latest_href,
105+
}
106+
107+
108+
def _annotate_option_hrefs(*, view_name, url_kwargs, options):
109+
"""Attach `.href` to each option for the current page; return the latest's.
110+
111+
The swap is a plain `reverse()` with the option's version substituted in —
112+
on a library detail page at `/library/1.88.0/foo/`, picking 1.70.0 yields
113+
`/library/1.70.0/foo/` even if that library didn't exist in 1.70.0. The
114+
target view is responsible for rendering the empty / "not available" state.
115+
116+
Examples
117+
--------
118+
Current URL /releases/1.88.0/
119+
view_name = "release-detail"
120+
url_kwargs = {"version_slug": "boost-1-88-0"}
121+
→ each option gets .href = "/releases/<that version>/"
122+
→ returns "/releases/latest/"
123+
124+
Current URL /libraries/1.88.0/grid/containers/
125+
view_name = "libraries-list"
126+
url_kwargs = {"version_slug": "boost-1-88-0",
127+
"library_view_str": "grid",
128+
"category_slug": "containers"}
129+
→ each option gets .href = "/libraries/<that version>/grid/containers/"
130+
→ returns "/libraries/latest/grid/containers/"
131+
"""
132+
for v in options:
133+
try:
134+
v.href = reverse(view_name, kwargs={**url_kwargs, "version_slug": v.slug})
135+
except NoReverseMatch:
136+
v.href = ""
137+
138+
try:
139+
return reverse(
140+
view_name,
141+
kwargs={**url_kwargs, "version_slug": LATEST_RELEASE_URL_PATH_STR},
142+
)
143+
except NoReverseMatch:
144+
return ""
145+
146+
15147
class NavItem(StrEnum):
16148
LIBRARIES = "libraries"
17149
LEARN = "learn"

static/css/v3/header.css

Lines changed: 86 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626

2727
.header__nav--desktop,
2828
.header__search-bar,
29-
.header__utilities-latest,
29+
.header__version-trigger,
3030
.header__icon-btn,
3131
.header__menu-btn,
3232
.header__drawer-nav,
@@ -198,7 +198,7 @@
198198
gap: var(--space-s);
199199
}
200200

201-
.header__utilities-latest:hover,
201+
.header__version-trigger:hover,
202202
.header__nav-btn:hover,
203203
.header__icon-btn:hover,
204204
.header__user-trigger:hover {
@@ -212,20 +212,6 @@
212212
border-color: var(--color-stroke-weak);
213213
}
214214

215-
.header__utilities-latest {
216-
display: flex;
217-
justify-content: center;
218-
align-items: center;
219-
gap: var(--space-s);
220-
padding: 0 var(--space-large);
221-
}
222-
223-
.header__utilities-latest .header__icon--chevron {
224-
width: 16px;
225-
height: 16px;
226-
flex-shrink: 0;
227-
}
228-
229215
.header__nav-btn {
230216
display: flex;
231217
justify-content: center;
@@ -284,6 +270,90 @@ html.dark .header__icon--theme-moon {
284270
display: none;
285271
}
286272

273+
/* ── Version menu ──────────────────────────────── */
274+
275+
.header__version-menu {
276+
position: relative;
277+
}
278+
279+
.header__version-toggle {
280+
position: absolute;
281+
width: 1px;
282+
height: 1px;
283+
overflow: hidden;
284+
clip: rect(0, 0, 0, 0);
285+
}
286+
287+
.header__version-toggle:checked ~ .header__version-dropdown {
288+
display: flex;
289+
}
290+
291+
.header__version-toggle:checked ~ .header__version-trigger {
292+
border-color: var(--color-stroke-mid);
293+
background: var(--color-surface-mid);
294+
}
295+
296+
.header__version-toggle:focus-visible ~ .header__version-trigger {
297+
outline: 2px solid var(--color-stroke-link-accent);
298+
}
299+
300+
.header__version-trigger {
301+
display: flex;
302+
justify-content: center;
303+
align-items: center;
304+
gap: var(--space-s);
305+
padding: 0 var(--space-large);
306+
cursor: pointer;
307+
}
308+
309+
.header__version-trigger .header__icon--chevron {
310+
width: 16px;
311+
height: 16px;
312+
flex-shrink: 0;
313+
}
314+
315+
.header__version-dropdown {
316+
display: none;
317+
position: absolute;
318+
top: calc(100% + var(--space-s));
319+
right: 0;
320+
flex-direction: column;
321+
width: max-content;
322+
min-width: 100%;
323+
max-height: 60vh;
324+
overflow-y: auto;
325+
padding: var(--space-default) var(--space-medium);
326+
gap: var(--space-s);
327+
border-radius: var(--header-radius);
328+
border: 1px solid var(--color-stroke-mid);
329+
background: var(--color-surface-weak);
330+
z-index: 1000;
331+
}
332+
333+
.header__version-dropdown .header__nav-link {
334+
padding: 0 var(--space-s);
335+
width: 100%;
336+
}
337+
338+
/* Forms wrap individual options; `display: contents` lets the inner button
339+
inherit the dropdown's flex layout directly. */
340+
.header__version-form {
341+
display: contents;
342+
}
343+
344+
.header__version-dropdown button.header__nav-link {
345+
border: none;
346+
background: transparent;
347+
font: inherit;
348+
color: inherit;
349+
text-align: left;
350+
cursor: pointer;
351+
}
352+
353+
.header__version-option[aria-current="true"] {
354+
font-weight: var(--font-weight-bold);
355+
}
356+
287357
/* ── User menu & avatar ────────────────────────── */
288358

289359
.header__user {

templates/v3/examples/_v3_example_section.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -792,7 +792,7 @@ <h3>{{ section_title }}</h3>
792792
<div class="v3-examples-section__block header" id="{{ section_title|slugify }}">
793793
<h3>{{ section_title }}</h3>
794794
<div class="v3-examples-section__example-box">
795-
{% include "v3/includes/header/_header_utilities.html" with releases_url=releases_url only %}
795+
{% include "v3/includes/header/_header_utilities.html" with avatar_id="demo-desktop-user-menu" version_menu_id="demo-desktop-version-menu" %}
796796
</div>
797797
</div>
798798
{% endwith %}
@@ -802,7 +802,7 @@ <h3>{{ section_title }}</h3>
802802
<h3>{{ section_title }}</h3>
803803
<div class="v3-examples-section__example-box">
804804
<div class="header__drawer" style="display: flex; position: static;">
805-
{% include "v3/includes/header/_header_utilities.html" with releases_url=releases_url only %}
805+
{% include "v3/includes/header/_header_utilities.html" with avatar_id="demo-mobile-user-menu" version_menu_id="demo-mobile-version-menu" %}
806806
</div>
807807
</div>
808808
</div>

0 commit comments

Comments
 (0)