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 260 additions and 16 deletions
Showing only changes of commit 41a1be7ca4 - Show all commits

View file

@ -21,6 +21,12 @@ class Feeds extends Component
public string $newDescription = ''; public string $newDescription = '';
public ?int $editingFeedId = null;
public string $editName = '';
public string $editDescription = '';
public function toggle(int $feedId): void public function toggle(int $feedId): void
{ {
$feed = Feed::findOrFail($feedId); $feed = Feed::findOrFail($feedId);
@ -67,6 +73,41 @@ public function createFeed(CreateFeedAction $action): void
$this->closeCreateModal(); $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<string, array<string, mixed>> * @return array<string, array<string, mixed>>
*/ */
@ -86,6 +127,9 @@ public function render(): View
'feeds' => $feeds, 'feeds' => $feeds,
'providers' => $this->activeProviders(), 'providers' => $this->activeProviders(),
'languages' => Language::where('is_active', true)->orderBy('name')->get(), 'languages' => Language::where('is_active', true)->orderBy('name')->get(),
'editingFeed' => $this->editingFeedId !== null
? Feed::with('language')->find($this->editingFeedId)
: null,
])->layout('layouts.app'); ])->layout('layouts.app');
} }
} }

View file

@ -20,7 +20,7 @@
* @property string $provider * @property string $provider
* @property int|null $language_id * @property int|null $language_id
* @property Language|null $language * @property Language|null $language
* @property string $description * @property string|null $description
* @property FeedColorEnum|null $color * @property FeedColorEnum|null $color
* @property array<string, mixed> $settings * @property array<string, mixed> $settings
* @property bool $is_active * @property bool $is_active

View file

@ -18,12 +18,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 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\.$#' message: '#^Access to an undefined property App\\Models\\Route\:\:\$id\.$#'
identifier: property.notFound identifier: property.notFound

View file

@ -38,6 +38,18 @@ class="inline-flex items-center px-4 py-2 bg-blue-600 text-white text-sm font-me
@endif @endif
</div> </div>
</div> </div>
<div class="flex shrink-0 items-center space-x-1">
<button
wire:click="openEditModal({{ $feed->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 feed"
aria-label="Edit {{ $feed->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({{ $feed->id }})" wire:click="toggle({{ $feed->id }})"
class="p-1 rounded-full {{ $feed->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 {{ $feed->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' }}"
@ -48,6 +60,7 @@ class="p-1 rounded-full {{ $feed->is_active ? 'bg-green-100 text-green-600 dark:
</svg> </svg>
</button> </button>
</div> </div>
</div>
<div class="mt-4 flex items-center justify-between"> <div class="mt-4 flex items-center justify-between">
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
@ -177,4 +190,67 @@ class="inline-flex justify-center rounded-md border border-transparent shadow-xs
</form> </form>
</x-form-modal> </x-form-modal>
@endif @endif
<!-- Edit Feed Modal -->
@if ($editingFeed)
<x-form-modal title="Edit Feed" close="closeEditModal">
<form wire:submit="updateFeed" class="space-y-4">
<div>
<label for="edit-feed-name" class="block text-sm font-medium text-gray-700 dark:text-gray-200">Name</label>
<input
type="text"
id="edit-feed-name"
wire:model="editName"
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('editName') <p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p> @enderror
</div>
<div>
<label for="edit-feed-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-feed-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">Provider</dt>
<dd class="font-medium text-gray-700 dark:text-gray-200">{{ $providers[$editingFeed->provider]['name'] ?? $editingFeed->provider }}</dd>
</div>
<div class="flex justify-between">
<dt class="text-gray-500 dark:text-gray-400">Language</dt>
<dd class="font-medium text-gray-700 dark:text-gray-200">{{ $editingFeed->language?->name ?? '—' }}</dd>
</div>
</dl>
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
Provider and language define the feed's source URL and cannot be changed. Create a new feed to use a different source.
</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="updateFeed"
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

@ -154,4 +154,134 @@ public function test_toggle_flips_active_state(): void
$this->assertFalse($feed->fresh()->is_active); $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);
}
} }