feat(tables): persist grouping in the user's session (#20165)

* feat(tables): persist grouping in the user's session

Add a `persistGroupInSession()` method to the table configuration,
mirroring the existing `persistSortInSession()` behavior, so a user's
chosen grouping is restored on their next visit.

The Livewire `updatedTableGroupColumn()` hook never fired because the
underlying property is `$tableGrouping` (Livewire resolves the hook as
`updatedTableGrouping()`). Renaming it fixes that dormant hook, which is
what both resets the page and now writes the grouping to the session.

* fix(tables): align grouping session persistence

---------

Co-authored-by: Dan Harrin <git@danharrin.com>
This commit is contained in:
IGedeon
2026-09-04 09:27:10 +01:00
committed by GitHub
co-authored by Dan Harrin
parent 6c1217745d
commit d8ef648e60
6 changed files with 204 additions and 3 deletions
+18
View File
@@ -381,3 +381,21 @@ public function table(Table $table): Table
->groupingDirectionSettingHidden();
}
```
## Persisting the grouping in the user's session
To persist the grouping in the user's session, use the `persistGroupInSession()` method:
```php
use Filament\Tables\Table;
public function table(Table $table): Table
{
return $table
->groups([
'status',
'category',
])
->persistGroupInSession();
}
```
@@ -29,9 +29,24 @@ trait CanGroupRecords
return $this->getTable()->getDefaultGroup();
}
public function updatedTableGrouping(): void
{
if ($this->getTable()->persistsGroupInSession()) {
session()->put(
$this->getTableGroupingSessionKey(),
$this->tableGrouping,
);
}
$this->resetPage();
}
/**
* @deprecated Use `updatedTableGrouping()` instead.
*/
public function updatedTableGroupColumn(): void
{
$this->resetPage();
$this->updatedTableGrouping();
}
public function getTableGroupingDirection(): ?string
@@ -65,4 +80,11 @@ trait CanGroupRecords
return $query;
}
public function getTableGroupingSessionKey(): string
{
$table = md5($this::class);
return "tables.{$table}_grouping";
}
}
@@ -91,8 +91,24 @@ trait InteractsWithTable
);
}
if ($this->getTable()->isDefaultGroupSelectable()) {
$this->tableGrouping = "{$this->getTable()->getDefaultGroup()->getId()}:asc";
$shouldPersistGroupInSession = $this->getTable()->persistsGroupInSession();
$groupingSessionKey = $this->getTableGroupingSessionKey();
$hasPersistedGroupInSession = $shouldPersistGroupInSession && session()->exists($groupingSessionKey);
if (blank($this->tableGrouping)) {
if ($hasPersistedGroupInSession) {
$sessionGrouping = session()->get($groupingSessionKey);
$this->tableGrouping = is_string($sessionGrouping) ? $sessionGrouping : null;
} elseif ($this->getTable()->isDefaultGroupSelectable()) {
$this->tableGrouping = "{$this->getTable()->getDefaultGroup()->getId()}:asc";
}
}
if ($shouldPersistGroupInSession) {
session()->put(
$groupingSessionKey,
$this->tableGrouping,
);
}
$shouldPersistSearchInSession = $this->getTable()->persistsSearchInSession();
@@ -25,6 +25,8 @@ trait CanGroupRecords
protected bool | Closure $isGroupsOnly = false;
protected bool | Closure | null $persistsGroupInSession = false;
protected bool | Closure $areGroupingSettingsInDropdownOnDesktop = false;
protected bool | Closure $areGroupingSettingsHidden = false;
@@ -104,6 +106,13 @@ trait CanGroupRecords
return $this;
}
public function persistGroupInSession(bool | Closure $condition = true): static
{
$this->persistsGroupInSession = $condition;
return $this;
}
public function getGroupRecordsTriggerAction(): Action
{
$action = Action::make('groupRecords')
@@ -213,4 +222,9 @@ trait CanGroupRecords
{
return (bool) $this->evaluate($this->isGroupsOnly);
}
public function persistsGroupInSession(): bool
{
return (bool) $this->evaluate($this->persistsGroupInSession);
}
}
@@ -0,0 +1,45 @@
<?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\Table;
use Filament\Tests\Fixtures\Models\Post;
use Illuminate\Contracts\View\View;
use Livewire\Attributes\Url;
use Livewire\Component;
class PostsTableWithGroupPersistedInSession extends Component implements HasActions, HasSchemas, Tables\Contracts\HasTable
{
use InteractsWithActions;
use InteractsWithSchemas;
use Tables\Concerns\InteractsWithTable;
#[Url(as: 'grouping')]
public ?string $tableGrouping = null;
public function table(Table $table): Table
{
return $table
->query(Post::query())
->groups(static fn () => [
Tables\Grouping\Group::make('title'),
Tables\Grouping\Group::make('author.name'),
])
->defaultGroup('title')
->persistGroupInSession()
->columns([
Tables\Columns\TextColumn::make('title')->sortable(),
Tables\Columns\TextColumn::make('author.name')->sortable(),
]);
}
public function render(): View
{
return view('livewire.table');
}
}
+86
View File
@@ -3,6 +3,7 @@
use Filament\Tables\Grouping\Group;
use Filament\Tests\Fixtures\Livewire\GroupedCustomDataTable;
use Filament\Tests\Fixtures\Livewire\PostsTable;
use Filament\Tests\Fixtures\Livewire\PostsTableWithGroupPersistedInSession;
use Filament\Tests\Fixtures\Livewire\PostsTableWithoutSummarizers;
use Filament\Tests\Fixtures\Livewire\TicketMessagesTable;
use Filament\Tests\Fixtures\Livewire\UsersTable;
@@ -18,6 +19,7 @@ use Filament\Tests\Fixtures\Models\TicketMessage;
use Filament\Tests\Fixtures\Models\User;
use Filament\Tests\Tables\TestCase;
use Livewire\Features\SupportTesting\Testable;
use Livewire\Livewire;
use function Filament\Tests\livewire;
@@ -961,3 +963,87 @@ it('can group records by `HasOneThrough` -> `BelongsTo` relationship that uses `
->set('tableGrouping', 'setting.languageWithTrashed.name')
->assertCanSeeTableRecords($sortedUsers, inOrder: true);
});
it('resets pagination when `$tableGrouping` is updated', function (): void {
Post::factory()->count(20)->create();
livewire(PostsTable::class)
->call('setPage', 2)
->assertSet('paginators.page', 2)
->set('tableGrouping', 'title')
->assertSet('paginators.page', 1);
});
describe('session persistence', function (): void {
it('defaults `persistsGroupInSession()` to `false`', function (): void {
livewire(PostsTable::class)
->tap(function (Testable $testable): void {
/** @var PostsTable $livewire */
$livewire = $testable->instance();
expect($livewire->getTable()->persistsGroupInSession())->toBeFalse();
});
});
it('can enable grouping persistence with `persistGroupInSession()`', function (): void {
livewire(PostsTableWithGroupPersistedInSession::class)
->tap(function (Testable $testable): void {
/** @var PostsTableWithGroupPersistedInSession $livewire */
$livewire = $testable->instance();
expect($livewire->getTable()->persistsGroupInSession())->toBeTrue();
});
});
it('scopes `getTableGroupingSessionKey()` to the component class', function (): void {
/** @var PostsTableWithGroupPersistedInSession $livewire */
$livewire = livewire(PostsTableWithGroupPersistedInSession::class)->instance();
expect($livewire->getTableGroupingSessionKey())
->toBe('tables.' . md5($livewire::class) . '_grouping');
});
it('can persist the grouping in the user\'s session', function (): void {
Post::factory()->count(10)->create();
livewire(PostsTableWithGroupPersistedInSession::class)
->assertSet('tableGrouping', 'title:asc')
->set('tableGrouping', 'author.name:desc')
->assertSet('tableGrouping', 'author.name:desc');
livewire(PostsTableWithGroupPersistedInSession::class)
->assertSet('tableGrouping', 'author.name:desc');
});
it('can clear the persisted grouping in the user\'s session', function (): void {
Post::factory()->count(10)->create();
livewire(PostsTableWithGroupPersistedInSession::class)
->assertSet('tableGrouping', 'title:asc')
->set('tableGrouping', null)
->assertSet('tableGrouping', null);
livewire(PostsTableWithGroupPersistedInSession::class)
->assertSet('tableGrouping', null);
});
it('does not override explicit `$tableGrouping` with the persisted grouping', function (): void {
livewire(PostsTableWithGroupPersistedInSession::class)
->assertSet('tableGrouping', 'title:asc');
Livewire::withQueryParams(['grouping' => 'author.name:desc'])
->test(PostsTableWithGroupPersistedInSession::class)
->assertSet('tableGrouping', 'author.name:desc');
});
it('does not persist the grouping in the user\'s session by default', function (): void {
Post::factory()->count(10)->create();
livewire(PostsTable::class)
->set('tableGrouping', 'title')
->assertSet('tableGrouping', 'title');
livewire(PostsTable::class)
->assertSet('tableGrouping', null);
});
});