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'); + } +}