From 2b87f1f3a73cdc3e91c8789154e25a875fbf6310 Mon Sep 17 00:00:00 2001 From: myrmidex Date: Fri, 14 Aug 2026 00:54:17 +0200 Subject: [PATCH] 141 - Add channel deletion to the Channels page --- app/Livewire/Channels.php | 66 +++++++++++ resources/views/livewire/channels.blade.php | 13 ++ tests/Feature/Livewire/ChannelsTest.php | 124 ++++++++++++++++++++ 3 files changed, 203 insertions(+) diff --git a/app/Livewire/Channels.php b/app/Livewire/Channels.php index 795fc8b0..bbdc944f 100644 --- a/app/Livewire/Channels.php +++ b/app/Livewire/Channels.php @@ -3,10 +3,14 @@ namespace App\Livewire; use App\Actions\CreateChannelAction; +use App\Enums\LogLevelEnum; +use App\Events\ActionPerformed; +use App\Models\ArticlePublication; use App\Models\Language; use App\Models\PlatformAccount; use App\Models\PlatformChannel; use App\Models\PlatformInstance; +use App\Models\RouteArticle; use App\Services\Platform\CommunityDirectory; use Exception; use Illuminate\Contracts\View\View; @@ -49,6 +53,34 @@ public function toggle(int $channelId): void $channel->save(); } + public function deleteChannel(int $channelId): void + { + $channel = PlatformChannel::find($channelId); + + if (! $channel instanceof PlatformChannel) { + return; + } + + $name = $channel->display_name; + + // Routes, keywords, route articles, publications, account links and synced posts + // all cascade at the database level. + $channel->delete(); + + if ($this->managingChannelId === $channelId) { + $this->managingChannelId = null; + } + + if ($this->editingChannelId === $channelId) { + $this->editingChannelId = null; + } + + ActionPerformed::dispatch('Deleted platform channel', LogLevelEnum::WARNING, [ + 'platform_channel_id' => $channelId, + 'display_name' => $name, + ]); + } + public function openCreateModal(): void { $this->reset(['newCommunityId', 'newPlatformInstanceId', 'newLanguageId', 'newDescription', 'availableCommunities', 'communityLoadError']); @@ -208,6 +240,39 @@ public function detachAccount(int $channelId, int $accountId): void $channel->platformAccounts()->detach($accountId); } + /** + * Row counts per channel, so the delete confirmation can say what is about to go. + * + * @return array + */ + private function deletionImpact(): array + { + /** @var array $articles */ + $articles = RouteArticle::query() + ->selectRaw('platform_channel_id, COUNT(*) as aggregate') + ->groupBy('platform_channel_id') + ->pluck('aggregate', 'platform_channel_id') + ->all(); + + /** @var array $publications */ + $publications = ArticlePublication::query() + ->selectRaw('platform_channel_id, COUNT(*) as aggregate') + ->groupBy('platform_channel_id') + ->pluck('aggregate', 'platform_channel_id') + ->all(); + + $impact = []; + + foreach (array_keys($articles + $publications) as $channelId) { + $impact[$channelId] = [ + 'articles' => (int) ($articles[$channelId] ?? 0), + 'publications' => (int) ($publications[$channelId] ?? 0), + ]; + } + + return $impact; + } + public function render(): View { $channels = PlatformChannel::with(['platformInstance', 'platformAccounts'])->orderBy('name')->get(); @@ -228,6 +293,7 @@ public function render(): View ? PlatformChannel::with('platformInstance')->find($this->editingChannelId) : null, 'availableAccounts' => $availableAccounts, + 'deletionImpact' => $this->deletionImpact(), 'platformInstances' => PlatformInstance::where('is_active', true)->orderBy('name')->get(), 'languages' => Language::where('is_active', true)->orderBy('name')->get(), ])->layout('layouts.app'); diff --git a/resources/views/livewire/channels.blade.php b/resources/views/livewire/channels.blade.php index a29461ba..b3d69567 100644 --- a/resources/views/livewire/channels.blade.php +++ b/resources/views/livewire/channels.blade.php @@ -56,6 +56,19 @@ class="p-1 rounded-full {{ $channel->is_active ? 'bg-green-100 text-green-600 da + + @php($impact = $deletionImpact[$channel->id] ?? ['articles' => 0, 'publications' => 0]) + diff --git a/tests/Feature/Livewire/ChannelsTest.php b/tests/Feature/Livewire/ChannelsTest.php index f1a1e6df..0e3a894d 100644 --- a/tests/Feature/Livewire/ChannelsTest.php +++ b/tests/Feature/Livewire/ChannelsTest.php @@ -3,10 +3,14 @@ namespace Tests\Feature\Livewire; use App\Livewire\Channels; +use App\Models\Article; +use App\Models\ArticlePublication; +use App\Models\Feed; use App\Models\Language; use App\Models\PlatformAccount; use App\Models\PlatformChannel; use App\Models\PlatformInstance; +use App\Models\RouteArticle; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Http\Client\Factory; use Illuminate\Support\Facades\Cache; @@ -410,4 +414,124 @@ public function test_update_channel_does_nothing_without_an_open_modal(): void $this->assertSame('Original', $channel->fresh()->display_name); } + + public function test_channel_cards_show_a_delete_action(): void + { + PlatformChannel::factory()->create(['display_name' => 'Tech Community']); + + Livewire::test(Channels::class) + ->assertSee('Delete Tech Community'); + } + + public function test_deleting_a_channel_removes_it(): void + { + $channel = PlatformChannel::factory()->create(); + $survivor = PlatformChannel::factory()->create(); + + Livewire::test(Channels::class)->call('deleteChannel', $channel->id); + + $this->assertDatabaseMissing('platform_channels', ['id' => $channel->id]); + $this->assertDatabaseHas('platform_channels', ['id' => $survivor->id]); + } + + public function test_deleting_an_unknown_channel_does_nothing(): void + { + PlatformChannel::factory()->create(); + + Livewire::test(Channels::class) + ->call('deleteChannel', 999999) + ->assertHasNoErrors(); + + $this->assertSame(1, PlatformChannel::count()); + } + + public function test_deleting_the_channel_being_edited_closes_the_edit_modal(): void + { + $channel = PlatformChannel::factory()->create(); + + Livewire::test(Channels::class) + ->call('openEditModal', $channel->id) + ->assertSet('editingChannelId', $channel->id) + ->call('deleteChannel', $channel->id) + ->assertSet('editingChannelId', null); + } + + public function test_deleting_the_channel_being_managed_closes_the_account_modal(): void + { + $channel = PlatformChannel::factory()->create(); + + Livewire::test(Channels::class) + ->call('openAccountModal', $channel->id) + ->assertSet('managingChannelId', $channel->id) + ->call('deleteChannel', $channel->id) + ->assertSet('managingChannelId', null); + } + + public function test_the_confirmation_states_what_will_be_removed(): void + { + $feed = Feed::factory()->create(); + $channel = PlatformChannel::factory()->create(['display_name' => 'Tech Community']); + $article = Article::factory()->create(['feed_id' => $feed->id]); + + RouteArticle::factory()->create([ + 'feed_id' => $feed->id, + 'platform_channel_id' => $channel->id, + 'article_id' => $article->id, + ]); + + ArticlePublication::factory()->create([ + 'article_id' => $article->id, + 'platform_channel_id' => $channel->id, + ]); + + Livewire::test(Channels::class) + ->assertSee('1 article routed to it') + ->assertSee('1 publication record'); + } + + public function test_each_card_shows_its_own_counts_not_the_totals(): void + { + $feed = Feed::factory()->create(); + $busy = PlatformChannel::factory()->create(['name' => 'aaa-busy', 'display_name' => 'Busy Channel']); + $quiet = PlatformChannel::factory()->create(['name' => 'zzz-quiet', 'display_name' => 'Quiet Channel']); + + foreach (range(1, 3) as $ignored) { + $article = Article::factory()->create(['feed_id' => $feed->id]); + RouteArticle::factory()->create([ + 'feed_id' => $feed->id, + 'platform_channel_id' => $busy->id, + 'article_id' => $article->id, + ]); + } + + $quietArticle = Article::factory()->create(['feed_id' => $feed->id]); + RouteArticle::factory()->create([ + 'feed_id' => $feed->id, + 'platform_channel_id' => $quiet->id, + 'article_id' => $quietArticle->id, + ]); + + Livewire::test(Channels::class) + ->assertSee('3 articles routed to it') + ->assertSee('1 article routed to it'); + } + + public function test_the_confirmation_pluralises_counts(): void + { + $feed = Feed::factory()->create(); + $channel = PlatformChannel::factory()->create(); + + foreach (range(1, 2) as $ignored) { + $article = Article::factory()->create(['feed_id' => $feed->id]); + RouteArticle::factory()->create([ + 'feed_id' => $feed->id, + 'platform_channel_id' => $channel->id, + 'article_id' => $article->id, + ]); + } + + Livewire::test(Channels::class) + ->assertSee('2 articles routed to it') + ->assertSee('0 publication records'); + } }