137 - Add an edit option to channel cards
This commit is contained in:
parent
41a1be7ca4
commit
0163fa53b4
5 changed files with 315 additions and 16 deletions
|
|
@ -34,6 +34,14 @@ class Channels extends Component
|
|||
|
||||
public string $newDescription = '';
|
||||
|
||||
public ?int $editingChannelId = null;
|
||||
|
||||
public string $editDisplayName = '';
|
||||
|
||||
public ?int $editLanguageId = null;
|
||||
|
||||
public string $editDescription = '';
|
||||
|
||||
public function toggle(int $channelId): void
|
||||
{
|
||||
$channel = PlatformChannel::findOrFail($channelId);
|
||||
|
|
@ -127,6 +135,45 @@ public function createChannel(CreateChannelAction $action): void
|
|||
$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
|
||||
{
|
||||
$this->managingChannelId = $channelId;
|
||||
|
|
@ -177,6 +224,9 @@ public function render(): View
|
|||
return view('livewire.channels', [
|
||||
'channels' => $channels,
|
||||
'managingChannel' => $managingChannel,
|
||||
'editingChannel' => $this->editingChannelId !== null
|
||||
? PlatformChannel::with('platformInstance')->find($this->editingChannelId)
|
||||
: null,
|
||||
'availableAccounts' => $availableAccounts,
|
||||
'platformInstances' => PlatformInstance::where('is_active', true)->orderBy('name')->get(),
|
||||
'languages' => Language::where('is_active', true)->orderBy('name')->get(),
|
||||
|
|
|
|||
|
|
@ -17,7 +17,9 @@
|
|||
* @property PlatformInstance $platformInstance
|
||||
* @property int $channel_id
|
||||
* @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 bool $is_active
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -12,12 +12,6 @@ parameters:
|
|||
count: 1
|
||||
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\.$#'
|
||||
identifier: property.notFound
|
||||
|
|
|
|||
|
|
@ -35,15 +35,28 @@ class="text-sm text-blue-500 hover:underline">
|
|||
@endif
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
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' }}"
|
||||
title="{{ $channel->is_active ? 'Active - Click to disable' : 'Inactive - Click to enable' }}"
|
||||
>
|
||||
<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<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>
|
||||
<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
|
||||
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' }}"
|
||||
title="{{ $channel->is_active ? 'Active - Click to disable' : 'Inactive - Click to enable' }}"
|
||||
>
|
||||
<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if ($channel->description)
|
||||
|
|
@ -259,4 +272,82 @@ class="inline-flex justify-center rounded-md border border-transparent shadow-xs
|
|||
</form>
|
||||
</x-form-modal>
|
||||
@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>
|
||||
|
|
|
|||
|
|
@ -248,4 +248,166 @@ public function test_toggle_flips_active_state(): void
|
|||
|
||||
$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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue