Release v1.4.0 #146

Merged
myrmidex merged 71 commits from release/v1.4.0 into main 2026-08-15 00:36:54 +02:00
3 changed files with 203 additions and 0 deletions
Showing only changes of commit 2b87f1f3a7 - Show all commits

View file

@ -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<int, array{articles: int, publications: int}>
*/
private function deletionImpact(): array
{
/** @var array<int, int> $articles */
$articles = RouteArticle::query()
->selectRaw('platform_channel_id, COUNT(*) as aggregate')
->groupBy('platform_channel_id')
->pluck('aggregate', 'platform_channel_id')
->all();
/** @var array<int, int> $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');

View file

@ -56,6 +56,19 @@ class="p-1 rounded-full {{ $channel->is_active ? 'bg-green-100 text-green-600 da
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z" clip-rule="evenodd" />
</svg>
</button>
@php($impact = $deletionImpact[$channel->id] ?? ['articles' => 0, 'publications' => 0])
<button
wire:click="deleteChannel({{ $channel->id }})"
wire:confirm="Delete &quot;{{ $channel->display_name }}&quot;? This also removes its routes, {{ $impact['articles'] }} article{{ $impact['articles'] === 1 ? '' : 's' }} routed to it and {{ $impact['publications'] }} publication record{{ $impact['publications'] === 1 ? '' : 's' }}. This cannot be undone."
class="p-1 rounded-full text-gray-400 hover:bg-red-50 hover:text-red-600 dark:text-gray-500 dark:hover:bg-red-900/20 dark:hover:text-red-400"
title="Delete channel"
aria-label="Delete {{ $channel->display_name }}"
>
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" />
</svg>
</button>
</div>
</div>

View file

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