diff --git a/app/Livewire/Channels.php b/app/Livewire/Channels.php index 49ea79ca..2dab6276 100644 --- a/app/Livewire/Channels.php +++ b/app/Livewire/Channels.php @@ -2,15 +2,30 @@ namespace App\Livewire; +use App\Actions\CreateChannelAction; +use App\Models\Language; use App\Models\PlatformAccount; use App\Models\PlatformChannel; +use App\Models\PlatformInstance; use Illuminate\Contracts\View\View; +use Illuminate\Validation\Rule; use Livewire\Component; +use RuntimeException; class Channels extends Component { public ?int $managingChannelId = null; + public bool $showCreateModal = false; + + public string $newName = ''; + + public ?int $newPlatformInstanceId = null; + + public ?int $newLanguageId = null; + + public string $newDescription = ''; + public function toggle(int $channelId): void { $channel = PlatformChannel::findOrFail($channelId); @@ -18,6 +33,55 @@ public function toggle(int $channelId): void $channel->save(); } + public function openCreateModal(): void + { + $this->reset(['newName', 'newPlatformInstanceId', 'newLanguageId', 'newDescription']); + $this->resetErrorBag(); + $this->showCreateModal = true; + } + + public function closeCreateModal(): void + { + $this->showCreateModal = false; + } + + public function createChannel(CreateChannelAction $action): void + { + $this->validate([ + // name doubles as the Lemmy community slug (used verbatim as channel_id for + // community lookup at publish time), so it must be lowercase slug format. + 'newName' => [ + 'required', + 'string', + 'max:255', + 'regex:/^[a-z0-9_]+$/', + Rule::unique('platform_channels', 'name') + ->where('platform_instance_id', $this->newPlatformInstanceId), + ], + 'newPlatformInstanceId' => 'required|integer|exists:platform_instances,id', + 'newLanguageId' => 'nullable|integer|exists:languages,id', + ], [ + 'newName.regex' => 'The name must be a valid community slug (lowercase letters, numbers, and underscores only).', + 'newName.unique' => 'A channel with this name already exists for this instance.', + ]); + + try { + $action->execute( + $this->newName, + $this->newPlatformInstanceId, + $this->newLanguageId, + // Blade textarea binds an empty string when blank; the action expects null for "no description". + $this->newDescription !== '' ? $this->newDescription : null, + ); + } catch (RuntimeException $e) { + $this->addError('newPlatformInstanceId', $e->getMessage()); + + return; + } + + $this->closeCreateModal(); + } + public function openAccountModal(int $channelId): void { $this->managingChannelId = $channelId; @@ -69,6 +133,8 @@ public function render(): View 'channels' => $channels, 'managingChannel' => $managingChannel, 'availableAccounts' => $availableAccounts, + '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 9bf0c75c..756a6e60 100644 --- a/resources/views/livewire/channels.blade.php +++ b/resources/views/livewire/channels.blade.php @@ -1,5 +1,15 @@
- + + +
@forelse ($channels as $channel) @@ -98,6 +108,17 @@ class="text-red-500 hover:text-red-700"

No platform channels have been configured yet.

+
+ +
@endforelse
@@ -155,4 +176,98 @@ class="w-full inline-flex justify-center rounded-md border border-gray-300 shado @endif + + + @if ($showCreateModal) + + @endif diff --git a/tests/Feature/Livewire/ChannelsTest.php b/tests/Feature/Livewire/ChannelsTest.php new file mode 100644 index 00000000..ff3d5188 --- /dev/null +++ b/tests/Feature/Livewire/ChannelsTest.php @@ -0,0 +1,199 @@ +create(['url' => 'https://lemmy.world']); + PlatformAccount::factory()->create([ + 'instance_url' => 'https://lemmy.world', + 'is_active' => true, + ]); + + return $instance; + } + + public function test_add_button_renders_when_channels_exist(): void + { + PlatformChannel::factory()->create(['name' => 'existing_channel']); + + Livewire::test(Channels::class) + ->assertSee('Add Channel') + ->assertSee('existing_channel'); + } + + public function test_add_button_renders_in_empty_state(): void + { + Livewire::test(Channels::class) + ->assertSee('No channels') + ->assertSee('Add Channel'); + } + + public function test_open_create_modal_shows_modal(): void + { + Livewire::test(Channels::class) + ->assertSet('showCreateModal', false) + ->call('openCreateModal') + ->assertSet('showCreateModal', true); + } + + public function test_create_channel_requires_name(): void + { + $instance = $this->instanceWithActiveAccount(); + + Livewire::test(Channels::class) + ->call('openCreateModal') + ->set('newPlatformInstanceId', $instance->id) + ->call('createChannel') + ->assertHasErrors(['newName' => 'required']); + } + + public function test_create_channel_requires_platform_instance(): void + { + Livewire::test(Channels::class) + ->call('openCreateModal') + ->set('newName', 'tech_community') + ->call('createChannel') + ->assertHasErrors(['newPlatformInstanceId' => 'required']); + } + + public function test_create_channel_succeeds_and_attaches_account(): void + { + $instance = $this->instanceWithActiveAccount(); + $language = Language::factory()->create(); + + Livewire::test(Channels::class) + ->call('openCreateModal') + ->set('newName', 'tech_community') + ->set('newPlatformInstanceId', $instance->id) + ->set('newLanguageId', $language->id) + ->set('newDescription', 'A tech community') + ->call('createChannel') + ->assertHasNoErrors() + ->assertSet('showCreateModal', false); + + $this->assertDatabaseHas('platform_channels', [ + 'name' => 'tech_community', + 'display_name' => 'Tech_community', + 'platform_instance_id' => $instance->id, + 'language_id' => $language->id, + 'description' => 'A tech community', + 'is_active' => true, + ]); + + $channel = PlatformChannel::where('name', 'tech_community')->firstOrFail(); + $this->assertCount(1, $channel->platformAccounts); + } + + public function test_create_channel_leaves_description_null_when_blank(): void + { + $instance = $this->instanceWithActiveAccount(); + + Livewire::test(Channels::class) + ->call('openCreateModal') + ->set('newName', 'tech_community') + ->set('newPlatformInstanceId', $instance->id) + ->call('createChannel') + ->assertHasNoErrors(); + + $this->assertDatabaseHas('platform_channels', [ + 'name' => 'tech_community', + 'description' => null, + ]); + } + + public function test_create_channel_rejects_non_slug_name(): void + { + $instance = $this->instanceWithActiveAccount(); + + Livewire::test(Channels::class) + ->call('openCreateModal') + ->set('newName', 'Tech News') + ->set('newPlatformInstanceId', $instance->id) + ->call('createChannel') + ->assertHasErrors(['newName' => 'regex']); + + $this->assertDatabaseCount('platform_channels', 0); + } + + public function test_create_channel_rejects_duplicate_name_on_same_instance(): void + { + $instance = $this->instanceWithActiveAccount(); + PlatformChannel::factory()->create([ + 'platform_instance_id' => $instance->id, + 'name' => 'tech_community', + ]); + + Livewire::test(Channels::class) + ->call('openCreateModal') + ->set('newName', 'tech_community') + ->set('newPlatformInstanceId', $instance->id) + ->call('createChannel') + ->assertHasErrors(['newName' => 'unique']) + ->assertSet('showCreateModal', true); + + $this->assertDatabaseCount('platform_channels', 1); + } + + public function test_create_channel_allows_same_name_on_different_instance(): void + { + $instanceA = $this->instanceWithActiveAccount(); + $instanceB = PlatformInstance::factory()->create(['url' => 'https://lemmy.other']); + PlatformAccount::factory()->create([ + 'instance_url' => 'https://lemmy.other', + 'is_active' => true, + ]); + PlatformChannel::factory()->create([ + 'platform_instance_id' => $instanceB->id, + 'name' => 'tech_community', + ]); + + Livewire::test(Channels::class) + ->call('openCreateModal') + ->set('newName', 'tech_community') + ->set('newPlatformInstanceId', $instanceA->id) + ->call('createChannel') + ->assertHasNoErrors(); + + $this->assertDatabaseCount('platform_channels', 2); + } + + public function test_create_channel_surfaces_no_active_accounts_error(): void + { + // Instance exists but has no active account for its url. + $instance = PlatformInstance::factory()->create(['url' => 'https://lemmy.world']); + + Livewire::test(Channels::class) + ->call('openCreateModal') + ->set('newName', 'tech_community') + ->set('newPlatformInstanceId', $instance->id) + ->call('createChannel') + ->assertHasErrors('newPlatformInstanceId') + ->assertSet('showCreateModal', true); + + $this->assertDatabaseCount('platform_channels', 0); + } + + public function test_toggle_flips_active_state(): void + { + $channel = PlatformChannel::factory()->create(['is_active' => true]); + + Livewire::test(Channels::class) + ->call('toggle', $channel->id); + + $this->assertFalse($channel->fresh()->is_active); + } +}