diff --git a/app/Livewire/Feeds.php b/app/Livewire/Feeds.php index 1ac63001..8d0259db 100644 --- a/app/Livewire/Feeds.php +++ b/app/Livewire/Feeds.php @@ -21,6 +21,12 @@ class Feeds extends Component public string $newDescription = ''; + public ?int $editingFeedId = null; + + public string $editName = ''; + + public string $editDescription = ''; + public function toggle(int $feedId): void { $feed = Feed::findOrFail($feedId); @@ -67,6 +73,41 @@ public function createFeed(CreateFeedAction $action): void $this->closeCreateModal(); } + public function openEditModal(int $feedId): void + { + $feed = Feed::findOrFail($feedId); + + $this->resetErrorBag(); + $this->editingFeedId = $feedId; + $this->editName = $feed->name; + $this->editDescription = $feed->description ?? ''; + } + + public function closeEditModal(): void + { + $this->editingFeedId = null; + } + + // Provider and language are deliberately not editable: CreateFeedAction derives the unique + // feeds.url from that pair, so changing either re-points the feed and orphans its articles. + public function updateFeed(): void + { + if ($this->editingFeedId === null) { + return; + } + + $this->validate([ + 'editName' => 'required|string|max:255', + ]); + + Feed::findOrFail($this->editingFeedId)->update([ + 'name' => $this->editName, + 'description' => $this->editDescription !== '' ? $this->editDescription : null, + ]); + + $this->closeEditModal(); + } + /** * @return array> */ @@ -86,6 +127,9 @@ public function render(): View 'feeds' => $feeds, 'providers' => $this->activeProviders(), 'languages' => Language::where('is_active', true)->orderBy('name')->get(), + 'editingFeed' => $this->editingFeedId !== null + ? Feed::with('language')->find($this->editingFeedId) + : null, ])->layout('layouts.app'); } } diff --git a/app/Models/Feed.php b/app/Models/Feed.php index f5f3d9be..026e04b6 100644 --- a/app/Models/Feed.php +++ b/app/Models/Feed.php @@ -20,7 +20,7 @@ * @property string $provider * @property int|null $language_id * @property Language|null $language - * @property string $description + * @property string|null $description * @property FeedColorEnum|null $color * @property array $settings * @property bool $is_active diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 8f8dd6ae..f74a15a0 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -18,12 +18,6 @@ parameters: count: 1 path: tests/Unit/Actions/CreateChannelActionTest.php - - - message: '#^Call to method PHPUnit\\Framework\\Assert\:\:assertNull\(\) with string will always evaluate to false\.$#' - identifier: method.impossibleType - count: 2 - path: tests/Unit/Actions/CreateFeedActionTest.php - - message: '#^Access to an undefined property App\\Models\\Route\:\:\$id\.$#' identifier: property.notFound diff --git a/resources/views/livewire/feeds.blade.php b/resources/views/livewire/feeds.blade.php index 98e75dd6..bb6f2f87 100644 --- a/resources/views/livewire/feeds.blade.php +++ b/resources/views/livewire/feeds.blade.php @@ -38,15 +38,28 @@ class="inline-flex items-center px-4 py-2 bg-blue-600 text-white text-sm font-me @endif - +
+ + + +
@@ -177,4 +190,67 @@ class="inline-flex justify-center rounded-md border border-transparent shadow-xs @endif + + + @if ($editingFeed) + +
+
+ + + @error('editName')

{{ $message }}

@enderror +
+ +
+ + + @error('editDescription')

{{ $message }}

@enderror +
+ +
+
+
+
Provider
+
{{ $providers[$editingFeed->provider]['name'] ?? $editingFeed->provider }}
+
+
+
Language
+
{{ $editingFeed->language?->name ?? '—' }}
+
+
+

+ Provider and language define the feed's source URL and cannot be changed. Create a new feed to use a different source. +

+
+ +
+ + +
+
+
+ @endif
diff --git a/tests/Feature/Livewire/FeedsTest.php b/tests/Feature/Livewire/FeedsTest.php index 5e518173..320844af 100644 --- a/tests/Feature/Livewire/FeedsTest.php +++ b/tests/Feature/Livewire/FeedsTest.php @@ -154,4 +154,134 @@ public function test_toggle_flips_active_state(): void $this->assertFalse($feed->fresh()->is_active); } + + public function test_feed_cards_show_an_edit_action(): void + { + Feed::factory()->create(['name' => 'VRT Nieuws']); + + Livewire::test(Feeds::class) + ->assertSee('Edit VRT Nieuws'); + } + + public function test_open_edit_modal_prefills_the_current_values(): void + { + $feed = Feed::factory()->create([ + 'name' => 'VRT Nieuws', + 'description' => 'Flemish public broadcaster', + ]); + + Livewire::test(Feeds::class) + ->assertSet('editingFeedId', null) + ->call('openEditModal', $feed->id) + ->assertSet('editingFeedId', $feed->id) + ->assertSet('editName', 'VRT Nieuws') + ->assertSet('editDescription', 'Flemish public broadcaster') + ->assertSee('Edit Feed'); + } + + public function test_open_edit_modal_prefills_a_null_description_as_blank(): void + { + $feed = Feed::factory()->create(['description' => null]); + + Livewire::test(Feeds::class) + ->call('openEditModal', $feed->id) + ->assertSet('editDescription', ''); + } + + public function test_update_feed_persists_the_changes_and_closes_the_modal(): void + { + $feed = Feed::factory()->create([ + 'name' => 'Old Name', + 'description' => 'Old description', + ]); + + Livewire::test(Feeds::class) + ->call('openEditModal', $feed->id) + ->set('editName', 'New Name') + ->set('editDescription', 'New description') + ->call('updateFeed') + ->assertSet('editingFeedId', null) + ->assertHasNoErrors(); + + $feed->refresh(); + + $this->assertSame('New Name', $feed->name); + $this->assertSame('New description', $feed->description); + } + + public function test_update_feed_requires_a_name(): void + { + $feed = Feed::factory()->create(['name' => 'Original']); + + Livewire::test(Feeds::class) + ->call('openEditModal', $feed->id) + ->set('editName', '') + ->call('updateFeed') + ->assertHasErrors(['editName' => 'required']); + + $this->assertSame('Original', $feed->fresh()->name); + } + + public function test_update_feed_rejects_a_name_over_the_length_limit(): void + { + $feed = Feed::factory()->create(['name' => 'Original']); + + Livewire::test(Feeds::class) + ->call('openEditModal', $feed->id) + ->set('editName', str_repeat('a', 256)) + ->call('updateFeed') + ->assertHasErrors(['editName' => 'max']); + + $this->assertSame('Original', $feed->fresh()->name); + } + + public function test_update_feed_stores_a_blank_description_as_null(): void + { + $feed = Feed::factory()->create(['description' => 'Has one']); + + Livewire::test(Feeds::class) + ->call('openEditModal', $feed->id) + ->set('editDescription', '') + ->call('updateFeed'); + + $this->assertNull($feed->fresh()->description); + } + + public function test_update_feed_leaves_the_source_identity_untouched(): void + { + $feed = Feed::factory()->create(['name' => 'Original']); + $before = $feed->only(['url', 'provider', 'language_id', 'type']); + + Livewire::test(Feeds::class) + ->call('openEditModal', $feed->id) + ->set('editName', 'Renamed') + ->call('updateFeed'); + + $this->assertSame($before, $feed->fresh()->only(['url', 'provider', 'language_id', 'type'])); + } + + public function test_close_edit_modal_discards_the_edit(): void + { + $feed = Feed::factory()->create(['name' => 'Original']); + + Livewire::test(Feeds::class) + ->call('openEditModal', $feed->id) + ->set('editName', 'Discarded') + ->call('closeEditModal') + ->assertSet('editingFeedId', null); + + $this->assertSame('Original', $feed->fresh()->name); + } + + public function test_update_feed_does_nothing_without_an_open_modal(): void + { + $feed = Feed::factory()->create(['name' => 'Original']); + + Livewire::test(Feeds::class) + ->set('editName', 'Should not apply') + ->call('updateFeed') + ->assertHasNoErrors(); + + $this->assertSame('Original', $feed->fresh()->name); + } }