diff --git a/packages/tables/src/Filters/SelectFilter.php b/packages/tables/src/Filters/SelectFilter.php index dda0cca0082..383b38ab1a6 100644 --- a/packages/tables/src/Filters/SelectFilter.php +++ b/packages/tables/src/Filters/SelectFilter.php @@ -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(); @@ -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(); } @@ -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())) @@ -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 { @@ -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); } @@ -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); @@ -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); @@ -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(); } @@ -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, [ @@ -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()) { @@ -604,4 +622,16 @@ public function getOptionLabelFromRecordUsing(?Closure $callback): static return $this; } + + /** + * @param string | array $values + * @return array + */ + 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, + )); + } } diff --git a/tests/src/Fixtures/Livewire/PostsTableWithEmptyRelationshipFilter.php b/tests/src/Fixtures/Livewire/PostsTableWithEmptyRelationshipFilter.php new file mode 100644 index 00000000000..73122659a0a --- /dev/null +++ b/tests/src/Fixtures/Livewire/PostsTableWithEmptyRelationshipFilter.php @@ -0,0 +1,39 @@ +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'); + } +} diff --git a/tests/src/Fixtures/Livewire/PostsTableWithMultipleEmptyRelationshipFilter.php b/tests/src/Fixtures/Livewire/PostsTableWithMultipleEmptyRelationshipFilter.php new file mode 100644 index 00000000000..305db89d302 --- /dev/null +++ b/tests/src/Fixtures/Livewire/PostsTableWithMultipleEmptyRelationshipFilter.php @@ -0,0 +1,40 @@ +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'); + } +} diff --git a/tests/src/Tables/Filters/SelectFilterTest.php b/tests/src/Tables/Filters/SelectFilterTest.php new file mode 100644 index 00000000000..6437ad80c75 --- /dev/null +++ b/tests/src/Tables/Filters/SelectFilterTest.php @@ -0,0 +1,86 @@ +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); +});