From 85465080d230641c76b0a4deb60df7f8067fec27 Mon Sep 17 00:00:00 2001 From: bernardhanna Date: Thu, 17 Sep 2026 15:24:38 +0100 Subject: [PATCH] Keep the online activities filters when changing page The pagination links are ordinary hrefs, so clicking page two is a full browser navigation. Neither filter was in the URL and mount() reset both on every request, so the new page came back unfiltered. Carry the language and month in the query string, append them to the page links, and mark the matching option selected so the dropdowns agree with the list. Co-authored-by: Cursor --- app/Livewire/OnlineCalendar.php | 21 ++- .../views/livewire/online-calendar.blade.php | 6 +- .../OnlineActivitiesFilterPaginationTest.php | 151 ++++++++++++++++++ 3 files changed, 172 insertions(+), 6 deletions(-) create mode 100644 tests/Feature/OnlineActivitiesFilterPaginationTest.php diff --git a/app/Livewire/OnlineCalendar.php b/app/Livewire/OnlineCalendar.php index d4bc9f407..97388b9e8 100644 --- a/app/Livewire/OnlineCalendar.php +++ b/app/Livewire/OnlineCalendar.php @@ -5,6 +5,7 @@ use App\Country; use App\Event; use Carbon\Carbon; +use Livewire\Attributes\Url; use Livewire\Component; use Livewire\WithPagination; @@ -12,12 +13,16 @@ class OnlineCalendar extends Component { use WithPagination; + // The pagination links are ordinary hrefs, so every page change is a fresh request. + // Both filters have to travel in the URL or the new page comes back unfiltered. + #[Url(as: 'language')] public $selectedLanguage = ''; public $selectedYear; public $selectedMonth; + #[Url(as: 'month')] public $selectedDate = 'all'; public $months = []; @@ -26,8 +31,6 @@ public function mount() { $this->selectedYear = Carbon::now()->year; $this->selectedMonth = Carbon::now()->month; - $this->selectedDate = 'all'; - $this->selectedLanguage = ''; $this->months = $this->baseQuery() ->orderBy('start_date') @@ -114,13 +117,25 @@ public function render() return view('livewire.online-calendar', [ 'countryNames' => $this->getCountryNamesFromEvents($events), 'languages' => $languages, - 'filteredEvents' => $filteredEvents->paginate(24), + 'filteredEvents' => $filteredEvents->paginate(24)->appends($this->activeFilters()), 'totalUpcoming' => $totalUpcoming, 'visibleCount' => $filteredEvents->count(), 'monthLabel' => $monthLabel, ]); } + /** + * The active filters under the names they use in the URL, so that the page links + * rebuilt by the paginator carry them over. + */ + private function activeFilters(): array + { + return array_filter([ + 'language' => $this->selectedLanguage, + 'month' => $this->selectedDate === 'all' ? null : $this->selectedDate, + ]); + } + private function baseQuery() { // The start date alone decides what is upcoming. Gating on the end date instead diff --git a/resources/views/livewire/online-calendar.blade.php b/resources/views/livewire/online-calendar.blade.php index c9dfe1557..3fa7ee79b 100644 --- a/resources/views/livewire/online-calendar.blade.php +++ b/resources/views/livewire/online-calendar.blade.php @@ -12,9 +12,9 @@ wire:model.live="selectedDate" class="w-full appearance-none rounded-full border border-slate-200 bg-white py-3 pl-12 pr-10 text-slate-500 font-semibold focus:outline-none focus:ring-2 focus:ring-[#1C4DA1]" > - + @foreach($months as $month) - + @endforeach @@ -29,7 +29,7 @@ class="w-full appearance-none rounded-full border border-slate-200 bg-white py-3 class="w-full appearance-none rounded-full border border-slate-200 bg-white py-3 px-4 text-slate-500 font-semibold focus:outline-none focus:ring-2 focus:ring-[#1C4DA1]" > @foreach($languages as $language) - + @endforeach diff --git a/tests/Feature/OnlineActivitiesFilterPaginationTest.php b/tests/Feature/OnlineActivitiesFilterPaginationTest.php new file mode 100644 index 000000000..7fa5ee9a2 --- /dev/null +++ b/tests/Feature/OnlineActivitiesFilterPaginationTest.php @@ -0,0 +1,151 @@ +create([ + 'start_date' => Carbon::now()->addWeek(), + 'end_date' => Carbon::now()->addWeek()->addDay(), + 'status' => 'APPROVED', + 'activity_type' => 'open-online', + 'highlighted_status' => 'NONE', + 'language' => $languages, + 'title' => $title, + ]); + } + + /** + * Reported by the ambassadors: pick a language, walk to page two and the list is + * back to every language, because the page link is an ordinary browser navigation + * and the chosen filter never reached the URL. + */ + #[Test] + public function the_language_filter_survives_a_jump_to_the_second_page(): void + { + $this->seed('RolesAndPermissionsSeeder'); + + // 25 English activities push the English-only list onto a second page. + for ($i = 1; $i <= 25; $i++) { + $this->makeActivity('English Activity Number '.$i, ['en']); + } + + $german = $this->makeActivity('German Only Activity', ['de']); + + $this->get('/online-activities?language=en&page=2') + ->assertStatus(200) + ->assertDontSee($german->title); + } + + #[Test] + public function the_month_filter_survives_a_jump_to_the_second_page(): void + { + $this->seed('RolesAndPermissionsSeeder'); + + $nextMonth = Carbon::now()->addMonthNoOverflow()->startOfMonth(); + + for ($i = 1; $i <= 25; $i++) { + Event::factory()->create([ + 'start_date' => $nextMonth->copy()->addDays(2), + 'end_date' => $nextMonth->copy()->addDays(3), + 'status' => 'APPROVED', + 'activity_type' => 'open-online', + 'highlighted_status' => 'NONE', + 'language' => ['en'], + 'title' => 'Next Month Activity Number '.$i, + ]); + } + + $thisWeek = $this->makeActivity('Happening This Week Activity', ['en']); + + $month = $nextMonth->month.'/'.$nextMonth->year; + + $this->get('/online-activities?month='.urlencode($month).'&page=2') + ->assertStatus(200) + ->assertSee('for '.$nextMonth->format('F Y')) + ->assertDontSee($thisWeek->title); + } + + #[Test] + public function pagination_links_carry_the_active_filters(): void + { + $this->seed('RolesAndPermissionsSeeder'); + + for ($i = 1; $i <= 25; $i++) { + $this->makeActivity('English Activity Number '.$i, ['en']); + } + + $html = $this->get('/online-activities?language=en')->getContent(); + + $this->assertMatchesRegularExpression( + '/href="[^"]*page=2[^"]*language=en|href="[^"]*language=en[^"]*page=2/', + $html, + 'The link to page two must keep the chosen language.' + ); + } + + /** + * The order the ambassadors hit it in: pick the language from the dropdown, which is + * a Livewire round trip, then click page two, which is a browser navigation. + */ + #[Test] + public function choosing_a_language_then_paginating_keeps_the_language(): void + { + $this->seed('RolesAndPermissionsSeeder'); + + for ($i = 1; $i <= 25; $i++) { + $this->makeActivity('English Activity Number '.$i, ['en']); + } + + $german = $this->makeActivity('German Only Activity', ['de']); + + $html = Livewire::test(OnlineCalendar::class) + ->set('selectedLanguage', 'en') + ->html(); + + preg_match('/href="([^"]*page=2[^"]*)"/', $html, $matches); + + $this->assertNotEmpty($matches, 'A link to page two should be rendered.'); + + $this->get($matches[1]) + ->assertStatus(200) + ->assertDontSee($german->title); + } + + #[Test] + public function the_dropdowns_show_the_filter_that_is_in_force(): void + { + $this->seed('RolesAndPermissionsSeeder'); + + $this->makeActivity('English Activity', ['en']); + $this->makeActivity('German Activity', ['de']); + + $this->get('/online-activities?language=en') + ->assertStatus(200) + ->assertSee('', false); + } + + #[Test] + public function the_filters_are_read_from_the_query_string(): void + { + $this->seed('RolesAndPermissionsSeeder'); + + $this->makeActivity('English Activity', ['en']); + + Livewire::withQueryParams(['language' => 'en']) + ->test(OnlineCalendar::class) + ->assertSet('selectedLanguage', 'en'); + } +}