Release v1.4.0 #146
5 changed files with 260 additions and 16 deletions
|
|
@ -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');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -38,15 +38,28 @@ class="inline-flex items-center px-4 py-2 bg-blue-600 text-white text-sm font-me
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<div class="flex shrink-0 items-center space-x-1">
|
||||||
wire:click="toggle({{ $feed->id }})"
|
<button
|
||||||
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' }}"
|
wire:click="openEditModal({{ $feed->id }})"
|
||||||
title="{{ $feed->is_active ? 'Active - Click to disable' : 'Inactive - Click to enable' }}"
|
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"
|
||||||
<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 20 20">
|
aria-label="Edit {{ $feed->name }}"
|
||||||
<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>
|
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||||
</button>
|
<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({{ $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' }}"
|
||||||
|
title="{{ $feed->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>
|
</div>
|
||||||
|
|
||||||
<div class="mt-4 flex items-center justify-between">
|
<div class="mt-4 flex items-center justify-between">
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue