fix(versions): keep the changelog series stable on reload (0.10 -> 0.1 bug)

The series rail synced ?series=<key> via Livewire #[Url] with a bare numeric key like
"0.10". Livewire type-juggles that to 0.1 (the trailing zero is lost), so reloading
while viewing the 0.10 series silently switched to 0.1 and rewrote the URL. Prefix the
series key with a non-numeric "v" (?series=v0.10) so it round-trips as a string; the
rail label still shows the dotted "0.10". Surfaced now that a 0.10 series exists.

Browser-verified: selecting 0.10 -> ?series=v0.10, and it survives a reload.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
feat/v1-foundation
boban 2026-06-25 02:15:41 +02:00
parent d8da46151e
commit 8dfc11fd5d
2 changed files with 26 additions and 13 deletions

View File

@ -382,7 +382,10 @@ class Index extends Component
if ($rel['unreleased']) {
$key = 'unreleased';
} elseif (preg_match('/^v?(\d+)\.(\d+)\./', $rel['version'], $m)) {
$key = $m[1].'.'.$m[2];
// Prefix the series key with a non-numeric 'v' so the ?series= URL value is never a
// bare decimal — Livewire's #[Url] otherwise type-juggles "0.10" → 0.1 (the trailing
// zero is lost), silently switching the selected series on reload. The label strips it.
$key = 'v'.$m[1].'.'.$m[2];
} else {
$key = 'other';
}
@ -397,7 +400,7 @@ class Index extends Component
'label' => match ($key) {
'unreleased' => __('versions.unreleased'),
'other' => __('versions.series_other'),
default => $key,
default => ltrim($key, 'v'), // display the dotted series (the 'v' is only for the URL)
},
'unreleased' => $key === 'unreleased',
'count' => count($rels),
@ -415,7 +418,7 @@ class Index extends Component
return $ra <=> $rb;
}
return $ra === 1 ? version_compare($b['key'].'.0', $a['key'].'.0') : 0;
return $ra === 1 ? version_compare(ltrim($b['key'], 'v').'.0', ltrim($a['key'], 'v').'.0') : 0;
});
return $series;
@ -628,7 +631,7 @@ class Index extends Component
// Mark the series that holds the running version (a small "installed" cue in the rail).
$installed = ltrim((string) config('clusev.version'), 'vV');
$currentSeries = preg_match('/^(\d+)\.(\d+)\./', $installed, $m) ? $m[1].'.'.$m[2] : null;
$currentSeries = preg_match('/^(\d+)\.(\d+)\./', $installed, $m) ? 'v'.$m[1].'.'.$m[2] : null;
// Passive (no network): cached remote result if a check has run, else local .git.
$latestTag = $this->resolveLatestTag($this->channel());

View File

@ -81,8 +81,8 @@ class VersionsChangelogTest extends TestCase
// …selecting the 0.1 series brings it into view and jumps back to page 1.
Livewire::test(Index::class)
->set('changelogPage', 3)
->call('selectSeries', '0.1')
->assertSet('series', '0.1')
->call('selectSeries', 'v0.1')
->assertSet('series', 'v0.1')
->assertSet('changelogPage', 1)
->assertSee('v0.1.0');
}
@ -90,7 +90,7 @@ class VersionsChangelogTest extends TestCase
public function test_a_long_series_is_paginated_to_the_per_page_limit(): void
{
Livewire::test(Index::class)
->call('selectSeries', '0.9')
->call('selectSeries', 'v0.9')
->assertViewHas('totalPages', fn ($total): bool => $total > 1)
->assertViewHas('pagedReleases', fn ($releases): bool => is_array($releases) && count($releases) <= 8);
}
@ -100,7 +100,7 @@ class VersionsChangelogTest extends TestCase
$pageOne = null;
Livewire::test(Index::class)
->call('selectSeries', '0.9')
->call('selectSeries', 'v0.9')
->assertViewHas('pagedReleases', function ($releases) use (&$pageOne): bool {
$pageOne = array_column($releases, 'version');
@ -108,7 +108,7 @@ class VersionsChangelogTest extends TestCase
});
Livewire::test(Index::class)
->call('selectSeries', '0.9')
->call('selectSeries', 'v0.9')
->call('gotoChangelogPage', 2)
->assertViewHas('pagedReleases', function ($releases) use (&$pageOne): bool {
$pageTwo = array_column($releases, 'version');
@ -121,7 +121,7 @@ class VersionsChangelogTest extends TestCase
public function test_an_out_of_range_page_is_clamped(): void
{
Livewire::test(Index::class)
->call('selectSeries', '0.9')
->call('selectSeries', 'v0.9')
->call('gotoChangelogPage', 9999)
->assertViewHas('changelogPage', fn ($page): bool => $page >= 1)
->assertViewHas('pagedReleases', fn ($releases): bool => is_array($releases) && count($releases) >= 1)
@ -132,7 +132,7 @@ class VersionsChangelogTest extends TestCase
{
config()->set('clusev.version', '0.7.3');
Livewire::test(Index::class)->assertViewHas('currentSeries', '0.7');
Livewire::test(Index::class)->assertViewHas('currentSeries', 'v0.7');
}
public function test_total_release_count_is_passed_for_the_panel_subtitle(): void
@ -143,12 +143,22 @@ class VersionsChangelogTest extends TestCase
public function test_series_and_page_are_deep_linkable_via_the_query_string(): void
{
// ?series= + ?page= drive the view so a series/page is shareable and survives reload.
Livewire::withQueryParams(['series' => '0.9', 'page' => 2])
Livewire::withQueryParams(['series' => 'v0.9', 'page' => 2])
->test(Index::class)
->assertSet('series', '0.9')
->assertSet('series', 'v0.9')
->assertSet('changelogPage', 2);
}
public function test_a_trailing_zero_series_like_0_10_survives_a_deep_link(): void
{
// The 'v' prefix keeps the ?series= value non-numeric, so "0.10" is NOT type-juggled to
// "0.1" on reload (which used to silently switch the selected series).
Livewire::withQueryParams(['series' => 'v0.10'])
->test(Index::class)
->assertSet('series', 'v0.10')
->assertViewHas('activeKey', 'v0.10');
}
public function test_chevron_right_icon_renders_a_real_path(): void
{
// The accordion disclosure caret and the pagination "Next" button both use x-icon