From 817219df618a9d3b758dc4ee8deae459fd50034f Mon Sep 17 00:00:00 2001 From: myrmidex Date: Sun, 9 Aug 2026 09:41:47 +0200 Subject: [PATCH] 113 - Filter articles by feed on both tabs --- app/Livewire/Articles.php | 25 ++++ resources/views/livewire/articles.blade.php | 41 +++-- tests/Feature/Livewire/ArticlesTest.php | 157 ++++++++++++++++++++ 3 files changed, 210 insertions(+), 13 deletions(-) diff --git a/app/Livewire/Articles.php b/app/Livewire/Articles.php index 1375748e..199807d2 100644 --- a/app/Livewire/Articles.php +++ b/app/Livewire/Articles.php @@ -21,6 +21,8 @@ class Articles extends Component public string $search = ''; + public ?int $feedId = null; + public bool $isRefreshing = false; /** @var array */ @@ -34,6 +36,12 @@ public function setTab(string $tab): void $this->resetPage(); } + public function updatedFeedId(): void + { + $this->expandedFeeds = []; + $this->resetPage(); + } + public function toggleFeed(int $feedId): void { if (isset($this->expandedFeeds[$feedId])) { @@ -89,6 +97,7 @@ public function render(): View return view('livewire.articles', [ 'routeArticles' => null, 'pendingFeeds' => $this->pendingFeeds(), + 'feedOptions' => $this->feedOptions(), 'pendingCount' => $pendingCount, ])->layout('layouts.app'); } @@ -96,6 +105,10 @@ public function render(): View $query = RouteArticle::with(['article.feed', 'feed', 'platformChannel']) ->orderBy('created_at', 'desc'); + if ($this->feedId !== null) { + $query->where('feed_id', $this->feedId); + } + if ($this->search !== '') { $search = $this->search; $query->whereHas('article', function ($q) use ($search) { @@ -107,6 +120,7 @@ public function render(): View return view('livewire.articles', [ 'routeArticles' => $query->paginate(15), 'pendingFeeds' => null, + 'feedOptions' => $this->feedOptions(), 'pendingCount' => $pendingCount, ])->layout('layouts.app'); } @@ -119,6 +133,7 @@ private function pendingFeeds(): Collection $counts = RouteArticle::query() ->selectRaw('feed_id, COUNT(*) as aggregate') ->where('approval_status', ApprovalStatusEnum::PENDING) + ->when($this->feedId !== null, fn ($q) => $q->where('feed_id', $this->feedId)) ->groupBy('feed_id') ->pluck('aggregate', 'feed_id'); @@ -134,6 +149,16 @@ private function pendingFeeds(): Collection ->values(); } + /** + * @return EloquentCollection + */ + private function feedOptions(): EloquentCollection + { + return Feed::whereIn('id', RouteArticle::query()->select('feed_id')->distinct()) + ->orderBy('name') + ->get(); + } + /** * @return EloquentCollection */ diff --git a/resources/views/livewire/articles.blade.php b/resources/views/livewire/articles.blade.php index 91392bd0..56df96b7 100644 --- a/resources/views/livewire/articles.blade.php +++ b/resources/views/livewire/articles.blade.php @@ -39,7 +39,29 @@ class="whitespace-nowrap pb-3 px-1 border-b-2 font-medium text-sm {{ $tab === 'a {{-- Tab actions --}} -
+
+
+ + @if ($tab === 'all') +
+ +
+ @endif +
@if ($tab === 'pending' && $pendingCount > 0)
- @else -
@endif
@@ -115,10 +126,14 @@ class="h-4 w-4 shrink-0 text-gray-400 transition-transform {{ $isExpanded ? 'rot @endif

- @if ($tab === 'pending') + @if ($tab === 'pending' && $feedId !== null) + No pending articles for the selected feed. + @elseif ($tab === 'pending') All route articles have been reviewed. @elseif ($search !== '') No results for "{{ $search }}". + @elseif ($feedId !== null) + No articles for the selected feed. @else No route articles have been created yet. @endif diff --git a/tests/Feature/Livewire/ArticlesTest.php b/tests/Feature/Livewire/ArticlesTest.php index b56c3689..acde862d 100644 --- a/tests/Feature/Livewire/ArticlesTest.php +++ b/tests/Feature/Livewire/ArticlesTest.php @@ -11,6 +11,7 @@ use App\Models\RouteArticle; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Event; +use Livewire\Features\SupportTesting\Testable; use Livewire\Livewire; use Tests\TestCase; @@ -51,6 +52,17 @@ private function createRouteArticleForFeed(Feed $feed, string $title, ApprovalSt return $routeArticle; } + /** + * @param Testable $component + * @return array + */ + private function renderedFeedGroupIds(Testable $component): array + { + preg_match_all('/wire:key="feed-group-(\d+)"/', $component->html(), $matches); + + return array_map('intval', $matches[1]); + } + public function test_renders_successfully(): void { Livewire::test(Articles::class) @@ -289,6 +301,151 @@ public function test_all_tab_shows_a_flat_list_not_feed_groups(): void ->assertSee('Belga Article'); } + public function test_feed_filter_narrows_pending_tab_to_one_feed(): void + { + $vrt = Feed::factory()->create(['name' => 'VRT News']); + $belga = Feed::factory()->create(['name' => 'Belga Press']); + + $this->createRouteArticleForFeed($vrt, 'VRT Headline'); + $this->createRouteArticleForFeed($belga, 'Belga Headline'); + + $component = Livewire::test(Articles::class)->set('feedId', $vrt->id); + + $this->assertSame([$vrt->id], $this->renderedFeedGroupIds($component)); + } + + public function test_feed_filter_narrows_all_tab_to_one_feed(): void + { + $vrt = Feed::factory()->create(['name' => 'VRT News']); + $belga = Feed::factory()->create(['name' => 'Belga Press']); + + $this->createRouteArticleForFeed($vrt, 'VRT Headline'); + $this->createRouteArticleForFeed($belga, 'Belga Headline'); + + Livewire::test(Articles::class) + ->call('setTab', 'all') + ->set('feedId', $vrt->id) + ->assertSee('VRT Headline') + ->assertDontSee('Belga Headline'); + } + + public function test_feed_filter_combines_with_search_on_all_tab(): void + { + $vrt = Feed::factory()->create(['name' => 'VRT News']); + $belga = Feed::factory()->create(['name' => 'Belga Press']); + + $this->createRouteArticleForFeed($vrt, 'Belgian Politics Update'); + $this->createRouteArticleForFeed($vrt, 'Weather Forecast Today'); + $this->createRouteArticleForFeed($belga, 'Belgian Economy Report'); + + Livewire::test(Articles::class) + ->call('setTab', 'all') + ->set('feedId', $vrt->id) + ->set('search', 'Belgian') + ->assertSee('Belgian Politics Update') + ->assertDontSee('Weather Forecast Today') + ->assertDontSee('Belgian Economy Report'); + } + + public function test_feed_filter_combines_with_tab_status_filtering(): void + { + $vrt = Feed::factory()->create(['name' => 'VRT News']); + + $this->createRouteArticleForFeed($vrt, 'VRT Pending'); + $this->createRouteArticleForFeed($vrt, 'VRT Approved', ApprovalStatusEnum::APPROVED); + + Livewire::test(Articles::class) + ->set('feedId', $vrt->id) + ->call('toggleFeed', $vrt->id) + ->assertSee('VRT Pending') + ->assertDontSee('VRT Approved'); + } + + public function test_clearing_feed_filter_restores_all_feeds(): void + { + $vrt = Feed::factory()->create(['name' => 'VRT News']); + $belga = Feed::factory()->create(['name' => 'Belga Press']); + + $this->createRouteArticleForFeed($vrt, 'VRT Headline'); + $this->createRouteArticleForFeed($belga, 'Belga Headline'); + + $component = Livewire::test(Articles::class)->set('feedId', $vrt->id); + + $this->assertSame([$vrt->id], $this->renderedFeedGroupIds($component)); + + $component->set('feedId', null); + + $this->assertEqualsCanonicalizing([$vrt->id, $belga->id], $this->renderedFeedGroupIds($component)); + } + + public function test_changing_feed_filter_resets_pagination(): void + { + $vrt = Feed::factory()->create(['name' => 'VRT News']); + + for ($i = 0; $i < 20; $i++) { + $this->createRouteArticleForFeed($vrt, "VRT Article {$i}"); + } + + Livewire::test(Articles::class) + ->call('setTab', 'all') + ->set('paginators.page', 2) + ->set('feedId', $vrt->id) + ->assertSet('paginators.page', 1); + } + + public function test_changing_feed_filter_collapses_expanded_feeds(): void + { + $vrt = Feed::factory()->create(['name' => 'VRT News']); + + $this->createRouteArticleForFeed($vrt, 'VRT Headline'); + + Livewire::test(Articles::class) + ->call('toggleFeed', $vrt->id) + ->assertSee('VRT Headline') + ->set('feedId', $vrt->id) + ->assertSet('expandedFeeds', []) + ->assertDontSee('VRT Headline'); + } + + public function test_feed_filter_persists_across_tab_switches(): void + { + $vrt = Feed::factory()->create(['name' => 'VRT News']); + + $this->createRouteArticleForFeed($vrt, 'VRT Headline'); + + Livewire::test(Articles::class) + ->set('feedId', $vrt->id) + ->call('setTab', 'all') + ->assertSet('feedId', $vrt->id) + ->call('setTab', 'pending') + ->assertSet('feedId', $vrt->id); + } + + public function test_feed_filter_options_only_include_feeds_with_route_articles(): void + { + $vrt = Feed::factory()->create(['name' => 'VRT News']); + $unused = Feed::factory()->create(['name' => 'Unused Feed']); + + $this->createRouteArticleForFeed($vrt, 'VRT Headline'); + + Livewire::test(Articles::class) + ->assertSeeHtml('') + ->assertDontSeeHtml(''); + } + + public function test_empty_state_when_feed_filter_matches_nothing_on_pending_tab(): void + { + $vrt = Feed::factory()->create(['name' => 'VRT News']); + $belga = Feed::factory()->create(['name' => 'Belga Press']); + + $this->createRouteArticleForFeed($vrt, 'VRT Headline'); + $this->createRouteArticleForFeed($belga, 'Belga Approved', ApprovalStatusEnum::APPROVED); + + Livewire::test(Articles::class) + ->set('feedId', $belga->id) + ->assertSee('No pending articles for the selected feed.'); + } + public function test_empty_state_on_pending_tab(): void { Livewire::test(Articles::class)