[4.x] Fix last group summary (#19593)

* Fix last group summary

* fix cursor pagination and add negative test

* Update PostsTableWithCursorPagination.php

* Update SummaryTest.php

---------

Co-authored-by: Dan Harrin <git@danharrin.com>
This commit is contained in:
Alexander Jensen
2026-04-03 15:56:59 +01:00
committed by GitHub
co-authored by Dan Harrin
parent bbe9ad4dbb
commit dd580292da
4 changed files with 142 additions and 3 deletions
@@ -1301,7 +1301,7 @@
@endphp
@endforeach
@if ($hasSummary && (! $isReordering) && filled($previousRecordGroupTitle) && ((! $records instanceof \Illuminate\Contracts\Pagination\Paginator) || (! $records->hasMorePages())))
@if ($hasSummary && (! $isReordering) && filled($previousRecordGroupTitle) && $this->shouldRenderTrailingGroupedTableSummary($previousRecord))
<table class="fi-ta-table">
<tbody>
@php
@@ -2353,7 +2353,7 @@
@endphp
@endforeach
@if ($hasSummary && (! $isReordering) && filled($previousRecordGroupTitle) && ((! $records instanceof \Illuminate\Contracts\Pagination\Paginator) || (! $records->hasMorePages())))
@if ($hasSummary && (! $isReordering) && filled($previousRecordGroupTitle) && $this->shouldRenderTrailingGroupedTableSummary($previousRecord))
@php
$groupColumn = $group->getColumn();
$groupScopedAllTableSummaryQuery = $group->scopeQuery($this->getAllTableSummaryQuery(), $previousRecord);
@@ -4,13 +4,68 @@ namespace Filament\Tables\Concerns;
use Closure;
use Filament\Support\Services\RelationshipJoiner;
use Illuminate\Contracts\Pagination\CursorPaginator;
use Illuminate\Contracts\Pagination\Paginator;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Query\Expression;
use Illuminate\Support\Str;
use stdClass;
trait CanSummarizeRecords
{
/**
* @param Model | array<string, mixed> | null $lastRecord
*/
public function shouldRenderTrailingGroupedTableSummary(Model | array | null $lastRecord): bool
{
if ($lastRecord === null) {
return false;
}
$records = $this->getTableRecords();
$isPaginated = ($records instanceof Paginator) || ($records instanceof CursorPaginator);
if ((! $isPaginated) || (! $records->hasMorePages())) {
return true;
}
$group = $this->getTableGrouping();
if (! $group) {
return true;
}
$query = $this->getFilteredSortedTableQuery();
if ($query === null) {
return true;
}
if ($records instanceof CursorPaginator) {
$nextCursor = $records->nextCursor();
if (! $nextCursor) {
return true;
}
$nextPageFirstRecord = (clone $query)
->cursorPaginate(perPage: 1, cursor: $nextCursor)
->items()[0] ?? null;
} else {
$nextPageFirstRecord = (clone $query)
->skip($records->currentPage() * $records->perPage())
->first();
}
if ($nextPageFirstRecord === null) {
return true;
}
return $group->getStringKey($nextPageFirstRecord) !== $group->getStringKey($lastRecord);
}
public function getAllTableSummaryQuery(): ?Builder
{
return $this->getFilteredTableQuery();
@@ -0,0 +1,44 @@
<?php
namespace Filament\Tests\Fixtures\Livewire;
use Filament\Actions\Concerns\InteractsWithActions;
use Filament\Actions\Contracts\HasActions;
use Filament\Schemas\Concerns\InteractsWithSchemas;
use Filament\Schemas\Contracts\HasSchemas;
use Filament\Tables;
use Filament\Tables\Enums\PaginationMode;
use Filament\Tables\Table;
use Filament\Tests\Fixtures\Models\Post;
use Illuminate\Contracts\View\View;
use Livewire\Component;
class PostsTableWithCursorPagination extends Component implements HasActions, HasSchemas, Tables\Contracts\HasTable
{
use InteractsWithActions;
use InteractsWithSchemas;
use Tables\Concerns\InteractsWithTable;
public function table(Table $table): Table
{
return $table
->query(Post::query())
->paginationMode(PaginationMode::Cursor)
->groups(fn () => [
Tables\Grouping\Group::make('title'),
])
->columns([
Tables\Columns\TextColumn::make('title')
->sortable(),
Tables\Columns\TextColumn::make('rating')
->summarize([
Tables\Columns\Summarizers\Sum::make('sum'),
]),
]);
}
public function render(): View
{
return view('livewire.table');
}
}
+41 -1
View File
@@ -7,10 +7,10 @@ use Filament\Schemas\Contracts\HasSchemas;
use Filament\Tables;
use Filament\Tables\Table;
use Filament\Tests\Fixtures\Livewire\PostsTable;
use Filament\Tests\Fixtures\Livewire\PostsTableWithCursorPagination;
use Filament\Tests\Fixtures\Models\Post;
use Filament\Tests\Tables\TestCase;
use Illuminate\Contracts\View\View;
use Illuminate\Database\Query\Builder;
use Livewire\Component;
use function Filament\Tests\livewire;
@@ -169,6 +169,46 @@ it('renders group summaries when page and all-table summaries are disabled', fun
->assertDontSeeHtml('fi-ta-summary-header-row');
});
it('renders the trailing group summary when the next page starts a different group', function (): void {
Post::factory()->count(10)->create(['title' => 'A']);
Post::factory()->create(['title' => 'B']);
livewire(PostsTable::class)
->set('tableRecordsPerPage', 10)
->set('tableGrouping', 'title')
->assertSee('A summary')
->assertDontSee('B summary');
});
it('does not render the trailing group summary when the next page continues the same group', function (): void {
Post::factory()->count(15)->create(['title' => 'A']);
livewire(PostsTable::class)
->set('tableRecordsPerPage', 10)
->set('tableGrouping', 'title')
->assertDontSee('A summary');
});
it('renders the trailing group summary with cursor pagination when the next page starts a different group', function (): void {
Post::factory()->count(10)->create(['title' => 'A']);
Post::factory()->create(['title' => 'B']);
livewire(PostsTableWithCursorPagination::class)
->set('tableRecordsPerPage', 10)
->set('tableGrouping', 'title')
->assertSee('A summary')
->assertDontSee('B summary');
});
it('does not render the trailing group summary with cursor pagination when the next page continues the same group', function (): void {
Post::factory()->count(15)->create(['title' => 'A']);
livewire(PostsTableWithCursorPagination::class)
->set('tableRecordsPerPage', 10)
->set('tableGrouping', 'title')
->assertDontSee('A summary');
});
class TestTableWithGroupSummariesOnly extends Component implements HasActions, HasSchemas, Tables\Contracts\HasTable
{
use InteractsWithActions;