[4.x] Add array record support for table grouping (#19253)

* Support array groups

* Support array groups in bulk actions

---------

Co-authored-by: Dan Harrin <git@danharrin.com>
This commit is contained in:
Alexander Jensen
2026-02-12 18:37:15 +00:00
committed by GitHub
co-authored by Dan Harrin
parent 8beb182ee9
commit a6d0236e21
4 changed files with 222 additions and 15 deletions
@@ -135,10 +135,44 @@ trait HasBulkActions
*/
public function getGroupedSelectableTableRecordKeys(?string $group): array
{
$query = $this->getFilteredTableQuery();
$tableGrouping = $this->getTableGrouping();
if (! $this->getTable()->hasQuery()) {
$groupColumn = $tableGrouping->getColumn();
$records = $this->getTableRecords()
->filter(static function (array $record) use ($groupColumn, $group): bool {
$key = $record[$groupColumn] ?? null;
$stringKey = filled($key) ? strval($key) : null;
return $stringKey === $group;
});
if (! $this->getTable()->checksIfRecordIsSelectable()) {
/** @phpstan-ignore-next-line */
return $records
->map(fn (array $record): string => $this->getTableRecordKey($record))
->values()
->all();
}
/** @phpstan-ignore-next-line */
return $records->reduce(
function (array $carry, array $record): array {
if (! $this->getTable()->isRecordSelectable($record)) {
return $carry;
}
$carry[] = $this->getTableRecordKey($record);
return $carry;
},
initial: [],
);
}
$query = $this->getFilteredTableQuery();
$tableGrouping->scopeQueryByKey($query, $group);
if (! $this->getTable()->checksIfRecordIsSelectable()) {
+28 -13
View File
@@ -192,7 +192,10 @@ class Group extends Component
->ucfirst();
}
public function getDescription(Model $record, string | Htmlable | null $title): string | Htmlable | null
/**
* @param Model | array<string, mixed> $record
*/
public function getDescription(Model | array $record, string | Htmlable | null $title): string | Htmlable | null
{
if (! $this->getDescriptionFromRecordUsing) {
return null;
@@ -204,14 +207,17 @@ class Group extends Component
'record' => $record,
'title' => $title,
],
typedInjections: [
typedInjections: ($record instanceof Model) ? [
Model::class => $record,
$record::class => $record,
],
] : [],
);
}
public function getStringKey(Model $record): ?string
/**
* @param Model | array<string, mixed> $record
*/
public function getStringKey(Model | array $record): ?string
{
$key = $this->getKey($record);
@@ -230,7 +236,10 @@ class Group extends Component
return filled($key) ? strval($key) : null;
}
public function getKey(Model $record): mixed
/**
* @param Model | array<string, mixed> $record
*/
public function getKey(Model | array $record): mixed
{
$column = $this->getColumn();
@@ -241,17 +250,20 @@ class Group extends Component
'column' => $column,
'record' => $record,
],
typedInjections: [
typedInjections: ($record instanceof Model) ? [
Model::class => $record,
$record::class => $record,
],
] : [],
);
}
return Arr::get($record, $this->getColumn());
}
public function getTitle(Model $record): string | Htmlable | null
/**
* @param Model | array<string, mixed> $record
*/
public function getTitle(Model | array $record): string | Htmlable | null
{
$column = $this->getColumn();
@@ -262,10 +274,10 @@ class Group extends Component
'column' => $column,
'record' => $record,
],
typedInjections: [
typedInjections: ($record instanceof Model) ? [
Model::class => $record,
$record::class => $record,
],
] : [],
);
} else {
$title = Arr::get($record, $column);
@@ -332,7 +344,10 @@ class Group extends Component
return $query->orderBy($this->getRelationshipAttribute(), $direction);
}
public function scopeQuery(EloquentBuilder $query, Model $record): EloquentBuilder
/**
* @param Model | array<string, mixed> $record
*/
public function scopeQuery(EloquentBuilder $query, Model | array $record): EloquentBuilder
{
if ($this->scopeQueryUsing) {
return $this->evaluate(
@@ -342,10 +357,10 @@ class Group extends Component
'query' => $query,
'record' => $record,
],
typedInjections: [
typedInjections: ($record instanceof Model) ? [
Model::class => $record,
$record::class => $record,
],
] : [],
) ?? $query;
}
@@ -0,0 +1,64 @@
<?php
namespace Filament\Tests\Fixtures\Livewire;
use Filament\Actions\BulkAction;
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\Columns\TextColumn;
use Filament\Tables\Table;
use Illuminate\Contracts\View\View;
use Livewire\Component;
class GroupedCustomDataTable extends Component implements HasActions, HasSchemas, Tables\Contracts\HasTable
{
use InteractsWithActions;
use InteractsWithSchemas;
use Tables\Concerns\InteractsWithTable;
public function table(Table $table): Table
{
return $table
->records(fn (): array => [
1 => [
'title' => 'First item',
'status' => 'active',
],
2 => [
'title' => 'Second item',
'status' => 'inactive',
],
3 => [
'title' => 'Third item',
'status' => 'active',
],
4 => [
'title' => 'Fourth item',
'status' => 'inactive',
],
5 => [
'title' => 'Fifth item',
'status' => 'active',
],
])
->columns([
TextColumn::make('title'),
TextColumn::make('status'),
])
->groups([
Tables\Grouping\Group::make('status'),
])
->toolbarActions([
BulkAction::make('delete')
->action(fn () => null),
]);
}
public function render(): View
{
return view('livewire.table');
}
}
+94
View File
@@ -1,6 +1,7 @@
<?php
use Filament\Tables;
use Filament\Tests\Fixtures\Livewire\GroupedCustomDataTable;
use Filament\Tests\Fixtures\Livewire\PostsTable;
use Filament\Tests\Fixtures\Livewire\UsersTable;
use Filament\Tests\Fixtures\Models\Company;
@@ -502,3 +503,96 @@ it('can group records with nullable `BelongsTo` -> `HasOneThrough` relationship'
->set('tableGrouping', 'author.setting.theme')
->assertCanSeeTableRecords($allPosts);
});
it('can handle array records in `getKey()`', function (): void {
$livewire = livewire(PostsTable::class)->instance();
$group = \Filament\Tables\Grouping\Group::make('status')->table($livewire->getTable());
$arrayRecord = ['__key' => '1', 'name' => 'John', 'status' => 'active'];
expect($group->getKey($arrayRecord))->toBe('active');
});
it('can handle array records in `getStringKey()`', function (): void {
$livewire = livewire(PostsTable::class)->instance();
$group = \Filament\Tables\Grouping\Group::make('status')->table($livewire->getTable());
$arrayRecord = ['__key' => '1', 'name' => 'John', 'status' => 'active'];
expect($group->getStringKey($arrayRecord))->toBe('active');
});
it('can handle array records in `getTitle()`', function (): void {
$livewire = livewire(PostsTable::class)->instance();
$group = \Filament\Tables\Grouping\Group::make('status')->table($livewire->getTable());
$arrayRecord = ['__key' => '1', 'name' => 'John', 'status' => 'active'];
expect($group->getTitle($arrayRecord))->toBe('active');
});
it('can handle array records in `getDescription()`', function (): void {
$livewire = livewire(PostsTable::class)->instance();
$group = \Filament\Tables\Grouping\Group::make('status')
->getDescriptionFromRecordUsing(fn (array $record): string => 'User: ' . $record['name'])
->table($livewire->getTable());
$arrayRecord = ['__key' => '1', 'name' => 'John', 'status' => 'active'];
expect($group->getDescription($arrayRecord, 'Active'))->toBe('User: John');
});
it('can use custom `getKeyFromRecordUsing()` with array records', function (): void {
$livewire = livewire(PostsTable::class)->instance();
$group = \Filament\Tables\Grouping\Group::make('status')
->getKeyFromRecordUsing(fn (array $record): string => strtoupper($record['status']))
->table($livewire->getTable());
$arrayRecord = ['__key' => '1', 'name' => 'John', 'status' => 'active'];
expect($group->getKey($arrayRecord))->toBe('ACTIVE')
->and($group->getStringKey($arrayRecord))->toBe('ACTIVE');
});
it('can use custom `getTitleFromRecordUsing()` with array records', function (): void {
$livewire = livewire(PostsTable::class)->instance();
$group = \Filament\Tables\Grouping\Group::make('status')
->getTitleFromRecordUsing(fn (array $record): string => 'Status: ' . ucfirst($record['status']))
->table($livewire->getTable());
$arrayRecord = ['__key' => '1', 'name' => 'John', 'status' => 'active'];
expect($group->getTitle($arrayRecord))->toBe('Status: Active');
});
it('can get grouped selectable record keys for array tables', function (): void {
livewire(GroupedCustomDataTable::class)
->set('tableGrouping', 'status')
->tap(function (Testable $testable): void {
/** @var GroupedCustomDataTable $livewire */
$livewire = $testable->instance();
$activeKeys = $livewire->getGroupedSelectableTableRecordKeys('active');
$inactiveKeys = $livewire->getGroupedSelectableTableRecordKeys('inactive');
expect($activeKeys)
->toHaveCount(3)
->each->toBeString()
->and($inactiveKeys)
->toHaveCount(2)
->each->toBeString();
});
});
it('returns an empty array for a non-existent group in array tables', function (): void {
livewire(GroupedCustomDataTable::class)
->set('tableGrouping', 'status')
->tap(function (Testable $testable): void {
/** @var GroupedCustomDataTable $livewire */
$livewire = $testable->instance();
$keys = $livewire->getGroupedSelectableTableRecordKeys('nonexistent');
expect($keys)->toBeEmpty();
});
});