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
5 changed files with 315 additions and 16 deletions
Showing only changes of commit 0163fa53b4 - Show all commits

View file

@ -34,6 +34,14 @@ class Channels extends Component
public string $newDescription = ''; public string $newDescription = '';
public ?int $editingChannelId = null;
public string $editDisplayName = '';
public ?int $editLanguageId = null;
public string $editDescription = '';
public function toggle(int $channelId): void public function toggle(int $channelId): void
{ {
$channel = PlatformChannel::findOrFail($channelId); $channel = PlatformChannel::findOrFail($channelId);
@ -127,6 +135,45 @@ public function createChannel(CreateChannelAction $action): void
$this->closeCreateModal(); $this->closeCreateModal();
} }
public function openEditModal(int $channelId): void
{
$channel = PlatformChannel::findOrFail($channelId);
$this->resetErrorBag();
$this->editingChannelId = $channelId;
$this->editDisplayName = $channel->display_name;
$this->editLanguageId = $channel->language_id;
$this->editDescription = $channel->description ?? '';
}
public function closeEditModal(): void
{
$this->editingChannelId = null;
}
// The community pairing (name, channel_id, platform_instance_id) is deliberately immutable:
// it is the channel's remote identity, unique per instance, and re-pointing it would change
// the meaning of every route already attached.
public function updateChannel(): void
{
if ($this->editingChannelId === null) {
return;
}
$this->validate([
'editDisplayName' => 'required|string|max:255',
'editLanguageId' => 'nullable|integer|exists:languages,id',
]);
PlatformChannel::findOrFail($this->editingChannelId)->update([
'display_name' => $this->editDisplayName,
'language_id' => $this->editLanguageId,
'description' => $this->editDescription !== '' ? $this->editDescription : null,
]);
$this->closeEditModal();
}
public function openAccountModal(int $channelId): void public function openAccountModal(int $channelId): void
{ {
$this->managingChannelId = $channelId; $this->managingChannelId = $channelId;
@ -177,6 +224,9 @@ public function render(): View
return view('livewire.channels', [ return view('livewire.channels', [
'channels' => $channels, 'channels' => $channels,
'managingChannel' => $managingChannel, 'managingChannel' => $managingChannel,
'editingChannel' => $this->editingChannelId !== null
? PlatformChannel::with('platformInstance')->find($this->editingChannelId)
: null,
'availableAccounts' => $availableAccounts, 'availableAccounts' => $availableAccounts,
'platformInstances' => PlatformInstance::where('is_active', true)->orderBy('name')->get(), 'platformInstances' => PlatformInstance::where('is_active', true)->orderBy('name')->get(),
'languages' => Language::where('is_active', true)->orderBy('name')->get(), 'languages' => Language::where('is_active', true)->orderBy('name')->get(),

View file

@ -17,7 +17,9 @@
* @property PlatformInstance $platformInstance * @property PlatformInstance $platformInstance
* @property int $channel_id * @property int $channel_id
* @property string $name * @property string $name
* @property int $language_id * @property string $display_name
* @property string|null $description
* @property int|null $language_id
* @property Language|null $language * @property Language|null $language
* @property bool $is_active * @property bool $is_active
*/ */

View file

@ -12,12 +12,6 @@ parameters:
count: 1 count: 1
path: tests/Unit/Actions/CreateChannelActionTest.php path: tests/Unit/Actions/CreateChannelActionTest.php
-
message: '#^Call to method PHPUnit\\Framework\\Assert\:\:assertNull\(\) with int will always evaluate to false\.$#'
identifier: method.impossibleType
count: 1
path: tests/Unit/Actions/CreateChannelActionTest.php
- -
message: '#^Access to an undefined property App\\Models\\Route\:\:\$id\.$#' message: '#^Access to an undefined property App\\Models\\Route\:\:\$id\.$#'
identifier: property.notFound identifier: property.notFound

View file

@ -35,6 +35,18 @@ class="text-sm text-blue-500 hover:underline">
@endif @endif
</div> </div>
</div> </div>
<div class="flex shrink-0 items-center space-x-1">
<button
wire:click="openEditModal({{ $channel->id }})"
class="p-1 rounded-full text-gray-400 hover:bg-gray-100 hover:text-gray-600 dark:text-gray-500 dark:hover:bg-gray-700 dark:hover:text-gray-300"
title="Edit channel"
aria-label="Edit {{ $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="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125" />
</svg>
</button>
<button <button
wire:click="toggle({{ $channel->id }})" wire:click="toggle({{ $channel->id }})"
class="p-1 rounded-full {{ $channel->is_active ? 'bg-green-100 text-green-600 dark:bg-green-900/30 dark:text-green-400' : 'bg-gray-100 text-gray-400 dark:bg-gray-700 dark:text-gray-500' }}" class="p-1 rounded-full {{ $channel->is_active ? 'bg-green-100 text-green-600 dark:bg-green-900/30 dark:text-green-400' : 'bg-gray-100 text-gray-400 dark:bg-gray-700 dark:text-gray-500' }}"
@ -45,6 +57,7 @@ class="p-1 rounded-full {{ $channel->is_active ? 'bg-green-100 text-green-600 da
</svg> </svg>
</button> </button>
</div> </div>
</div>
@if ($channel->description) @if ($channel->description)
<p class="mt-3 text-sm text-gray-500 line-clamp-2 dark:text-gray-400"> <p class="mt-3 text-sm text-gray-500 line-clamp-2 dark:text-gray-400">
@ -259,4 +272,82 @@ class="inline-flex justify-center rounded-md border border-transparent shadow-xs
</form> </form>
</x-form-modal> </x-form-modal>
@endif @endif
<!-- Edit Channel Modal -->
@if ($editingChannel)
<x-form-modal title="Edit Channel" close="closeEditModal">
<form wire:submit="updateChannel" class="space-y-4">
<div>
<label for="edit-channel-display-name" class="block text-sm font-medium text-gray-700 dark:text-gray-200">Display name</label>
<input
type="text"
id="edit-channel-display-name"
wire:model="editDisplayName"
class="mt-1 block w-full rounded-md border border-gray-300 shadow-xs focus:border-blue-500 focus:ring-blue-500 sm:text-sm dark:border-gray-600"
/>
@error('editDisplayName') <p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p> @enderror
</div>
<div>
<label for="edit-channel-language" class="block text-sm font-medium text-gray-700 dark:text-gray-200">Language <span class="text-gray-400 dark:text-gray-500">(optional)</span></label>
<select
id="edit-channel-language"
wire:model="editLanguageId"
class="mt-1 block w-full rounded-md border border-gray-300 shadow-xs focus:border-blue-500 focus:ring-blue-500 sm:text-sm dark:border-gray-600"
>
<option value="">No language</option>
@foreach ($languages as $language)
<option value="{{ $language->id }}">{{ $language->name }}</option>
@endforeach
</select>
@error('editLanguageId') <p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p> @enderror
</div>
<div>
<label for="edit-channel-description" class="block text-sm font-medium text-gray-700 dark:text-gray-200">Description <span class="text-gray-400 dark:text-gray-500">(optional)</span></label>
<textarea
id="edit-channel-description"
wire:model="editDescription"
rows="2"
class="mt-1 block w-full rounded-md border border-gray-300 shadow-xs focus:border-blue-500 focus:ring-blue-500 sm:text-sm dark:border-gray-600"
></textarea>
@error('editDescription') <p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p> @enderror
</div>
<div class="rounded-md bg-gray-50 p-3 dark:bg-gray-700/30">
<dl class="space-y-1 text-sm">
<div class="flex justify-between">
<dt class="text-gray-500 dark:text-gray-400">Community</dt>
<dd class="font-medium text-gray-700 dark:text-gray-200">{{ $editingChannel->name }}</dd>
</div>
<div class="flex justify-between">
<dt class="text-gray-500 dark:text-gray-400">Instance</dt>
<dd class="font-medium text-gray-700 dark:text-gray-200">{{ $editingChannel->platformInstance->name }}</dd>
</div>
</dl>
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
The community and instance identify this channel on the remote platform and cannot be changed. Every route attached to this channel publishes here. Create a new channel to post somewhere else.
</p>
</div>
<div class="mt-6 flex justify-end space-x-3">
<button
type="button"
wire:click="closeEditModal"
class="inline-flex justify-center rounded-md border border-gray-300 shadow-xs px-4 py-2 bg-white text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-hidden focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-200 dark:hover:bg-gray-700/50"
>
Cancel
</button>
<button
type="submit"
wire:loading.attr="disabled"
wire:target="updateChannel"
class="inline-flex justify-center rounded-md border border-transparent shadow-xs px-4 py-2 bg-blue-600 text-sm font-medium text-white hover:bg-blue-700 focus:outline-hidden focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
>
Save changes
</button>
</div>
</form>
</x-form-modal>
@endif
</div> </div>

View file

@ -248,4 +248,166 @@ public function test_toggle_flips_active_state(): void
$this->assertFalse($channel->fresh()->is_active); $this->assertFalse($channel->fresh()->is_active);
} }
public function test_channel_cards_show_an_edit_action(): void
{
PlatformChannel::factory()->create(['display_name' => 'Tech Community']);
Livewire::test(Channels::class)
->assertSee('Edit Tech Community');
}
public function test_open_edit_modal_prefills_the_current_values(): void
{
$language = Language::factory()->create();
$channel = PlatformChannel::factory()->create([
'display_name' => 'Tech Community',
'description' => 'A place for tech',
'language_id' => $language->id,
]);
Livewire::test(Channels::class)
->assertSet('editingChannelId', null)
->call('openEditModal', $channel->id)
->assertSet('editingChannelId', $channel->id)
->assertSet('editDisplayName', 'Tech Community')
->assertSet('editDescription', 'A place for tech')
->assertSet('editLanguageId', $language->id)
->assertSee('Edit Channel');
}
public function test_open_edit_modal_prefills_a_null_description_as_blank(): void
{
$channel = PlatformChannel::factory()->create(['description' => null]);
Livewire::test(Channels::class)
->call('openEditModal', $channel->id)
->assertSet('editDescription', '');
}
public function test_update_channel_persists_the_changes_and_closes_the_modal(): void
{
$language = Language::factory()->create();
$channel = PlatformChannel::factory()->create([
'display_name' => 'Old Name',
'description' => 'Old description',
'language_id' => null,
]);
Livewire::test(Channels::class)
->call('openEditModal', $channel->id)
->set('editDisplayName', 'New Name')
->set('editDescription', 'New description')
->set('editLanguageId', $language->id)
->call('updateChannel')
->assertSet('editingChannelId', null)
->assertHasNoErrors();
$channel->refresh();
$this->assertSame('New Name', $channel->display_name);
$this->assertSame('New description', $channel->description);
$this->assertSame($language->id, $channel->language_id);
}
public function test_update_channel_requires_a_display_name(): void
{
$channel = PlatformChannel::factory()->create(['display_name' => 'Original']);
Livewire::test(Channels::class)
->call('openEditModal', $channel->id)
->set('editDisplayName', '')
->call('updateChannel')
->assertHasErrors(['editDisplayName' => 'required']);
$this->assertSame('Original', $channel->fresh()->display_name);
}
public function test_update_channel_rejects_a_display_name_over_the_length_limit(): void
{
$channel = PlatformChannel::factory()->create(['display_name' => 'Original']);
Livewire::test(Channels::class)
->call('openEditModal', $channel->id)
->set('editDisplayName', str_repeat('a', 256))
->call('updateChannel')
->assertHasErrors(['editDisplayName' => 'max']);
$this->assertSame('Original', $channel->fresh()->display_name);
}
public function test_update_channel_rejects_an_unknown_language(): void
{
$channel = PlatformChannel::factory()->create(['display_name' => 'Original']);
Livewire::test(Channels::class)
->call('openEditModal', $channel->id)
->set('editLanguageId', 999999)
->call('updateChannel')
->assertHasErrors(['editLanguageId' => 'exists']);
}
public function test_update_channel_allows_clearing_the_language(): void
{
$language = Language::factory()->create();
$channel = PlatformChannel::factory()->create(['language_id' => $language->id]);
Livewire::test(Channels::class)
->call('openEditModal', $channel->id)
->set('editLanguageId', null)
->call('updateChannel')
->assertHasNoErrors();
$this->assertNull($channel->fresh()->language_id);
}
public function test_update_channel_stores_a_blank_description_as_null(): void
{
$channel = PlatformChannel::factory()->create(['description' => 'Has one']);
Livewire::test(Channels::class)
->call('openEditModal', $channel->id)
->set('editDescription', '')
->call('updateChannel');
$this->assertNull($channel->fresh()->description);
}
public function test_update_channel_leaves_the_community_pairing_untouched(): void
{
$channel = PlatformChannel::factory()->create(['display_name' => 'Original']);
$before = $channel->only(['name', 'channel_id', 'platform_instance_id']);
Livewire::test(Channels::class)
->call('openEditModal', $channel->id)
->set('editDisplayName', 'Renamed')
->call('updateChannel');
$this->assertSame($before, $channel->fresh()->only(['name', 'channel_id', 'platform_instance_id']));
}
public function test_close_edit_modal_discards_the_edit(): void
{
$channel = PlatformChannel::factory()->create(['display_name' => 'Original']);
Livewire::test(Channels::class)
->call('openEditModal', $channel->id)
->set('editDisplayName', 'Discarded')
->call('closeEditModal')
->assertSet('editingChannelId', null);
$this->assertSame('Original', $channel->fresh()->display_name);
}
public function test_update_channel_does_nothing_without_an_open_modal(): void
{
$channel = PlatformChannel::factory()->create(['display_name' => 'Original']);
Livewire::test(Channels::class)
->set('editDisplayName', 'Should not apply')
->call('updateChannel')
->assertHasNoErrors();
$this->assertSame('Original', $channel->fresh()->display_name);
}
} }