From 518bc9aa4139bc584bb69a52935b19e35ff222f4 Mon Sep 17 00:00:00 2001 From: nexxo Date: Wed, 29 Jul 2026 23:46:13 +0200 Subject: [PATCH] Put the open tab in the address bar, including the first one MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reported as "die Tabs fehlen in der Query beim Kunden". The binding was there and identical to the settings page — checked in the rendered snapshot, which is where Livewire's JS reads it from — but `except` hid the parameter while the DEFAULT tab was open. So arriving on a customer showed no ?tab= at all, and a link to the page could not say which tab it meant unless the tab happened not to be the first one. From the address bar that is indistinguishable from the feature not existing. No `except` now, on all three tabbed pages: the parameter is there from the first render. And `history: true`, so each tab is a step the back button can take rather than a silent replaceState — which also makes the change unmistakable while clicking. Asserted against the rendered page rather than the component, because the parameter is written client-side from the `url` memo in the snapshot: the memo IS the behaviour, and asserting the property would have passed throughout the bug. Co-Authored-By: Claude Opus 5 --- VERSION | 2 +- app/Livewire/Admin/CustomerDetail.php | 12 ++++++- app/Livewire/Admin/Integrations.php | 12 ++++++- app/Livewire/Admin/Settings.php | 12 ++++++- .../Feature/Admin/CustomerDetailTabsTest.php | 2 +- tests/Feature/Admin/SettingsTabsTest.php | 33 ++++++++++++++++++- 6 files changed, 67 insertions(+), 6 deletions(-) diff --git a/VERSION b/VERSION index 3eba9bd..36a5f6b 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.3.42 +1.3.43 diff --git a/app/Livewire/Admin/CustomerDetail.php b/app/Livewire/Admin/CustomerDetail.php index 76df2d5..a43cd40 100644 --- a/app/Livewire/Admin/CustomerDetail.php +++ b/app/Livewire/Admin/CustomerDetail.php @@ -50,7 +50,17 @@ class CustomerDetail extends Component public Customer $customer; - #[Url(except: 'overview')] + /** + * Which tab is open, in the address bar. + * + * `history: true`, so each tab is a step the back button can take — and no + * `except`, so the parameter is there from the first render rather than + * appearing only once somebody leaves the default tab. Hiding it on the + * default read as "the tab is not in the URL", which is exactly how it was + * reported: a link to this page could not say which tab it meant unless the + * tab happened not to be the first one. + */ + #[Url(history: true)] public string $tab = 'overview'; // ---- Writing to them ---- diff --git a/app/Livewire/Admin/Integrations.php b/app/Livewire/Admin/Integrations.php index a9fbcea..bd0f8e7 100644 --- a/app/Livewire/Admin/Integrations.php +++ b/app/Livewire/Admin/Integrations.php @@ -76,7 +76,17 @@ class Integrations extends Component */ public const TABS = ['services', 'platform', 'env']; - #[Url(except: 'services')] + /** + * Which tab is open, in the address bar. + * + * `history: true`, so each tab is a step the back button can take — and no + * `except`, so the parameter is there from the first render rather than + * appearing only once somebody leaves the default tab. Hiding it on the + * default read as "the tab is not in the URL", which is exactly how it was + * reported: a link to this page could not say which tab it meant unless the + * tab happened not to be the first one. + */ + #[Url(history: true)] public string $tab = 'services'; use ConfirmsPassword { diff --git a/app/Livewire/Admin/Settings.php b/app/Livewire/Admin/Settings.php index 297a8f7..0a369d5 100644 --- a/app/Livewire/Admin/Settings.php +++ b/app/Livewire/Admin/Settings.php @@ -64,7 +64,17 @@ class Settings extends Component * needs no parameter, and a query string that appears the moment a page * loads makes every link to it look different from the one in the menu. */ - #[Url(except: 'installation')] + /** + * Which tab is open, in the address bar. + * + * `history: true`, so each tab is a step the back button can take — and no + * `except`, so the parameter is there from the first render rather than + * appearing only once somebody leaves the default tab. Hiding it on the + * default read as "the tab is not in the URL", which is exactly how it was + * reported: a link to this page could not say which tab it meant unless the + * tab happened not to be the first one. + */ + #[Url(history: true)] public string $tab = 'installation'; /** This page changes the signed-in OPERATOR's password, never a portal one. */ diff --git a/tests/Feature/Admin/CustomerDetailTabsTest.php b/tests/Feature/Admin/CustomerDetailTabsTest.php index 33e3748..2b5b3f1 100644 --- a/tests/Feature/Admin/CustomerDetailTabsTest.php +++ b/tests/Feature/Admin/CustomerDetailTabsTest.php @@ -34,7 +34,7 @@ beforeEach(function () { ]); }); -it('opens on the details, and keeps that tab out of the address bar', function () { +it('opens on the details', function () { Livewire::actingAs(operator('Owner'), 'operator') ->test(CustomerDetail::class, ['uuid' => $this->customer->uuid]) ->assertSet('tab', 'overview') diff --git a/tests/Feature/Admin/SettingsTabsTest.php b/tests/Feature/Admin/SettingsTabsTest.php index e876cc7..b1809e3 100644 --- a/tests/Feature/Admin/SettingsTabsTest.php +++ b/tests/Feature/Admin/SettingsTabsTest.php @@ -13,7 +13,7 @@ use Livewire\Livewire; * refreshes itself while an update runs, so a tab kept only in the component * would drop them back to the first one mid-deployment. */ -it('opens the installation tab by default and keeps it out of the address bar', function () { +it('opens the installation tab by default', function () { Livewire::actingAs(operator('Owner'), 'operator') ->test(Settings::class) ->assertSet('tab', 'installation') @@ -133,3 +133,34 @@ it('tabs the integrations page the same way, and keeps the raw file to the Owner ->assertSet('tab', 'services') ->assertViewHas('tabs', fn (array $tabs) => ! in_array('env', $tabs, true)); }); + +it('puts the open tab in the address bar from the first render, on every tabbed page', function () { + // Reported as "die Tabs fehlen in der Query beim Kunden". The binding was + // there, but `except` hid the parameter while the default tab was open — so + // a link to the page could not say which tab it meant unless the tab + // happened not to be the first one, and the address bar looked like the + // feature was missing. + // + // Asserted against the rendered page rather than the component: the + // parameter is written by Livewire's JS from the `url` memo in the snapshot, + // so the memo IS the behaviour. + $customer = App\Models\Customer::factory()->create(); + + $pages = [ + route('admin.settings'), + route('admin.integrations'), + route('admin.customer', $customer->uuid), + ]; + + foreach ($pages as $url) { + $html = html_entity_decode( + (string) $this->actingAs(operator('Owner'), 'operator')->get($url)->getContent() + ); + + expect($html)->toContain('"url":{"tab":') + // No `except`: shown even on the default tab. + ->and($html)->toMatch('/"tab":\{[^}]*"except":null/') + // push, not replace: each tab is a step the back button can take. + ->and($html)->toMatch('/"tab":\{[^}]*"use":"push"/'); + } +});