diff --git a/packages/tables/src/Concerns/HasBulkActions.php b/packages/tables/src/Concerns/HasBulkActions.php index 82b0a4ca3a..4e84ce53a3 100644 --- a/packages/tables/src/Concerns/HasBulkActions.php +++ b/packages/tables/src/Concerns/HasBulkActions.php @@ -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()) { diff --git a/packages/tables/src/Grouping/Group.php b/packages/tables/src/Grouping/Group.php index a5e961f46c..3b11ae3f2d 100644 --- a/packages/tables/src/Grouping/Group.php +++ b/packages/tables/src/Grouping/Group.php @@ -192,7 +192,10 @@ class Group extends Component ->ucfirst(); } - public function getDescription(Model $record, string | Htmlable | null $title): string | Htmlable | null + /** + * @param Model | array $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 $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 $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 $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 $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; } diff --git a/tests/src/Fixtures/Livewire/GroupedCustomDataTable.php b/tests/src/Fixtures/Livewire/GroupedCustomDataTable.php new file mode 100644 index 0000000000..63eac0dfdb --- /dev/null +++ b/tests/src/Fixtures/Livewire/GroupedCustomDataTable.php @@ -0,0 +1,64 @@ +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'); + } +} diff --git a/tests/src/Tables/GroupingTest.php b/tests/src/Tables/GroupingTest.php index d542923b3c..815d4d998c 100644 --- a/tests/src/Tables/GroupingTest.php +++ b/tests/src/Tables/GroupingTest.php @@ -1,6 +1,7 @@ `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(); + }); +});