Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 60 additions & 30 deletions packages/tables/src/Filters/SelectFilter.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ class SelectFilter extends BaseFilter

protected ?Closure $getOptionLabelFromRecordUsing = null;

protected const EMPTY_RELATIONSHIP_OPTION_KEY = '__empty';

protected function setUp(): void
{
parent::setUp();
Expand All @@ -64,7 +66,7 @@ protected function setUp(): void

if (
$filter->hasEmptyRelationshipOption() &&
in_array('__empty', $state['values'])
in_array(static::EMPTY_RELATIONSHIP_OPTION_KEY, $state['values'])
) {
$labels[] = $filter->getEmptyRelationshipOptionLabel();
}
Expand All @@ -78,7 +80,7 @@ protected function setUp(): void
)
->when(
$filter->getRelationshipKey(),
fn (Builder $query, string $relationshipKey) => $query->whereIn($relationshipKey, $state['values']),
fn (Builder $query, string $relationshipKey) => $query->whereIn($relationshipKey, $filter->getRelationshipQueryValues($state['values'])),
fn (Builder $query) => $query->whereKey($state['values'])
)
->pluck($relationshipQuery->qualifyColumn($filter->getRelationshipTitleAttribute()))
Expand Down Expand Up @@ -113,7 +115,7 @@ protected function setUp(): void
if ($filter->queriesRelationships()) {
if (
$filter->hasEmptyRelationshipOption() &&
($state['value'] === '__empty')
($state['value'] === static::EMPTY_RELATIONSHIP_OPTION_KEY)
) {
$label = $filter->getEmptyRelationshipOptionLabel();
} else {
Expand Down Expand Up @@ -192,33 +194,49 @@ public function apply(Builder $query, array $data = []): Builder
);
}

$applyRelationshipScope = fn (Builder $query) => $query->whereHas(
$this->getRelationshipName(),
function (Builder $query) use ($isMultiple, $values) {
if ($this->modifyRelationshipQueryUsing) {
$query = $this->evaluate($this->modifyRelationshipQueryUsing, [
'query' => $query,
]) ?? $query;
}
$filteredValues = $this->getRelationshipQueryValues($values);

if ($relationshipKey = $this->getRelationshipKey($query)) {
return $query->{$isMultiple ? 'whereIn' : 'where'}(
$relationshipKey,
$values,
);
}
$applyRelationshipScope = function (Builder $query) use ($isMultiple, $filteredValues): void {
if (empty($filteredValues)) {
return;
}

return $query->whereKey($values);
},
);
$query->whereHas(
$this->getRelationshipName(),
function (Builder $query) use ($isMultiple, $filteredValues): void {
if ($this->modifyRelationshipQueryUsing) {
$query = $this->evaluate($this->modifyRelationshipQueryUsing, [
'query' => $query,
]) ?? $query;
}

$queryValues = $isMultiple ? $filteredValues : $filteredValues[0];

if ($relationshipKey = $this->getRelationshipKey($query)) {
$query->{$isMultiple ? 'whereIn' : 'where'}(
$relationshipKey,
$queryValues,
);

return;
}

$query->whereKey($queryValues);
},
);
};

if (
$this->hasEmptyRelationshipOption() &&
in_array('__empty', Arr::wrap($values))
in_array(static::EMPTY_RELATIONSHIP_OPTION_KEY, Arr::wrap($values))
) {
$query->where(fn (Builder $query) => $query
->where(fn (Builder $query) => $applyRelationshipScope($query))
->orWhereDoesntHave($this->getRelationshipName()));
if (filled($filteredValues)) {
$query
->where(fn (Builder $query) => $applyRelationshipScope($query))
->orWhereDoesntHave($this->getRelationshipName());
} else {
$query->whereDoesntHave($this->getRelationshipName());
}
} else {
$applyRelationshipScope($query);
}
Expand Down Expand Up @@ -350,7 +368,7 @@ public function getFormField(): Select
$this->hasEmptyRelationshipOption() &&
str($this->getEmptyRelationshipOptionLabel())->lower()->contains(Str::lower($search))
) {
$options['__empty'] = $this->getEmptyRelationshipOptionLabel();
$options[static::EMPTY_RELATIONSHIP_OPTION_KEY] = $this->getEmptyRelationshipOptionLabel();
}

$qualifiedRelatedKeyName = $component->getQualifiedRelatedKeyNameForRelationship($relationship);
Expand Down Expand Up @@ -416,7 +434,7 @@ public function getFormField(): Select
$options = [];

if ($this->hasEmptyRelationshipOption()) {
$options['__empty'] = $this->getEmptyRelationshipOptionLabel();
$options[static::EMPTY_RELATIONSHIP_OPTION_KEY] = $this->getEmptyRelationshipOptionLabel();
}

$qualifiedRelatedKeyName = $component->getQualifiedRelatedKeyNameForRelationship($relationship);
Expand Down Expand Up @@ -457,7 +475,7 @@ public function getFormField(): Select
->getOptionLabelUsing(function (Select $component) {
if (
$this->hasEmptyRelationshipOption() &&
($component->getState() === '__empty')
($component->getState() === static::EMPTY_RELATIONSHIP_OPTION_KEY)
) {
return $this->getEmptyRelationshipOptionLabel();
}
Expand Down Expand Up @@ -487,7 +505,7 @@ public function getFormField(): Select

$qualifiedRelatedKeyName = $component->getQualifiedRelatedKeyNameForRelationship($relationship);

$relationshipQuery->whereIn($qualifiedRelatedKeyName, $values);
$relationshipQuery->whereIn($qualifiedRelatedKeyName, $this->getRelationshipQueryValues($values));

if ($this->modifyRelationshipQueryUsing) {
$relationshipQuery = $component->evaluate($this->modifyRelationshipQueryUsing, [
Expand All @@ -500,9 +518,9 @@ public function getFormField(): Select

if (
$this->hasEmptyRelationshipOption() &&
in_array('__empty', $values)
in_array(static::EMPTY_RELATIONSHIP_OPTION_KEY, $values)
) {
$labels['__empty'] = $this->getEmptyRelationshipOptionLabel();
$labels[static::EMPTY_RELATIONSHIP_OPTION_KEY] = $this->getEmptyRelationshipOptionLabel();
}

if ($component->hasOptionLabelFromRecordUsingCallback()) {
Expand Down Expand Up @@ -604,4 +622,16 @@ public function getOptionLabelFromRecordUsing(?Closure $callback): static

return $this;
}

/**
* @param string | array<string> $values
* @return array<string>
*/
protected function getRelationshipQueryValues(string | array $values): array
{
return array_values(array_filter(
Arr::wrap($values),
fn (string $value): bool => $value !== static::EMPTY_RELATIONSHIP_OPTION_KEY,
));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?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\Component;

class PostsTableWithEmptyRelationshipFilter 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())
->columns([
Tables\Columns\TextColumn::make('title'),
Tables\Columns\TextColumn::make('author.name'),
])
->filters([
Tables\Filters\SelectFilter::make('author')
->relationship('author', 'name', hasEmptyOption: true),
]);
}

public function render(): View
{
return view('livewire.table');
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?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\Component;

class PostsTableWithMultipleEmptyRelationshipFilter 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())
->columns([
Tables\Columns\TextColumn::make('title'),
Tables\Columns\TextColumn::make('author.name'),
])
->filters([
Tables\Filters\SelectFilter::make('author')
->relationship('author', 'name', hasEmptyOption: true)
->multiple(),
]);
}

public function render(): View
{
return view('livewire.table');
}
}
86 changes: 86 additions & 0 deletions tests/src/Tables/Filters/SelectFilterTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<?php

use Filament\Tests\Fixtures\Livewire\PostsTableWithEmptyRelationshipFilter;
use Filament\Tests\Fixtures\Livewire\PostsTableWithMultipleEmptyRelationshipFilter;
use Filament\Tests\Fixtures\Models\Post;
use Filament\Tests\Fixtures\Models\User;
use Filament\Tests\Tables\TestCase;

use function Filament\Tests\livewire;

uses(TestCase::class);

it('can filter records with no relationship using `hasEmptyRelationshipOption`', function (): void {
$author = User::factory()->create();

$postsWithAuthor = Post::factory()->count(3)->create(['author_id' => $author->getKey()]);
$postsWithoutAuthor = Post::factory()->count(2)->create(['author_id' => null]);

livewire(PostsTableWithEmptyRelationshipFilter::class)
->assertCanSeeTableRecords($postsWithAuthor->merge($postsWithoutAuthor))
->filterTable('author', '__empty')
->assertCanSeeTableRecords($postsWithoutAuthor)
->assertCanNotSeeTableRecords($postsWithAuthor);
});

it('can filter records by specific relationship value using `hasEmptyRelationshipOption`', function (): void {
$author1 = User::factory()->create();
$author2 = User::factory()->create();

$postsWithAuthor1 = Post::factory()->count(3)->create(['author_id' => $author1->getKey()]);
$postsWithAuthor2 = Post::factory()->count(2)->create(['author_id' => $author2->getKey()]);
$postsWithoutAuthor = Post::factory()->count(2)->create(['author_id' => null]);

livewire(PostsTableWithEmptyRelationshipFilter::class)
->assertCanSeeTableRecords($postsWithAuthor1->merge($postsWithAuthor2)->merge($postsWithoutAuthor))
->filterTable('author', $author1->getKey())
->assertCanSeeTableRecords($postsWithAuthor1)
->assertCanNotSeeTableRecords($postsWithAuthor2)
->assertCanNotSeeTableRecords($postsWithoutAuthor);
});

it('can filter records with no relationship using `hasEmptyRelationshipOption` with `multiple()`', function (): void {
$author = User::factory()->create();

$postsWithAuthor = Post::factory()->count(3)->create(['author_id' => $author->getKey()]);
$postsWithoutAuthor = Post::factory()->count(2)->create(['author_id' => null]);

livewire(PostsTableWithMultipleEmptyRelationshipFilter::class)
->assertCanSeeTableRecords($postsWithAuthor->merge($postsWithoutAuthor))
->filterTable('author', ['__empty'])
->assertCanSeeTableRecords($postsWithoutAuthor)
->assertCanNotSeeTableRecords($postsWithAuthor);
});

it('can filter records by specific relationship values using `hasEmptyRelationshipOption` with `multiple()`', function (): void {
$author1 = User::factory()->create();
$author2 = User::factory()->create();
$author3 = User::factory()->create();

$postsWithAuthor1 = Post::factory()->count(2)->create(['author_id' => $author1->getKey()]);
$postsWithAuthor2 = Post::factory()->count(2)->create(['author_id' => $author2->getKey()]);
$postsWithAuthor3 = Post::factory()->count(2)->create(['author_id' => $author3->getKey()]);
$postsWithoutAuthor = Post::factory()->count(2)->create(['author_id' => null]);

livewire(PostsTableWithMultipleEmptyRelationshipFilter::class)
->assertCanSeeTableRecords($postsWithAuthor1->merge($postsWithAuthor2)->merge($postsWithAuthor3)->merge($postsWithoutAuthor))
->filterTable('author', [$author1->getKey(), $author2->getKey()])
->assertCanSeeTableRecords($postsWithAuthor1->merge($postsWithAuthor2))
->assertCanNotSeeTableRecords($postsWithAuthor3)
->assertCanNotSeeTableRecords($postsWithoutAuthor);
});

it('can filter records by relationship values combined with empty option using `hasEmptyRelationshipOption` with `multiple()`', function (): void {
$author1 = User::factory()->create();
$author2 = User::factory()->create();

$postsWithAuthor1 = Post::factory()->count(2)->create(['author_id' => $author1->getKey()]);
$postsWithAuthor2 = Post::factory()->count(2)->create(['author_id' => $author2->getKey()]);
$postsWithoutAuthor = Post::factory()->count(2)->create(['author_id' => null]);

livewire(PostsTableWithMultipleEmptyRelationshipFilter::class)
->assertCanSeeTableRecords($postsWithAuthor1->merge($postsWithAuthor2)->merge($postsWithoutAuthor))
->filterTable('author', ['__empty', $author1->getKey()])
->assertCanSeeTableRecords($postsWithAuthor1->merge($postsWithoutAuthor))
->assertCanNotSeeTableRecords($postsWithAuthor2);
});
Loading