Release v1.4.0 #146
3 changed files with 103 additions and 6 deletions
|
|
@ -21,6 +21,13 @@ public function mount(): void
|
||||||
$this->applyPreset('today');
|
$this->applyPreset('today');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Islands whose content depends on the global range; skipped islands never re-render on their own.
|
||||||
|
*
|
||||||
|
* @var array<int, string>
|
||||||
|
*/
|
||||||
|
private const RANGE_DEPENDENT_ISLANDS = ['article-statistics'];
|
||||||
|
|
||||||
public function applyPreset(string $preset): void
|
public function applyPreset(string $preset): void
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
|
|
@ -29,13 +36,29 @@ public function applyPreset(string $preset): void
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->from = $range->from->toDateString();
|
$this->applyRange($range->from->toDateString(), $range->to->toDateString());
|
||||||
$this->to = $range->to->toDateString();
|
}
|
||||||
|
|
||||||
|
public function applyRange(string $from, string $to): void
|
||||||
|
{
|
||||||
|
$this->from = $from;
|
||||||
|
$this->to = $to;
|
||||||
|
|
||||||
|
$this->validateRange();
|
||||||
|
$this->refreshRangeDependentIslands();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function refreshRangeDependentIslands(): void
|
||||||
|
{
|
||||||
|
foreach (self::RANGE_DEPENDENT_ISLANDS as $island) {
|
||||||
|
$this->renderIsland(name: $island);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array<string, string>
|
* @return array<string, string>
|
||||||
*/
|
*/
|
||||||
|
#[Computed]
|
||||||
public function presets(): array
|
public function presets(): array
|
||||||
{
|
{
|
||||||
return DateRange::presets();
|
return DateRange::presets();
|
||||||
|
|
@ -100,6 +123,7 @@ public function updated(string $property): void
|
||||||
{
|
{
|
||||||
if (in_array($property, ['from', 'to'], true)) {
|
if (in_array($property, ['from', 'to'], true)) {
|
||||||
$this->validateRange();
|
$this->validateRange();
|
||||||
|
$this->refreshRangeDependentIslands();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -127,8 +151,6 @@ private function validateRange(): void
|
||||||
|
|
||||||
public function render(): View
|
public function render(): View
|
||||||
{
|
{
|
||||||
return view('livewire.dashboard', [
|
return view('livewire.dashboard')->layout('layouts.app');
|
||||||
'presets' => $this->presets(),
|
|
||||||
])->layout('layouts.app');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
<x-page-header title="Dashboard" subtitle="Overview of your feed management system" />
|
<x-page-header title="Dashboard" subtitle="Overview of your feed management system" />
|
||||||
|
|
||||||
<!-- System Statistics -->
|
<!-- System Statistics -->
|
||||||
|
@island('system-overview')
|
||||||
<div class="mb-8">
|
<div class="mb-8">
|
||||||
<h2 class="text-lg font-semibold text-gray-900 mb-4 dark:text-gray-100">System Overview</h2>
|
<h2 class="text-lg font-semibold text-gray-900 mb-4 dark:text-gray-100">System Overview</h2>
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||||
|
|
@ -78,6 +79,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@endisland
|
||||||
|
|
||||||
<!-- Article Statistics -->
|
<!-- Article Statistics -->
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -86,7 +88,7 @@
|
||||||
|
|
||||||
<div class="flex flex-col gap-2 sm:flex-row sm:items-end">
|
<div class="flex flex-col gap-2 sm:flex-row sm:items-end">
|
||||||
<div class="flex flex-wrap gap-1">
|
<div class="flex flex-wrap gap-1">
|
||||||
@foreach ($presets as $value => $label)
|
@foreach ($this->presets as $value => $label)
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
wire:click="applyPreset('{{ $value }}')"
|
wire:click="applyPreset('{{ $value }}')"
|
||||||
|
|
@ -125,6 +127,7 @@ class="rounded-md border border-gray-300 text-sm focus:border-blue-500 focus:rin
|
||||||
<p class="mb-4 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
|
<p class="mb-4 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
|
||||||
@enderror
|
@enderror
|
||||||
|
|
||||||
|
@island('article-statistics')
|
||||||
<div @class(['grid grid-cols-1 md:grid-cols-3 gap-6', 'opacity-40' => ! $rangeIsValid])>
|
<div @class(['grid grid-cols-1 md:grid-cols-3 gap-6', 'opacity-40' => ! $rangeIsValid])>
|
||||||
<!-- Articles Fetched -->
|
<!-- Articles Fetched -->
|
||||||
<div class="bg-white p-6 rounded-lg shadow-sm dark:bg-gray-800">
|
<div class="bg-white p-6 rounded-lg shadow-sm dark:bg-gray-800">
|
||||||
|
|
@ -177,5 +180,6 @@ class="rounded-md border border-gray-300 text-sm focus:border-blue-500 focus:rin
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@endisland
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
use App\Models\Article;
|
use App\Models\Article;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
use Illuminate\Support\Carbon;
|
use Illuminate\Support\Carbon;
|
||||||
|
use Livewire\Features\SupportTesting\Testable;
|
||||||
use Livewire\Livewire;
|
use Livewire\Livewire;
|
||||||
use Tests\TestCase;
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
|
@ -119,4 +120,74 @@ public function test_it_renders_the_preset_buttons(): void
|
||||||
->assertSee('This Month')
|
->assertSee('This Month')
|
||||||
->assertSee('All Time');
|
->assertSee('All Time');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_it_renders_each_panel_in_its_own_island(): void
|
||||||
|
{
|
||||||
|
$html = Livewire::test(Dashboard::class)->html();
|
||||||
|
|
||||||
|
foreach (['system-overview', 'article-statistics'] as $island) {
|
||||||
|
$this->assertSame(
|
||||||
|
1,
|
||||||
|
substr_count($html, "[if FRAGMENT:type=island|name={$island}|"),
|
||||||
|
"Expected exactly one [{$island}] island.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Testable<Dashboard> $component
|
||||||
|
*/
|
||||||
|
private function islandFragments(Testable $component): string
|
||||||
|
{
|
||||||
|
/** @var array<int, string> $fragments */
|
||||||
|
$fragments = $component->effects['islandFragments'] ?? [];
|
||||||
|
|
||||||
|
return implode("\n", $fragments);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_it_re_renders_the_article_statistics_island_when_the_range_changes(): void
|
||||||
|
{
|
||||||
|
Article::factory()->count(3)->create(['created_at' => Carbon::parse('2026-07-10 12:00:00')]);
|
||||||
|
|
||||||
|
$fragments = $this->islandFragments(
|
||||||
|
Livewire::test(Dashboard::class)->call('applyRange', '2026-07-01', '2026-07-31')
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertMatchesRegularExpression('/Articles Fetched.*?>\s*3\s*</s', $fragments);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_it_reflects_a_narrowed_range_in_the_re_rendered_island(): void
|
||||||
|
{
|
||||||
|
Article::factory()->count(3)->create(['created_at' => Carbon::parse('2026-07-10 12:00:00')]);
|
||||||
|
|
||||||
|
$fragments = $this->islandFragments(
|
||||||
|
Livewire::test(Dashboard::class)->call('applyRange', '2026-08-01', '2026-08-31')
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertMatchesRegularExpression('/Articles Fetched.*?>\s*0\s*</s', $fragments);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_it_refreshes_only_the_range_dependent_island(): void
|
||||||
|
{
|
||||||
|
$fragments = $this->islandFragments(
|
||||||
|
Livewire::test(Dashboard::class)->call('applyRange', '2026-07-01', '2026-07-31')
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertStringContainsString('name=article-statistics|', $fragments);
|
||||||
|
$this->assertStringNotContainsString('name=system-overview|', $fragments);
|
||||||
|
$this->assertStringNotContainsString('Active Feeds', $fragments);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_it_re_renders_the_article_statistics_island_when_a_preset_is_applied(): void
|
||||||
|
{
|
||||||
|
Carbon::setTestNow('2026-07-15 13:45:00');
|
||||||
|
|
||||||
|
Article::factory()->count(2)->create(['created_at' => Carbon::parse('2026-07-15 09:00:00')]);
|
||||||
|
|
||||||
|
$fragments = $this->islandFragments(
|
||||||
|
Livewire::test(Dashboard::class)->call('applyPreset', 'month')
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertMatchesRegularExpression('/Articles Fetched.*?>\s*2\s*</s', $fragments);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue