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)
+
+
+
+
+
+
+
+
+
Add Channel
+
+
+
+
+
+
+
+ @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);
+ }
+}